diff --git a/.gitignore b/.gitignore index 3b7ea3fc..968028b6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ gen/ .talismanrc/ .dccache/ .vscode/ +docs/ .idea/ .gradle/ diff --git a/.talismanrc b/.talismanrc index a8d2e18d..6acc6e78 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1 +1,7 @@ -threshold: high \ No newline at end of file +threshold: medium + +fileignoreconfig: + - filename: contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Spdy3.java + checksum: 5f6979f6336684787ff20dc5ccb0ea31665dcb4801845a34770d3bd30387f129 + - filename: contentstack/src/main/java/com/contentstack/okhttp/internal/DiskLruCache.java + checksum: 54a52cc9b0d897e500087e7cce65bd1c7c2615a82dad8f5942d87964b3ec4ab2 diff --git a/README.md b/README.md new file mode 100644 index 00000000..35fd89d3 --- /dev/null +++ b/README.md @@ -0,0 +1,277 @@ +[![Contentstack](https://www.contentstack.com/docs/static/images/contentstack.png)](https://www.contentstack.com/) + +# Android SDK for Contentstack + +Contentstack is a headless CMS with an API-first approach. +It is a CMS that developers can use to build powerful cross-platform applications +in their favorite languages. Build your application frontend, and Contentstack will +take care of the rest. [Read More](https://www.contentstack.com/). + +Contentstack provides Android SDK to build application on top of Android. +Given below is the detailed guide and helpful resources to get started with our Android SDK. + +## Prerequisite + +You will need one of the following: [Android Studio](https://developer.android.com/studio/install.html) +or [Eclipse](https://eclipse.org/downloads/eclipse-packages/?show_instructions=TRUE). + +### Setup and Installation + +To use the Contentstack Android SDK to your existing project, perform the steps given below: + +- **Gradle** + +```java +implementation 'com.contentstack.sdk:android:{version}' +``` + +- **Maven** + +```java + + com.contentstack.sdk + android + {version} + +``` + +Download updated version from [here](https://search.maven.org/artifact/com.contentstack.sdk/android) + +Or, + +To add the Contentstack Android SDK to your existing project manually, perform the steps given below: + +1. [Download the Android SDK](https://docs.contentstack.com/platforms/android/android_sdk_latest) + and extract the ZIP file to your local disk. +2. Add references/dependencies using Eclipse/Android Studio: + +### Android Studio + +- Copy the 'Contentstack-x.x.x.jar' file into your project's libs folder. +- Add the dependency code into your 'build.gradle' file. + +compile fileTree(dir: 'libs', include: ['*.jar']) + +### Eclipse + +- Copy the 'Contentstack-x.x.x-javadoc' folder and the 'Contentstack-x.x.x.jar' and 'Contentstack-x.x.x.jar.properties' files into your project's 'libs' folder. +- Open the 'Properties' window of the project. Select the 'Java Build Path' option on the left-hand side menu, click on the 'Libraries' tab, and add the JAR references there. +- Configure 'AndroidManifest.xml' with permissions and receivers using the following code: + + + + + + + + + + + + + + + + + + +To initialize the SDK, specify application context, the API key, access token, and environment name of the stack as shown in the snippet given below: + +```java +Stack stack = Contentstack.stack(context, "api_key", "access_token", "enviroment_name"); +``` + +Once you have initialized the SDK, you can query entries to fetch the required content. + +### Key Concepts for using Contentstack + +#### Stack + +A stack is like a container that holds the content of your app. Learn more about [Stacks](https://www.contentstack.com/docs/guide/stack). + +#### Content Type + +Content type lets you define the structure or blueprint of a page or a section of your digital property. It is a form-like page that gives Content Managers an interface to input and upload content. [Read more](https://www.contentstack.com/docs/guide/content-types). + +#### Entry + +An entry is the actual piece of content created using one of the defined content types. Learn more about [Entries](https://www.contentstack.com/docs/guide/content-management#working-with-entries). + +#### Asset + +Assets refer to all the media files (images, videos, PDFs, audio files, and so on) uploaded to Contentstack. These files can be used in multiple entries. Read more about [Assets](https://www.contentstack.com/docs/guide/content-management#working-with-assets). + +#### Environment + +A publishing environment corresponds to one or more deployment servers or a content delivery destination where the entries need to be published. Learn how to work with [Environments](https://www.contentstack.com/docs/guide/environments). + +### Contentstack Android SDK: 5-minute Quickstart + +#### Initializing your SDK + +To initialize the SDK, specify application context, the API key, access token, and environment name of the stack as shown in the snippet given below: + +```java +Stack stack = Contentstack.stack(context, "api_key", "access_token", "enviroment_name"); +``` + +Once you have initialized the SDK, you can query entries to fetch the required content. + +#### Querying content from your stack + +To retrieve a single entry from a content type use the code snippet given below: + +ContentType contentType = stack.contentType("content_type_uid"); + +```java +Entry blogEntry = contentType.entry("entry_uid"); +blogEntry.fetch(new EntryResultCallBack() { + @Override + public void onCompletion(ResponseType responseType, Error error) { + if (error == null) { + // Success block + } else { + // Error block + }} +}); +``` + +##### Get Multiple Entries + +To retrieve multiple entries of a particular content type, use the code snippet given below: + +```java +//stack is an instance of Stack class +Query blogQuery = stack.contentType("content_type_uid").query(); +blogQuery.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryResult, Error error) { + if(error == null){ + //Success block + }else{ + //Error block + }} +}); +``` + +### Advanced Queries + +You can query for content types, entries, assets and more using our Android API Reference. + +[Android API Reference Doc](https://www.contentstack.com/docs/platforms/android/api-reference/) + +### Working with Images + +We have introduced Image Delivery APIs that let you retrieve images and then manipulate and optimize them for your digital properties. It lets you perform a host of other actions such as crop, trim, resize, rotate, overlay, and so on. + +For example, if you want to crop an image (with width as 300 and height as 400), you simply need to append query parameters at the end of the image URL, such as, . There are several more parameters that you can use for your images. + +[Read Image Delivery API documentation](https://www.contentstack.com/docs/apis/image-delivery-api/). + +```java +// set the image quality to 100 +LinkedHashMap imageParams = new LinkedHashMap(); +imageParams.put("quality", 100); +imageUrl = Stack.ImageTransform(imageUrl, imageParams); + +// resize the image by specifying width and height +LinkedHashMap imageParams = new LinkedHashMap(); +imageParams.put("width", 100); +imageParams.put("height",100); +imageUrl = Stack.ImageTransform(imageUrl, imageParams); + +// enable auto optimization for the image +LinkedHashMap imageParams = new LinkedHashMap(); +imageParams.put("auto", "webp"); +imageUrl = Stack.ImageTransform(imageUrl, imageParams); +``` + +### Using the Sync API with Android SDK + +The Sync API takes care of syncing your Contentstack data with your app and ensures that the data is always up-to-date by providing delta updates. Contentstack’s Android SDK supports Sync API, which you can use to build powerful apps. Read through to understand how to use the Sync API with Contentstack Android SDK. + +### Initial Sync + +The Initial sync request performs a complete sync of your app data. It returns all the published entries and assets of the specified stack in response. To start the Initial sync process, use the syncInit method. + +```java +//stack is an instance of Stack class +stack.sync(new SyncResultCallBack() { + @Override + public void onCompletion(SyncStack syncStack,Error error) { + if(error == null){ + //Success block + }else{ + //Error block + }} +}); +``` + +### Pagination Sync + +If the result of the initial sync (or subsequent sync) contains more than 100 records, the response would be paginated. It provides pagination token in the response. However, you don’t have to use the pagination token manually to get the next batch; the SDK does that automatically until the sync is complete. + +Pagination token can be used in case you want to fetch only selected batches. It is especially useful if the sync process is interrupted midway (due to network issues, etc.). In such cases, this token can be used to restart the sync process from where it was interrupted. + +```java +//stack is an instance of Stack class +stack.syncPaginationToken("pagination_token", new SyncResultCallBack() { + @Override + public void onCompletion(SyncStack syncStack, Error error) { + if(error == null){ + //Success block + }else{ + //Error block + }} +}); +``` + +### Subsequent sync + +You can use the sync token (that you receive after initial sync) to get the updated content next time. The sync token fetches only the content that was added after your last sync, and the details of the content that was deleted or updated. + +```java +//stack is an instance of Stack class +stack.syncToken("sync_token", new SyncResultCallBack() { + @Override + public void onCompletion(SyncStack syncStack,Error error) { + if(error == null){ + //Success block + }else{ + //Error block + }} +}); +``` + +### Advanced sync queries + +You can use advanced sync queries to fetch custom results while performing initial sync. [Read advanced sync queries](http://www.contentstack.com/docs/guide/synchronization/using-the-sync-api-with-ios-sdk#advanced-sync-queries) documentation. + +### Helpful Links + +- [Contentstack Website](https://www.contentstack.com) +- [Official Documentation](https://contentstack.com/docs) +- [Content Delivery API Docs](https://contentstack.com/docs/apis/content-delivery-api/) + +### The MIT License (MIT) + +Copyright © 2012-2020 [Contentstack](https://www.contentstack.com/). All Rights Reserved + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/contentstack/src/androidTest/java/com/contentstack/sdk/AssetTestCase.java b/contentstack/src/androidTest/java/com/contentstack/sdk/AssetTestCase.java new file mode 100644 index 00000000..2cde9703 --- /dev/null +++ b/contentstack/src/androidTest/java/com/contentstack/sdk/AssetTestCase.java @@ -0,0 +1,168 @@ +package com.contentstack.sdk; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.util.Log; +import org.junit.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import static junit.framework.Assert.assertTrue; +import static junit.framework.TestCase.assertEquals; + + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class AssetTestCase { + + private final String TAG = AssetTestCase.class.getSimpleName(); + private static String assetUid = BuildConfig.assetUID; + private static Stack stack; + private static CountDownLatch latch; + + @BeforeClass + public static void oneTimeSetUp() throws Exception { + Context appContext = InstrumentationRegistry.getTargetContext(); + Config config = new Config(); + String DEFAULT_API_KEY = BuildConfig.APIKey; + String DEFAULT_DELIVERY_TOKEN = BuildConfig.deliveryToken; + String DEFAULT_ENV = BuildConfig.environment; + String DEFAULT_HOST = BuildConfig.host; + config.setHost(DEFAULT_HOST); + stack = Contentstack.stack(appContext, DEFAULT_API_KEY, DEFAULT_DELIVERY_TOKEN, DEFAULT_ENV, config); + } + + + @Test() + public void test_A_getAllAssetsToSetAssetUID() { + final AssetLibrary assetLibrary = stack.assetLibrary(); + assetLibrary.fetchAll(new FetchAssetsCallback() { + @Override + public void onCompletion(ResponseType responseType, List assets, Error error) { + if (error == null) { + Log.d(TAG, "response: " + assets.get(0).getAssetUid()); + assetUid = assets.get(0).getAssetUid(); + Log.e(assetUid , assetUid); + } + } + }); + + } + + @Test + public void test_B_VerifyAssetUID() { + + final Asset asset = stack.asset(assetUid); + asset.fetch(new FetchResultCallback() { + @Override + public void onCompletion(ResponseType responseType, Error error) { + if (error == null) { + // Success Block. + Log.d(TAG, "response: " + asset.getAssetUid()); + assertEquals(assetUid, asset.getAssetUid()); + } + } + }); + } + + @Test + public void test_C_Asset_fetch() { + final Asset asset = stack.asset(assetUid); + asset.fetch(new FetchResultCallback() { + @Override + public void onCompletion(ResponseType responseType, Error error) { + if (error == null) { + assertEquals(BuildConfig.assetUID, asset.getAssetUid()); + assertEquals("image/jpeg", asset.getFileType()); + assertEquals("phoenix2.jpg", asset.getFileName()); + assertEquals("482141", asset.getFileSize()); + } else { + assertEquals(145, error.getErrorCode()); + } + } + }); + } + + @Test + public void test_D_AssetLibrary_fetch() { + final AssetLibrary assetLibrary = stack.assetLibrary(); + assetLibrary.fetchAll(new FetchAssetsCallback() { + @Override + public void onCompletion(ResponseType responseType, List assets, Error error) { + if (error == null) { + assets.forEach(asset -> { + Log.d(TAG, "----Test--Asset-D--Success----" + asset.toJSON()); + Log.d(TAG, "----Test--Asset-D--Success----" + asset.getFileType()); + Log.d(TAG, "----Test--Asset-D--Success----" + asset.getCreatedBy()); + Log.d(TAG, "----Test--Asset-D--Success----" + asset.getUpdatedBy()); + Log.d(TAG, "----Test--Asset-D--Success----" + asset.getFileName()); + Log.d(TAG, "----Test--Asset-D--Success----" + asset.getFileSize()); + Log.d(TAG, "----Test--Asset-D--Success----" + asset.getAssetUid()); + Log.d(TAG, "----Test--Asset-D--Success----" + asset.getUrl()); + }); + } + } + }); + } + + @Test + public void test_E_AssetLibrary_includeCount_fetch() { + final AssetLibrary assetLibrary = stack.assetLibrary(); + assetLibrary.includeCount(); + assetLibrary.fetchAll(new FetchAssetsCallback() { + @Override + public void onCompletion(ResponseType responseType, List assets, Error error) { + if (error == null) { + assertEquals(16, assetLibrary.getCount()); + } + } + }); + } + + @Test + public void test_F_AssetLibrary_includeRelativeUrl_fetch() { + final AssetLibrary assetLibrary = stack.assetLibrary(); + assetLibrary.includeRelativeUrl(); + assetLibrary.fetchAll(new FetchAssetsCallback() { + public void onCompletion(ResponseType responseType, List assets, Error error) { + if (error == null) { + assertTrue( assets.get(0).getUrl().contains("phoenix2.jpg")); + } + } + }); + } + + @Test + public void test_G_Include_Dimension() { + final Asset asset = stack.asset(assetUid); + asset.includeDimension(); + asset.fetch(new FetchResultCallback() { + @Override + public void onCompletion(ResponseType responseType, Error error) { + if (error == null) { + Log.d(TAG, asset.getAssetUid()); + assertEquals(assetUid, asset.getAssetUid()); + } + } + }); + } + + + @Test + public void test_H_include_fallback() { + final Asset asset = stack.asset(assetUid); + asset.includeFallback(); + asset.fetch(new FetchResultCallback() { + @Override + public void onCompletion(ResponseType responseType, Error error) { + if (error == null) { + Log.d(TAG, asset.getAssetUid()); + assertEquals(assetUid, asset.getAssetUid()); + } + } + }); + } + +} diff --git a/contentstack/src/androidTest/java/com/contentstack/sdk/EntryTestCase.java b/contentstack/src/androidTest/java/com/contentstack/sdk/EntryTestCase.java new file mode 100644 index 00000000..877b3682 --- /dev/null +++ b/contentstack/src/androidTest/java/com/contentstack/sdk/EntryTestCase.java @@ -0,0 +1,319 @@ +package com.contentstack.sdk; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.*; +import org.junit.runners.MethodSorters; + +import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; + +import static junit.framework.TestCase.*; + + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class EntryTestCase { + + private final static String TAG = EntryTestCase.class.getSimpleName(); + private static String entryUID; + private static final String CONTENT_TYPE_UID = BuildConfig.contentTypeUID; + private static CountDownLatch latch; + private static Stack stack; + + + @BeforeClass + public static void oneTimeSetUp() throws Exception { + Context appContext = InstrumentationRegistry.getTargetContext(); + Config config = new Config(); + String DEFAULT_HOST = BuildConfig.host; + config.setHost(DEFAULT_HOST); + stack = Contentstack.stack(appContext, BuildConfig.APIKey, BuildConfig.deliveryToken, BuildConfig.environment, config); + + latch = new CountDownLatch(1); + Log.d(TAG, "test started..."); + } + + @AfterClass + public static void oneTimeTearDown() { + // one-time cleanup code + Log.d(TAG, "When all the test cases of class finishes..."); + Log.d(TAG, "Total testcase: " + latch.getCount()); + } + + /** + * Sets up the test fixture. + * (Called before every test case method.) + */ + @Before + public void setUp() { + latch = new CountDownLatch(1); + } + + + /** + * Tears down the test fixture. + * (Called after every test case method.) + */ + @After + public void tearDown() { + Log.d(TAG, "Runs after every testcase completes."); + latch.countDown(); + } + + + @Test + public void test_01_findAllEntries() { + final Query query = stack.contentType(CONTENT_TYPE_UID).query(); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + entryUID = queryresult.getResultObjects().get(15).getUid(); + } + } + }); + } + + @Test + public void test_02_only_fetch() { + final Entry entry = stack.contentType(CONTENT_TYPE_UID).entry(entryUID); + entry.only(new String[]{"price"}); + entry.fetch(new EntryResultCallBack() { + @Override + public void onCompletion(ResponseType responseType, Error error) { + if (error == null) { + assertEquals(786, entry.toJSON().opt("price")); + } + } + }); + } + + @Test + public void test_03_except_fetch() { + final Entry entry = stack.contentType(CONTENT_TYPE_UID).entry(entryUID); + entry.except(new String[]{"title"}); + entry.fetch(new EntryResultCallBack() { + @Override + public void onCompletion(ResponseType responseType, Error error) { + if (error == null) { + Log.e(TAG, entry.toJSON().optString("title")); + } else { + Log.e(TAG, error.getErrorMessage()); + } + } + }); + } + + @Test + public void test_04_includeReference_fetch() { + final Entry entry = stack.contentType(CONTENT_TYPE_UID).entry(entryUID); + entry.includeReference("category"); + entry.fetch(new EntryResultCallBack() { + @Override + public void onCompletion(ResponseType responseType, Error error) { + if (error == null) { + JSONArray categoryArray = entry.getJSONArray("category"); + + try { + for (int index = 0; index < categoryArray.length(); index++) { + JSONObject array = (JSONObject) categoryArray.get(index); + assertTrue(array.toString().contains("_content_type_uid")); + } + } catch (Exception e) { + Log.e(TAG, e.getLocalizedMessage()); + } + + } + } + }); + } + + @Test + public void test_05_includeReferenceOnly_fetch() { + final Entry entry = stack.contentType(CONTENT_TYPE_UID).entry(entryUID); + ArrayList strings = new ArrayList<>(); + strings.add("title"); + strings.add("orange"); + strings.add("mango"); + entry.onlyWithReferenceUid(strings, "category"); + entry.fetch(new EntryResultCallBack() { + @Override + public void onCompletion(ResponseType responseType, Error error) { + if (error == null) { + assertEquals("laptop", entry.toJSON().optString("title")); + } + } + }); + + } + + + @Test + public void test_06_includeReferenceExcept_fetch() throws InterruptedException { + final Entry entry = stack.contentType(CONTENT_TYPE_UID).entry(entryUID); + ArrayList strings = new ArrayList<>(); + strings.add("color"); + strings.add("price_in_usd"); + entry.exceptWithReferenceUid(strings, "category"); + entry.fetch(new EntryResultCallBack() { + @Override + public void onCompletion(ResponseType responseType, Error error) { + if (error == null) { + latch.countDown(); + } else { + latch.countDown(); + } + + } + }); + latch.await(); + + } + + + @Test + public void test_07_getMarkdown_fetch() throws InterruptedException { + + final Entry entry = stack.contentType("user").entry(entryUID); + entry.fetch(new EntryResultCallBack() { + @Override + public void onCompletion(ResponseType responseType, Error error) { + if (error == null) { + latch.countDown(); + } else { + latch.countDown(); + } + } + }); + latch.await(); + } + + + @Test + public void test_08_get() throws InterruptedException { + final Entry entry = stack.contentType("user").entry(entryUID); + entry.fetch(new EntryResultCallBack() { + @Override + public void onCompletion(ResponseType responseType, Error error) { + if (error == null) { + latch.countDown(); + } else { + latch.countDown(); + } + } + }); + latch.await(); + } + + + @Test + public void test_09_getParam() throws InterruptedException { + final Entry entry = stack.contentType("user").entry(entryUID); + entry.addParam("include_dimensions", "true"); + entry.fetch(new EntryResultCallBack() { + @Override + public void onCompletion(ResponseType responseType, Error error) { + if (error == null) { + latch.countDown(); + } else { + latch.countDown(); + } + } + }); + latch.await(); + } + + + @Test + public void test_10_IncludeReferenceContentTypeUID() throws InterruptedException { + final Entry entry = stack.contentType("user").entry(entryUID); + entry.includeReferenceContentTypeUID(); + entry.fetch(new EntryResultCallBack() { + @Override + public void onCompletion(ResponseType responseType, Error error) { + if (error == null) { + JSONObject jsonResult = entry.toJSON(); + try { + JSONArray cartList = (JSONArray) jsonResult.get("cart"); + Object whatTYPE = cartList.get(0); + if (whatTYPE instanceof JSONObject) { + assertTrue(true); + } + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + latch.countDown(); + } else { + latch.countDown(); + } + } + }); + latch.await(); + + } + + + @Test + public void test_11_Locale() throws InterruptedException { + final Entry entry = stack.contentType("user").entry(entryUID); + entry.fetch(new EntryResultCallBack() { + @Override + public void onCompletion(ResponseType responseType, Error error) { + if (error == null) { + String checkResp = entry.getLocale(); + Log.e(TAG, checkResp); + latch.countDown(); + } else { + latch.countDown(); + } + + } + }); + latch.await(); + } + + @Test + public void test_12_entry_except() throws InterruptedException { + final Entry entry = stack.contentType("user").entry(entryUID); + String[] allValues = {"color", "price_in_usd"}; + entry.except(allValues); + entry.fetch(new EntryResultCallBack() { + @Override + public void onCompletion(ResponseType responseType, Error error) { + if (error == null) { + String checkResp = entry.getLocale(); + Log.d(TAG, checkResp); + latch.countDown(); + } else { + latch.countDown(); + } + } + }); + latch.await(); + } + + @Test + public void test_13_entry_include_embedded_items_unit_test() throws InterruptedException { + + final Entry entry = stack.contentType("user").entry(entryUID); + entry.includeEmbeddedItems().fetch(new EntryResultCallBack() { + @Override + public void onCompletion(ResponseType responseType, Error error) { + if (error == null) { + String checkResp = entry.getLocale(); + Log.d(TAG, checkResp); + } + boolean hasEmbeddedItemKey = entry.otherPostJSON.has("include_embedded_items[]"); + Assert.assertTrue(hasEmbeddedItemKey); + latch.countDown(); + } + }); + latch.await(); + } + +} diff --git a/contentstack/src/androidTest/java/com/contentstack/sdk/ImageTransformTestcase.java b/contentstack/src/androidTest/java/com/contentstack/sdk/ImageTransformTestcase.java new file mode 100644 index 00000000..9b698029 --- /dev/null +++ b/contentstack/src/androidTest/java/com/contentstack/sdk/ImageTransformTestcase.java @@ -0,0 +1,237 @@ +package com.contentstack.sdk; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.util.Log; + +import org.junit.*; +import org.junit.runner.JUnitCore; + +import java.util.LinkedHashMap; +import java.util.concurrent.CountDownLatch; + + +public class ImageTransformTestcase { + + static final String TAG = ImageTransformTestcase.class.getSimpleName(); + private static CountDownLatch latch; + private static Stack stack; + private final LinkedHashMap imageParams = new LinkedHashMap<>(); + private final String IMAGE_URL = "https://images.contentstack.io/v3/assets/download"; + + + @BeforeClass + public static void oneTimeSetUp() throws Exception { + Context appContext = InstrumentationRegistry.getTargetContext(); + + Config config = new Config(); + String DEFAULT_API_KEY = BuildConfig.APIKey; + String DEFAULT_DELIVERY_TOKEN = BuildConfig.deliveryToken; + String DEFAULT_ENV = BuildConfig.environment; + String DEFAULT_HOST = BuildConfig.host; + config.setHost(DEFAULT_HOST); + stack = Contentstack.stack(appContext, DEFAULT_API_KEY, DEFAULT_DELIVERY_TOKEN, DEFAULT_ENV, config); + + latch = new CountDownLatch(1); + } + + @Before + public void setUp() { + latch = new CountDownLatch(1); + } + + + @Test + public void test_00_fetchAllImageTransformation() { + + imageParams.put("auto", "webp"); + imageParams.put("quality", 200); + imageParams.put("width", 100); + imageParams.put("height", 50); + imageParams.put("format", "png"); + imageParams.put("crop", "3:5"); + imageParams.put("trim", "20,20,20,20"); + imageParams.put("disable", "upscale"); + imageParams.put("pad", "10,10,10,10"); + imageParams.put("bg-color", "#FFFFFF"); + imageParams.put("dpr", 20); + imageParams.put("canvas", "3:5"); + imageParams.put("orient", "l"); + + String image_url = stack.ImageTransform(IMAGE_URL, imageParams); + int counter = 0; + if (!image_url.equalsIgnoreCase("") && image_url.contains("?")) { + String[] imgKeys = image_url.split("\\?"); + String rightUrl = imgKeys[1]; + String[] getAllPairs = rightUrl.split("\\&"); + counter = 0; + if (imageParams.size() > 0) { + for (int i = 0; i < imageParams.size(); i++) { + String keyValueParis = getAllPairs[i]; + Log.i(TAG, "pairs:--> " + keyValueParis); + ++counter; + } + } + } else { + try { + latch.await(); + } catch (Exception e) { + Log.i(TAG, "---------------||" + e.toString()); + } + } + + if (counter == imageParams.size()) { + latch.countDown(); + Log.i(TAG, "Testcases Passed"); + } else { + Log.i(TAG, "Testcases Failed"); + try { + latch.await(); + } catch (Exception e) { + Log.i(TAG, "---------------||" + e.toString()); + } + } + } + + + @Test + public void test_01_fetchAllImageTransformation() { + + imageParams.put("auto", "webp"); + imageParams.put("quality", 200); + imageParams.put("width", 100); + imageParams.put("height", 50); + imageParams.put("format", "png"); + imageParams.put("crop", "3:5"); + + String image_url = stack.ImageTransform(IMAGE_URL, imageParams); + int counter = 0; + /* check url contains "?" */ + if (!image_url.equalsIgnoreCase("") && image_url.contains("?")) { + String[] imgKeys = image_url.split("\\?"); + String rightUrl = imgKeys[1]; + String[] getAllPairs = rightUrl.split("\\&"); + counter = 0; + if (imageParams.size() > 0) { + for (int i = 0; i < imageParams.size(); i++) { + String keyValueParis = getAllPairs[i]; + Log.i(TAG, "pairs:--> " + keyValueParis); + ++counter; + } + } + } else { + Log.i(TAG, "Testcases Failed"); + try { + latch.await(); + } catch (Exception e) { + Log.i(TAG, "---------------||" + e.toString()); + } + } + + if (counter == imageParams.size()) { + latch.countDown(); + Log.i(TAG, "Testcases Passed"); + } else { + Log.i(TAG, "Testcases Failed"); + try { + latch.await(); + } catch (Exception e) { + Log.i(TAG, "---------------||" + e.toString()); + } + } + } + + + @Test + public void test_02_fetchAllImageTransformation() { + imageParams.put("trim", "20,20,20,20"); + imageParams.put("disable", "upscale"); + imageParams.put("pad", "10,10,10,10"); + imageParams.put("bg-color", "#FFFFFF"); + imageParams.put("dpr", 20); + imageParams.put("canvas", "3:5"); + imageParams.put("orient", "l"); + String image_url = stack.ImageTransform(IMAGE_URL, imageParams); + int counter = 0; + /* check url contains "?" */ + if (!image_url.equalsIgnoreCase("") && image_url.contains("?")) { + String[] imgKeys = image_url.split("\\?"); + String rightUrl = imgKeys[1]; + String[] getAllPairs = rightUrl.split("\\&"); + counter = 0; + if (imageParams.size() > 0) { + for (int i = 0; i < imageParams.size(); i++) { + String keyValueParis = getAllPairs[i]; + Log.i(TAG, "pairs:--> " + keyValueParis); + ++counter; + } + } + } else { + Log.i(TAG, "Testcases Failed"); + try { + latch.await(); + } catch (Exception e) { + Log.i(TAG, "---------------||" + e.toString()); + } + } + + if (counter == imageParams.size()) { + latch.countDown(); + Log.i(TAG, "Testcases Passed"); + } else { + Log.i(TAG, "Testcases Failed"); + try { + latch.await(); + } catch (Exception e) { + Log.i(TAG, "---------------||" + e.toString()); + } + } + } + + + @Test + public void test_03_fetchAllImageTransformation() { + + + imageParams.put("trim", "20,20,20,20"); + imageParams.put("disable", "upscale"); + imageParams.put("canvas", "3:5"); + imageParams.put("orient", "l"); + + String image_url = stack.ImageTransform(IMAGE_URL, imageParams); + int counter = 0; + /* check url contains "?" */ + if (!image_url.equalsIgnoreCase("") && image_url.contains("?")) { + String[] imgKeys = image_url.split("\\?"); + String rightUrl = imgKeys[1]; + String[] getAllPairs = rightUrl.split("\\&"); + counter = 0; + if (imageParams.size() > 0) { + for (int i = 0; i < imageParams.size(); i++) { + String keyValueParis = getAllPairs[i]; + Log.i(TAG, "pairs:--> " + keyValueParis); + ++counter; + } + } + } else { + Log.i(TAG, "Testcases Failed"); + try { + latch.await(); + } catch (Exception e) { + Log.i(TAG, "---------------||" + e.toString()); + } + } + + if (counter == imageParams.size()) { + latch.countDown(); + Log.i(TAG, "Testcases Passed"); + } else { + Log.i(TAG, "Testcases Failed"); + try { + latch.await(); + } catch (Exception e) { + Log.i(TAG, "---------------||" + e.toString()); + } + } + } +} diff --git a/contentstack/src/androidTest/java/com/contentstack/sdk/QueryTestCase.java b/contentstack/src/androidTest/java/com/contentstack/sdk/QueryTestCase.java new file mode 100644 index 00000000..7cbfb3df --- /dev/null +++ b/contentstack/src/androidTest/java/com/contentstack/sdk/QueryTestCase.java @@ -0,0 +1,640 @@ +package com.contentstack.sdk; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.*; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import static junit.framework.TestCase.assertEquals; + + +public class QueryTestCase { + + private static final String TAG = AssetTestCase.class.getSimpleName(); + private static Stack stack; + private static Query query; + private static final String contentTypeUID = BuildConfig.contentTypeUID; + + @BeforeClass + public static void oneTimeSetUp() throws Exception { + Context appContext = InstrumentationRegistry.getTargetContext(); + Config config = new Config(); + String DEFAULT_API_KEY = BuildConfig.APIKey; + String DEFAULT_DELIVERY_TOKEN = BuildConfig.deliveryToken; + String DEFAULT_ENV = BuildConfig.environment; + String DEFAULT_HOST = BuildConfig.host; + config.setHost(DEFAULT_HOST); + stack = Contentstack.stack(appContext, DEFAULT_API_KEY, DEFAULT_DELIVERY_TOKEN, DEFAULT_ENV, config); + query = stack.contentType(contentTypeUID).query(); + } + + + /** + * Sets up the test fixture. + * (Called before every test case method.) + */ + @Before + public void setUp() { + query = stack.contentType(contentTypeUID).query(); + } + + + @Test + public void test_01_fetchAllEntries() { + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List listOfEntries = queryresult.getResultObjects(); + } + } + }); + } + + @Test() + public void test_03_fetchSingleNonExistingEntry() { + Query query = stack.contentType("categories").query(); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List listOfEntries = queryresult.getResultObjects(); + } + } + }); + } + + @Test + public void test_04_fetchEntryWithIncludeReference() { + query.includeReference("category"); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List listOfEntries = queryresult.getResultObjects(); + } + } + }); + } + + + @Test + public void test_05_fetchEntryNotContainedInField() { + String[] containArray = new String[]{"Roti Maker", "kids dress"}; + query.notContainedIn("title", containArray); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + + if (error == null) { + List entries = queryresult.getResultObjects(); + int price = entries.get(0).toJSON().optInt("price"); + assertEquals(45, price); + } + } + }); + } + + + @Test + public void test_07_fetchEntryNotEqualToField() { + query.notEqualTo("title", "yellow t shirt"); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List entries = queryresult.getResultObjects(); + entries.forEach(entry -> Log.i(TAG, entry.getString("title"))); + } + } + }); + } + + + @Test + public void test_08_fetchEntryGreaterThanEqualToField() { + query.greaterThanOrEqualTo("price", 90); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List entries = queryresult.getResultObjects(); + entries.forEach(entry -> { + //og.i(TAG,entry.getString("price")); + }); + } + } + }); + } + + + @Test + public void test_09_fetchEntryGreaterThanField() { + query.greaterThan("price", 90); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List entries = queryresult.getResultObjects(); + entries.forEach(entry -> { + //Log.i(TAG,entry.getString("price"); + }); + } + } + }); + } + + + @Test + public void test_10_fetchEntryLessThanEqualField() { + query.lessThanOrEqualTo("price", 90); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List entries = queryresult.getResultObjects(); + int price = entries.get(0).toJSON().optInt("price"); + assertEquals(45, price); + } + } + }); + } + + + @Test + public void test_11_fetchEntryLessThanField() { + query.lessThan("price", "90"); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List resp = queryresult.getResultObjects(); + resp.forEach(entry -> { + Log.i(TAG, "Is price less than 90..? " + entry.get("price")); + }); + } + } + }); + } + + + @Test + public void test_12_fetchEntriesWithOr() { + + ContentType ct = stack.contentType(contentTypeUID); + Query orQuery = ct.query(); + + Query query = ct.query(); + query.lessThan("price", 90); + + Query subQuery = ct.query(); + subQuery.containedIn("discount", new Integer[]{20, 45}); + + ArrayList array = new ArrayList(); + array.add(query); + array.add(subQuery); + + orQuery.or(array); + + orQuery.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List listOfEntries = queryresult.getResultObjects(); + } + } + }); + } + + + @Test + public void test_13_fetchEntriesWithAnd() { + + ContentType ct = stack.contentType(contentTypeUID); + Query orQuery = ct.query(); + + Query query = ct.query(); + query.lessThan("price", 90); + + Query subQuery = ct.query(); + subQuery.containedIn("discount", new Integer[]{20, 45}); + + ArrayList array = new ArrayList(); + array.add(query); + array.add(subQuery); + + orQuery.and(array); + orQuery.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List listOfEntries = queryresult.getResultObjects(); + } + } + }); + } + + + @Test + public void test_14_addQuery() { + query.addQuery("limit", "8"); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List listOfEntries = queryresult.getResultObjects(); + } + } + }); + } + + + @Test + public void test_15_removeQueryFromQuery() { + query.addQuery("limit", "8"); + query.removeQuery("limit"); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List listOfEntries = queryresult.getResultObjects(); + } + } + }); + } + + + @Test + public void test_16_includeSchema() { + query.includeContentType(); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + JSONObject contentTypeObj = queryresult.getContentType(); + } + } + }); + } + + + @Test + public void test_17_search() { + query.search("dress"); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List entries = queryresult.getResultObjects(); + for (Entry entry : entries) { + JSONObject jsonObject = entry.toJSON(); + Iterator iter = jsonObject.keys(); + while (iter.hasNext()) { + String key = iter.next(); + try { + Object value = jsonObject.opt(key); + if (value instanceof String && ((String) value).contains("dress")) + Log.i(TAG, value.toString()); + } catch (Exception e) { + Log.i(TAG, "----------------setQueryJson" + e.toString()); + } + } + } + } + } + }); + } + + + @Test + public void test_18_ascending() { + query.ascending("title"); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List entries = queryresult.getResultObjects(); + for (Entry entry : entries) { + Log.i(TAG, entry.getString("title")); + } + } + } + }); + } + + + @Test + public void test_19_descending() { + query.descending("title"); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List entries = queryresult.getResultObjects(); + for (Entry entry : entries) { + Log.i(TAG, entry.getString("title")); + } + } + } + }); + } + + + @Test + public void test_20_limit() { + query.limit(3); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List entries = queryresult.getResultObjects(); + for (Entry entry : entries) { + Log.i(TAG, " entry = [" + entry.getString("title") + "]"); + } + } + } + }); + } + + + @Test + public void test_21_skip() { + query.skip(3); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List entries = queryresult.getResultObjects(); + } + } + }); + } + + + @Test + public void test_22_only() { + query.only(new String[]{"price"}); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List entries = queryresult.getResultObjects(); + } + } + }); + } + + + @Test + public void test_23_except() { + query.locale("en-eu"); + query.except(new String[]{"price", "chutiya"}); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List entries = queryresult.getResultObjects(); + } + } + }); + } + + + @Test + public void test_24_count() { + query.count(); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + int count = queryresult.getCount(); + } + } + }); + } + + + @Test + public void test_25_regex() { + query.regex("title", "lap*", "i"); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List entries = queryresult.getResultObjects(); + } + } + }); + } + + + @Test + public void test_26_exist() { + query.exists("title"); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List entries = queryresult.getResultObjects(); + } + } + }); + } + + + @Test + public void test_27_notExist() { + query.notExists("price1"); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + int entryCount = queryresult.getCount(); + } + } + }); + + + } + + + @Test + public void test_28_tags() { + query.tags(new String[]{"pink"}); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List entries = queryresult.getResultObjects(); + } + } + }); + + + } + + + @Test + public void test_29_language() { + query.language(Language.ENGLISH_UNITED_KINGDOM); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List entries = queryresult.getResultObjects(); + } + } + }); + + + } + + + @Test + public void test_33_findOne() { + query.includeCount(); + query.where("in_stock", true); + query.findOne(new SingleQueryResultCallback() { + @Override + public void onCompletion(ResponseType responseType, Entry entry, Error error) { + if (error == null) { + System.out.println("entry: " + entry); + } + } + }); + } + + + @Test + public void test_34_complexFind() { + query.notEqualTo("title", "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.*************************************Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.*************************************Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.*************************************Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.*************************************Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.************************************Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.************************************Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.************************************Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.************************************Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.************************************Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.************************************Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.************************************Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.************************************Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.*******"); + query.includeCount(); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + List entries = queryresult.getResultObjects(); + } + } + }); + } + + + @Test + public void test_35_includeSchema() { + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + JSONArray result; + if (error == null) { + result = queryresult.getSchema(); + } + } + }); + } + + + @Test + public void test_36_includeContentType() { + query.includeContentType(); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + JSONObject entries = queryresult.getContentType(); + } + } + }); + } + + + @Test + public void test_38_include_content_type() { + query.includeContentType(); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + JSONObject result; + if (error == null) { + result = queryresult.getContentType(); + } + } + }); + } + + + @Test + public void test_39_include_content_type() { + query.includeContentType(); + query.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + JSONObject entries = queryresult.getContentType(); + } + } + }); + } + + + @Test + public void test_40_WithoutIncludeFallback() { + Query fallbackQuery = stack.contentType("categories").query(); + fallbackQuery.locale("hi-in"); + fallbackQuery.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + assertEquals(0, queryresult.getResultObjects().size()); + fallbackQuery.includeFallback().locale("hi-in"); + fallbackQuery.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + assertEquals(0, queryresult.getResultObjects().size()); + } + }); + } + } + }); + } + + @Test + public void test_40_WithIncludeFallback() { + Query fallbackQuery = stack.contentType("categories").query(); + fallbackQuery.locale("hi-in"); + fallbackQuery.find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + assertEquals(0, queryresult.getResultObjects().size()); + } + } + }); + } + + + @Test + public void test_41_entry_include_embedded_items_unit_test() throws InterruptedException { + + final Query query = stack.contentType("user").query(); + query.includeEmbeddedItems().find(new QueryResultsCallBack() { + @Override + public void onCompletion(ResponseType responseType, QueryResult queryresult, Error error) { + if (error == null) { + Entry checkResp = queryresult.getResultObjects().get(0); + Log.d(TAG, checkResp.toString()); + } + boolean hasEmbeddedItemKey = query.mainJSON.has("include_embedded_items[]"); + Assert.assertTrue(hasEmbeddedItemKey); + } + }); + } + +} \ No newline at end of file diff --git a/contentstack/src/main/AndroidManifest.xml b/contentstack/src/main/AndroidManifest.xml new file mode 100755 index 00000000..ecbfcf7a --- /dev/null +++ b/contentstack/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/contentstack/src/main/java/com/contentstack/okhttp/Address.java b/contentstack/src/main/java/com/contentstack/okhttp/Address.java new file mode 100755 index 00000000..ae8c9469 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/Address.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp; + +import com.contentstack.okhttp.internal.Util; + +import java.net.Proxy; +import java.util.List; + +import javax.net.SocketFactory; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSocketFactory; + +/** + * A specification for a connection to an origin server. For simple connections, + * this is the server's hostname and port. If an explicit proxy is requested (or + * {@linkplain Proxy#NO_PROXY no proxy} is explicitly requested), this also includes + * that proxy information. For secure connections the address also includes the + * SSL socket factory and hostname verifier. + * + *

HTTP requests that share the same {@code Address} may also share the same + * {@link Connection}. + */ +public final class Address { + final Proxy proxy; + final String uriHost; + final int uriPort; + final SocketFactory socketFactory; + final SSLSocketFactory sslSocketFactory; + final HostnameVerifier hostnameVerifier; + final Authenticator authenticator; + final List protocols; + + public Address(String uriHost, int uriPort, SocketFactory socketFactory, + SSLSocketFactory sslSocketFactory, HostnameVerifier hostnameVerifier, + Authenticator authenticator, Proxy proxy, List protocols) { + if (uriHost == null) throw new NullPointerException("uriHost == null"); + if (uriPort <= 0) throw new IllegalArgumentException("uriPort <= 0: " + uriPort); + if (authenticator == null) throw new IllegalArgumentException("authenticator == null"); + if (protocols == null) throw new IllegalArgumentException("protocols == null"); + this.proxy = proxy; + this.uriHost = uriHost; + this.uriPort = uriPort; + this.socketFactory = socketFactory; + this.sslSocketFactory = sslSocketFactory; + this.hostnameVerifier = hostnameVerifier; + this.authenticator = authenticator; + this.protocols = Util.immutableList(protocols); + } + + /** Returns the hostname of the origin server. */ + public String getUriHost() { + return uriHost; + } + + /** + * Returns the port of the origin server; typically 80 or 443. Unlike + * may {@code getPort()} accessors, this method never returns -1. + */ + public int getUriPort() { + return uriPort; + } + + /** Returns the socket factory for new connections. */ + public SocketFactory getSocketFactory() { + return socketFactory; + } + + /** + * Returns the SSL socket factory, or null if this is not an HTTPS + * address. + */ + public SSLSocketFactory getSslSocketFactory() { + return sslSocketFactory; + } + + /** + * Returns the hostname verifier, or null if this is not an HTTPS + * address. + */ + public HostnameVerifier getHostnameVerifier() { + return hostnameVerifier; + } + + /** + * Returns the client's authenticator. This method never returns null. + */ + public Authenticator getAuthenticator() { + return authenticator; + } + + /** + * Returns the protocols the client supports. This method always returns a + * non-null list that contains minimally {@link Protocol#HTTP_1_1}. + */ + public List getProtocols() { + return protocols; + } + + /** + * Returns this address's explicitly-specified HTTP proxy, or null to + * delegate to the HTTP client's proxy selector. + */ + public Proxy getProxy() { + return proxy; + } + + @Override public boolean equals(Object other) { + if (other instanceof Address) { + Address that = (Address) other; + return Util.equal(this.proxy, that.proxy) + && this.uriHost.equals(that.uriHost) + && this.uriPort == that.uriPort + && Util.equal(this.sslSocketFactory, that.sslSocketFactory) + && Util.equal(this.hostnameVerifier, that.hostnameVerifier) + && Util.equal(this.authenticator, that.authenticator) + && Util.equal(this.protocols, that.protocols); + } + return false; + } + + @Override public int hashCode() { + int result = 17; + result = 31 * result + uriHost.hashCode(); + result = 31 * result + uriPort; + result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0); + result = 31 * result + (hostnameVerifier != null ? hostnameVerifier.hashCode() : 0); + result = 31 * result + authenticator.hashCode(); + result = 31 * result + (proxy != null ? proxy.hashCode() : 0); + result = 31 * result + protocols.hashCode(); + return result; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/Authenticator.java b/contentstack/src/main/java/com/contentstack/okhttp/Authenticator.java new file mode 100755 index 00000000..f204f805 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/Authenticator.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp; + +import java.io.IOException; +import java.net.Proxy; + +/** + * Responds to authentication challenges from the remote web or proxy server. + */ +public interface Authenticator { + /** + * Returns a request that includes a credential to satisfy an authentication + * challenge in {@code response}. Returns null if the challenge cannot be + * satisfied. This method is called in response to an HTTP 401 unauthorized + * status code sent by the origin server. + * + *

Typical implementations will look up a credential and create a request + * derived from the initial request by setting the "Authorization" header. + *

   {@code
+   *
+   *    String credential = Credentials.basic(...)
+   *    return response.request().newBuilder()
+   *        .header("Authorization", credential)
+   *        .build();
+   * }
+ */ + Request authenticate(Proxy proxy, Response response) throws IOException; + + /** + * Returns a request that includes a credential to satisfy an authentication + * challenge made by {@code response}. Returns null if the challenge cannot be + * satisfied. This method is called in response to an HTTP 407 unauthorized + * status code sent by the proxy server. + * + *

Typical implementations will look up a credential and create a request + * derived from the initial request by setting the "Proxy-Authorization" + * header.

   {@code
+   *
+   *    String credential = Credentials.basic(...)
+   *    return response.request().newBuilder()
+   *        .header("Proxy-Authorization", credential)
+   *        .build();
+   * }
+ */ + Request authenticateProxy(Proxy proxy, Response response) throws IOException; +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/Cache.java b/contentstack/src/main/java/com/contentstack/okhttp/Cache.java new file mode 100755 index 00000000..00aa4494 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/Cache.java @@ -0,0 +1,616 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contentstack.okhttp; + +import com.contentstack.okhttp.internal.DiskLruCache; +import com.contentstack.okhttp.internal.InternalCache; +import com.contentstack.okhttp.internal.Util; +import com.contentstack.okhttp.internal.http.CacheRequest; +import com.contentstack.okhttp.internal.http.CacheStrategy; +import com.contentstack.okhttp.internal.http.HttpMethod; +import com.contentstack.okhttp.internal.http.OkHeaders; +import com.contentstack.okhttp.internal.http.StatusLine; +import com.contentstack.okio.BufferedSink; +import com.contentstack.okio.BufferedSource; +import com.contentstack.okio.ByteString; +import com.contentstack.okio.ForwardingSink; +import com.contentstack.okio.ForwardingSource; +import com.contentstack.okio.Okio; +import com.contentstack.okio.Sink; +import com.contentstack.okio.Source; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Caches HTTP and HTTPS responses to the filesystem so they may be reused, + * saving time and bandwidth. + * + *

Cache Optimization

+ * To measure cache effectiveness, this class tracks three statistics: + *
    + *
  • {@linkplain #getRequestCount() Request Count:} the + * number of HTTP requests issued since this cache was created. + *
  • {@linkplain #getNetworkCount() Network Count:} the + * number of those requests that required network use. + *
  • {@linkplain #getHitCount() Hit Count:} the number of + * those requests whose responses were served by the cache. + *
+ * Sometimes a request will result in a conditional cache hit. If the cache + * contains a stale copy of the response, the client will issue a conditional + * {@code GET}. The server will then send either the updated response if it has + * changed, or a short 'not modified' response if the client's copy is still + * valid. Such responses increment both the network count and hit count. + * + *

The best way to improve the cache hit rate is by configuring the web + * server to return cacheable responses. Although this client honors all HTTP/1.1 (RFC 2068) cache + * headers, it doesn't cache partial responses. + * + *

Force a Network Response

+ * In some situations, such as after a user clicks a 'refresh' button, it may be + * necessary to skip the cache, and fetch data directly from the server. To force + * a full refresh, add the {@code no-cache} directive:
   {@code
+ *         connection.addRequestProperty("Cache-Control", "no-cache");
+ * }
+ * If it is only necessary to force a cached response to be validated by the + * server, use the more efficient {@code max-age=0} instead:
   {@code
+ *         connection.addRequestProperty("Cache-Control", "max-age=0");
+ * }
+ * + *

Force a Cache Response

+ * Sometimes you'll want to show resources if they are available immediately, + * but not otherwise. This can be used so your application can show + * something while waiting for the latest data to be downloaded. To + * restrict a request to locally-cached resources, add the {@code + * only-if-cached} directive:
   {@code
+ *     try {
+ *         connection.addRequestProperty("Cache-Control", "only-if-cached");
+ *         InputStream cached = connection.getInputStream();
+ *         // the resource was cached! show it
+ *     } catch (FileNotFoundException e) {
+ *         // the resource was not cached
+ *     }
+ * }
+ * This technique works even better in situations where a stale response is + * better than no response. To permit stale cached responses, use the {@code + * max-stale} directive with the maximum staleness in seconds:
   {@code
+ *         int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
+ *         connection.addRequestProperty("Cache-Control", "max-stale=" + maxStale);
+ * }
+ */ +public final class Cache { + private static final int VERSION = 201105; + private static final int ENTRY_METADATA = 0; + private static final int ENTRY_BODY = 1; + private static final int ENTRY_COUNT = 2; + + final InternalCache internalCache = new InternalCache() { + @Override public Response get(Request request) throws IOException { + return Cache.this.get(request); + } + @Override public CacheRequest put(Response response) throws IOException { + return Cache.this.put(response); + } + @Override public void remove(Request request) throws IOException { + Cache.this.remove(request); + } + @Override public void update(Response cached, Response network) throws IOException { + Cache.this.update(cached, network); + } + @Override public void trackConditionalCacheHit() { + Cache.this.trackConditionalCacheHit(); + } + @Override public void trackResponse(CacheStrategy cacheStrategy) { + Cache.this.trackResponse(cacheStrategy); + } + }; + + private final DiskLruCache cache; + + /* read and write statistics, all guarded by 'this' */ + private int writeSuccessCount; + private int writeAbortCount; + private int networkCount; + private int hitCount; + private int requestCount; + + public Cache(File directory, long maxSize) throws IOException { + cache = DiskLruCache.open(directory, VERSION, ENTRY_COUNT, maxSize); + } + + private static String urlToKey(Request request) { + return Util.hash(request.urlString()); + } + + Response get(Request request) { + String key = urlToKey(request); + DiskLruCache.Snapshot snapshot; + Entry entry; + try { + snapshot = cache.get(key); + if (snapshot == null) { + return null; + } + } catch (IOException e) { + // Give up because the cache cannot be read. + return null; + } + + try { + entry = new Entry(snapshot.getSource(ENTRY_METADATA)); + } catch (IOException e) { + Util.closeQuietly(snapshot); + return null; + } + + Response response = entry.response(request, snapshot); + + if (!entry.matches(request, response)) { + Util.closeQuietly(response.body()); + return null; + } + + return response; + } + + private CacheRequest put(Response response) throws IOException { + String requestMethod = response.request().method(); + + if (HttpMethod.invalidatesCache(response.request().method())) { + try { + remove(response.request()); + } catch (IOException ignored) { + // The cache cannot be written. + } + return null; + } + if (!requestMethod.equals("GET")) { + // Don't cache non-GET responses. We're technically allowed to cache + // HEAD requests and some POST requests, but the complexity of doing + // so is high and the benefit is low. + return null; + } + + if (OkHeaders.hasVaryAll(response)) { + return null; + } + + Entry entry = new Entry(response); + DiskLruCache.Editor editor = null; + try { + editor = cache.edit(urlToKey(response.request())); + if (editor == null) { + return null; + } + entry.writeTo(editor); + return new CacheRequestImpl(editor); + } catch (IOException e) { + abortQuietly(editor); + return null; + } + } + + private void remove(Request request) throws IOException { + cache.remove(urlToKey(request)); + } + + private void update(Response cached, Response network) { + Entry entry = new Entry(network); + DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot; + DiskLruCache.Editor editor = null; + try { + editor = snapshot.edit(); // Returns null if snapshot is not current. + if (editor != null) { + entry.writeTo(editor); + editor.commit(); + } + } catch (IOException e) { + abortQuietly(editor); + } + } + + private void abortQuietly(DiskLruCache.Editor editor) { + // Give up because the cache cannot be written. + try { + if (editor != null) { + editor.abort(); + } + } catch (IOException ignored) { + } + } + + /** + * Closes the cache and deletes all of its stored values. This will delete + * all files in the cache directory including files that weren't created by + * the cache. + */ + public void delete() throws IOException { + cache.delete(); + } + + public synchronized int getWriteAbortCount() { + return writeAbortCount; + } + + public synchronized int getWriteSuccessCount() { + return writeSuccessCount; + } + + public long getSize() { + return cache.size(); + } + + public long getMaxSize() { + return cache.getMaxSize(); + } + + public void flush() throws IOException { + cache.flush(); + } + + public void close() throws IOException { + cache.close(); + } + + public File getDirectory() { + return cache.getDirectory(); + } + + public boolean isClosed() { + return cache.isClosed(); + } + + private synchronized void trackResponse(CacheStrategy cacheStrategy) { + requestCount++; + + if (cacheStrategy.networkRequest != null) { + // If this is a conditional request, we'll increment hitCount if/when it hits. + networkCount++; + + } else if (cacheStrategy.cacheResponse != null) { + // This response uses the cache and not the network. That's a cache hit. + hitCount++; + } + } + + private synchronized void trackConditionalCacheHit() { + hitCount++; + } + + public synchronized int getNetworkCount() { + return networkCount; + } + + public synchronized int getHitCount() { + return hitCount; + } + + public synchronized int getRequestCount() { + return requestCount; + } + + private final class CacheRequestImpl implements CacheRequest { + private final DiskLruCache.Editor editor; + private Sink cacheOut; + private boolean done; + private Sink body; + + public CacheRequestImpl(final DiskLruCache.Editor editor) throws IOException { + this.editor = editor; + this.cacheOut = editor.newSink(ENTRY_BODY); + this.body = new ForwardingSink(cacheOut) { + @Override public void close() throws IOException { + synchronized (Cache.this) { + if (done) { + return; + } + done = true; + writeSuccessCount++; + } + super.close(); + editor.commit(); + } + }; + } + + @Override public void abort() { + synchronized (Cache.this) { + if (done) { + return; + } + done = true; + writeAbortCount++; + } + Util.closeQuietly(cacheOut); + try { + editor.abort(); + } catch (IOException ignored) { + } + } + + @Override public Sink body() { + return body; + } + } + + private static final class Entry { + private final String url; + private final Headers varyHeaders; + private final String requestMethod; + private final Protocol protocol; + private final int code; + private final String message; + private final Headers responseHeaders; + private final Handshake handshake; + + /** + * Reads an entry from an input stream. A typical entry looks like this: + *
{@code
+     *   http://google.com/foo
+     *   GET
+     *   2
+     *   Accept-Language: fr-CA
+     *   Accept-Charset: UTF-8
+     *   HTTP/1.1 200 OK
+     *   3
+     *   Content-Type: image/png
+     *   Content-Length: 100
+     *   Cache-Control: max-age=600
+     * }
+ * + *

A typical HTTPS file looks like this: + *

{@code
+     *   https://google.com/foo
+     *   GET
+     *   2
+     *   Accept-Language: fr-CA
+     *   Accept-Charset: UTF-8
+     *   HTTP/1.1 200 OK
+     *   3
+     *   Content-Type: image/png
+     *   Content-Length: 100
+     *   Cache-Control: max-age=600
+     *
+     *   AES_256_WITH_MD5
+     *   2
+     *   base64-encoded peerCertificate[0]
+     *   base64-encoded peerCertificate[1]
+     *   -1
+     * }
+ * The file is newline separated. The first two lines are the URL and + * the request method. Next is the number of HTTP Vary request header + * lines, followed by those lines. + * + *

Next is the response status line, followed by the number of HTTP + * response header lines, followed by those lines. + * + *

HTTPS responses also contain SSL session information. This begins + * with a blank line, and then a line containing the cipher suite. Next + * is the length of the peer certificate chain. These certificates are + * base64-encoded and appear each on their own line. The next line + * contains the length of the local certificate chain. These + * certificates are also base64-encoded and appear each on their own + * line. A length of -1 is used to encode a null array. + */ + public Entry(Source in) throws IOException { + try { + BufferedSource source = Okio.buffer(in); + url = source.readUtf8LineStrict(); + requestMethod = source.readUtf8LineStrict(); + Headers.Builder varyHeadersBuilder = new Headers.Builder(); + int varyRequestHeaderLineCount = readInt(source); + for (int i = 0; i < varyRequestHeaderLineCount; i++) { + varyHeadersBuilder.addLine(source.readUtf8LineStrict()); + } + varyHeaders = varyHeadersBuilder.build(); + + StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict()); + protocol = statusLine.protocol; + code = statusLine.code; + message = statusLine.message; + Headers.Builder responseHeadersBuilder = new Headers.Builder(); + int responseHeaderLineCount = readInt(source); + for (int i = 0; i < responseHeaderLineCount; i++) { + responseHeadersBuilder.addLine(source.readUtf8LineStrict()); + } + responseHeaders = responseHeadersBuilder.build(); + + if (isHttps()) { + String blank = source.readUtf8LineStrict(); + if (blank.length() > 0) { + throw new IOException("expected \"\" but was \"" + blank + "\""); + } + String cipherSuite = source.readUtf8LineStrict(); + List peerCertificates = readCertificateList(source); + List localCertificates = readCertificateList(source); + handshake = Handshake.get(cipherSuite, peerCertificates, localCertificates); + } else { + handshake = null; + } + } finally { + in.close(); + } + } + + public Entry(Response response) { + this.url = response.request().urlString(); + this.varyHeaders = OkHeaders.varyHeaders(response); + this.requestMethod = response.request().method(); + this.protocol = response.protocol(); + this.code = response.code(); + this.message = response.message(); + this.responseHeaders = response.headers(); + this.handshake = response.handshake(); + } + + public void writeTo(DiskLruCache.Editor editor) throws IOException { + BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA)); + + sink.writeUtf8(url); + sink.writeByte('\n'); + sink.writeUtf8(requestMethod); + sink.writeByte('\n'); + sink.writeUtf8(Integer.toString(varyHeaders.size())); + sink.writeByte('\n'); + for (int i = 0; i < varyHeaders.size(); i++) { + sink.writeUtf8(varyHeaders.name(i)); + sink.writeUtf8(": "); + sink.writeUtf8(varyHeaders.value(i)); + sink.writeByte('\n'); + } + + sink.writeUtf8(new StatusLine(protocol, code, message).toString()); + sink.writeByte('\n'); + sink.writeUtf8(Integer.toString(responseHeaders.size())); + sink.writeByte('\n'); + for (int i = 0; i < responseHeaders.size(); i++) { + sink.writeUtf8(responseHeaders.name(i)); + sink.writeUtf8(": "); + sink.writeUtf8(responseHeaders.value(i)); + sink.writeByte('\n'); + } + + if (isHttps()) { + sink.writeByte('\n'); + sink.writeUtf8(handshake.cipherSuite()); + sink.writeByte('\n'); + writeCertArray(sink, handshake.peerCertificates()); + writeCertArray(sink, handshake.localCertificates()); + } + sink.close(); + } + + private boolean isHttps() { + return url.startsWith("https://"); + } + + private List readCertificateList(BufferedSource source) throws IOException { + int length = readInt(source); + if (length == -1) return Collections.emptyList(); // OkHttp v1.2 used -1 to indicate null. + + try { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + List result = new ArrayList(length); + for (int i = 0; i < length; i++) { + String line = source.readUtf8LineStrict(); + byte[] bytes = ByteString.decodeBase64(line).toByteArray(); + result.add(certificateFactory.generateCertificate(new ByteArrayInputStream(bytes))); + } + return result; + } catch (CertificateException e) { + throw new IOException(e.getMessage()); + } + } + + private void writeCertArray(BufferedSink sink, List certificates) + throws IOException { + try { + sink.writeUtf8(Integer.toString(certificates.size())); + sink.writeByte('\n'); + for (int i = 0, size = certificates.size(); i < size; i++) { + byte[] bytes = certificates.get(i).getEncoded(); + String line = ByteString.of(bytes).base64(); + sink.writeUtf8(line); + sink.writeByte('\n'); + } + } catch (CertificateEncodingException e) { + throw new IOException(e.getMessage()); + } + } + + public boolean matches(Request request, Response response) { + return url.equals(request.urlString()) + && requestMethod.equals(request.method()) + && OkHeaders.varyMatches(response, varyHeaders, request); + } + + public Response response(Request request, DiskLruCache.Snapshot snapshot) { + String contentType = responseHeaders.get("Content-Type"); + String contentLength = responseHeaders.get("Content-Length"); + Request cacheRequest = new Request.Builder() + .url(url) + .method(requestMethod, null) + .headers(varyHeaders) + .build(); + return new Response.Builder() + .request(cacheRequest) + .protocol(protocol) + .code(code) + .message(message) + .headers(responseHeaders) + .body(new CacheResponseBody(snapshot, contentType, contentLength)) + .handshake(handshake) + .build(); + } + } + + private static int readInt(BufferedSource source) throws IOException { + String line = source.readUtf8LineStrict(); + try { + return Integer.parseInt(line); + } catch (NumberFormatException e) { + throw new IOException("Expected an integer but was \"" + line + "\""); + } + } + + private static class CacheResponseBody extends ResponseBody { + private final DiskLruCache.Snapshot snapshot; + private final BufferedSource bodySource; + private final String contentType; + private final String contentLength; + + public CacheResponseBody(final DiskLruCache.Snapshot snapshot, + String contentType, String contentLength) { + this.snapshot = snapshot; + this.contentType = contentType; + this.contentLength = contentLength; + + Source source = snapshot.getSource(ENTRY_BODY); + bodySource = Okio.buffer(new ForwardingSource(source) { + @Override public void close() throws IOException { + snapshot.close(); + super.close(); + } + }); + } + + @Override public MediaType contentType() { + return contentType != null ? MediaType.parse(contentType) : null; + } + + @Override public long contentLength() { + try { + return contentLength != null ? Long.parseLong(contentLength) : -1; + } catch (NumberFormatException e) { + return -1; + } + } + + @Override public BufferedSource source() { + return bodySource; + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/CacheControl.java b/contentstack/src/main/java/com/contentstack/okhttp/CacheControl.java new file mode 100755 index 00000000..fcbe6db3 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/CacheControl.java @@ -0,0 +1,176 @@ +package com.contentstack.okhttp; + +import com.contentstack.okhttp.internal.http.HeaderParser; + +/** + * A Cache-Control header with cache directives from a server or client. These + * directives set policy on what responses can be stored, and which requests can + * be satisfied by those stored responses. + * + *

See RFC + * 2616, 14.9. + */ +public final class CacheControl { + private final boolean noCache; + private final boolean noStore; + private final int maxAgeSeconds; + private final int sMaxAgeSeconds; + private final boolean isPublic; + private final boolean mustRevalidate; + private final int maxStaleSeconds; + private final int minFreshSeconds; + private final boolean onlyIfCached; + + private CacheControl(boolean noCache, boolean noStore, int maxAgeSeconds, int sMaxAgeSeconds, + boolean isPublic, boolean mustRevalidate, int maxStaleSeconds, int minFreshSeconds, + boolean onlyIfCached) { + this.noCache = noCache; + this.noStore = noStore; + this.maxAgeSeconds = maxAgeSeconds; + this.sMaxAgeSeconds = sMaxAgeSeconds; + this.isPublic = isPublic; + this.mustRevalidate = mustRevalidate; + this.maxStaleSeconds = maxStaleSeconds; + this.minFreshSeconds = minFreshSeconds; + this.onlyIfCached = onlyIfCached; + } + + /** + * In a response, this field's name "no-cache" is misleading. It doesn't + * prevent us from caching the response; it only means we have to validate the + * response with the origin server before returning it. We can do this with a + * conditional GET. + * + *

In a request, it means do not use a cache to satisfy the request. + */ + public boolean noCache() { + return noCache; + } + + /** If true, this response should not be cached. */ + public boolean noStore() { + return noStore; + } + + /** + * The duration past the response's served date that it can be served without + * validation. + */ + public int maxAgeSeconds() { + return maxAgeSeconds; + } + + /** + * The "s-maxage" directive is the max age for shared caches. Not to be + * confused with "max-age" for non-shared caches, As in Firefox and Chrome, + * this directive is not honored by this cache. + */ + public int sMaxAgeSeconds() { + return sMaxAgeSeconds; + } + + public boolean isPublic() { + return isPublic; + } + + public boolean mustRevalidate() { + return mustRevalidate; + } + + public int maxStaleSeconds() { + return maxStaleSeconds; + } + + public int minFreshSeconds() { + return minFreshSeconds; + } + + /** + * This field's name "only-if-cached" is misleading. It actually means "do + * not use the network". It is set by a client who only wants to make a + * request if it can be fully satisfied by the cache. Cached responses that + * would require validation (ie. conditional gets) are not permitted if this + * header is set. + */ + public boolean onlyIfCached() { + return onlyIfCached; + } + + /** + * Returns the cache directives of {@code headers}. This honors both + * Cache-Control and Pragma headers if they are present. + */ + public static CacheControl parse(Headers headers) { + boolean noCache = false; + boolean noStore = false; + int maxAgeSeconds = -1; + int sMaxAgeSeconds = -1; + boolean isPublic = false; + boolean mustRevalidate = false; + int maxStaleSeconds = -1; + int minFreshSeconds = -1; + boolean onlyIfCached = false; + + for (int i = 0; i < headers.size(); i++) { + if (!headers.name(i).equalsIgnoreCase("Cache-Control") + && !headers.name(i).equalsIgnoreCase("Pragma")) { + continue; + } + + String string = headers.value(i); + int pos = 0; + while (pos < string.length()) { + int tokenStart = pos; + pos = HeaderParser.skipUntil(string, pos, "=,;"); + String directive = string.substring(tokenStart, pos).trim(); + String parameter; + + if (pos == string.length() || string.charAt(pos) == ',' || string.charAt(pos) == ';') { + pos++; // consume ',' or ';' (if necessary) + parameter = null; + } else { + pos++; // consume '=' + pos = HeaderParser.skipWhitespace(string, pos); + + // quoted string + if (pos < string.length() && string.charAt(pos) == '\"') { + pos++; // consume '"' open quote + int parameterStart = pos; + pos = HeaderParser.skipUntil(string, pos, "\""); + parameter = string.substring(parameterStart, pos); + pos++; // consume '"' close quote (if necessary) + + // unquoted string + } else { + int parameterStart = pos; + pos = HeaderParser.skipUntil(string, pos, ",;"); + parameter = string.substring(parameterStart, pos).trim(); + } + } + + if ("no-cache".equalsIgnoreCase(directive)) { + noCache = true; + } else if ("no-store".equalsIgnoreCase(directive)) { + noStore = true; + } else if ("max-age".equalsIgnoreCase(directive)) { + maxAgeSeconds = HeaderParser.parseSeconds(parameter); + } else if ("s-maxage".equalsIgnoreCase(directive)) { + sMaxAgeSeconds = HeaderParser.parseSeconds(parameter); + } else if ("public".equalsIgnoreCase(directive)) { + isPublic = true; + } else if ("must-revalidate".equalsIgnoreCase(directive)) { + mustRevalidate = true; + } else if ("max-stale".equalsIgnoreCase(directive)) { + maxStaleSeconds = HeaderParser.parseSeconds(parameter); + } else if ("min-fresh".equalsIgnoreCase(directive)) { + minFreshSeconds = HeaderParser.parseSeconds(parameter); + } else if ("only-if-cached".equalsIgnoreCase(directive)) { + onlyIfCached = true; + } + } + } + + return new CacheControl(noCache, noStore, maxAgeSeconds, sMaxAgeSeconds, isPublic, + mustRevalidate, maxStaleSeconds, minFreshSeconds, onlyIfCached); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/Call.java b/contentstack/src/main/java/com/contentstack/okhttp/Call.java new file mode 100755 index 00000000..16e98ae6 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/Call.java @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp; + +import com.contentstack.okhttp.internal.NamedRunnable; +import com.contentstack.okhttp.internal.Util; +import com.contentstack.okhttp.internal.http.HttpEngine; +import com.contentstack.okhttp.internal.http.HttpMethod; +import com.contentstack.okhttp.internal.http.OkHeaders; +import com.contentstack.okhttp.internal.http.RetryableSink; +import com.contentstack.okio.BufferedSink; +import com.contentstack.okio.BufferedSource; + +import java.io.IOException; +import java.net.ProtocolException; + +/** + * A call is a request that has been prepared for execution. A call can be + * canceled. As this object represents a single request/response pair (stream), + * it cannot be executed twice. + */ +public class Call { + private final OkHttpClient client; + private int redirectionCount; + + // Guarded by this. + private boolean executed; + volatile boolean canceled; + + /** The request; possibly a consequence of redirects or auth headers. */ + private Request request; + HttpEngine engine; + + protected Call(OkHttpClient client, Request request) { + // Copy the client. Otherwise changes (socket factory, redirect policy, + // etc.) may incorrectly be reflected in the request when it is executed. + this.client = client.copyWithDefaults(); + this.request = request; + } + + /** + * Invokes the request immediately, and blocks until the response can be + * processed or is in error. + * + *

The caller may read the response body with the response's + * {@link Response#body} method. To facilitate connection recycling, callers + * should always {@link ResponseBody#close() close the response body}. + * + *

Note that transport-layer success (receiving a HTTP response code, + * headers and body) does not necessarily indicate application-layer success: + * {@code response} may still indicate an unhappy HTTP response code like 404 + * or 500. + * + * @throws IOException if the request could not be executed due to + * cancellation, a connectivity problem or timeout. Because networks can + * fail during an exchange, it is possible that the remote server + * accepted the request before the failure. + * + * @throws IllegalStateException when the call has already been executed. + */ + public Response execute() throws IOException { + synchronized (this) { + if (executed) throw new IllegalStateException("Already Executed"); + executed = true; + } + Response result = getResponse(); + engine.releaseConnection(); // Transfer ownership of the body to the caller. + if (result == null) throw new IOException("Canceled"); + return result; + } + + /** + * Schedules the request to be executed at some point in the future. + * + *

The {@link OkHttpClient#getDispatcher dispatcher} defines when the + * request will run: usually immediately unless there are several other + * requests currently being executed. + * + *

This client will later call back {@code responseCallback} with either + * an HTTP response or a failure exception. If you {@link #cancel} a request + * before it completes the callback will not be invoked. + * + * @throws IllegalStateException when the call has already been executed. + */ + public void enqueue(Callback responseCallback) { + synchronized (this) { + if (executed) throw new IllegalStateException("Already Executed"); + executed = true; + } + client.getDispatcher().enqueue(new AsyncCall(responseCallback)); + } + + /** + * Cancels the request, if possible. Requests that are already complete + * cannot be canceled. + */ + public void cancel() { + canceled = true; + if (engine != null) engine.disconnect(); + } + + final class AsyncCall extends NamedRunnable { + private final Callback responseCallback; + + private AsyncCall(Callback responseCallback) { + super("OkHttp %s", request.urlString()); + this.responseCallback = responseCallback; + } + + String host() { + return request.url().getHost(); + } + + Request request() { + return request; + } + + Object tag() { + return request.tag(); + } + + Call get() { + return Call.this; + } + + @Override protected void execute() { + boolean signalledCallback = false; + try { + Response response = getResponse(); + if (canceled) { + signalledCallback = true; + responseCallback.onFailure(request, new IOException("Canceled")); + } else { + signalledCallback = true; + engine.releaseConnection(); + responseCallback.onResponse(response); + } + } catch (IOException e) { + if (signalledCallback) throw new RuntimeException(e); // Do not signal the callback twice! + responseCallback.onFailure(request, e); + } finally { + client.getDispatcher().finished(this); + } + } + } + + /** + * Performs the request and returns the response. May return null if this + * call was canceled. + */ + private Response getResponse() throws IOException { + // Copy body metadata to the appropriate request headers. + RequestBody body = request.body(); + RetryableSink requestBodyOut = null; + if (body != null) { + Request.Builder requestBuilder = request.newBuilder(); + + MediaType contentType = body.contentType(); + if (contentType != null) { + requestBuilder.header("Content-Type", contentType.toString()); + } + + long contentLength = body.contentLength(); + if (contentLength != -1) { + requestBuilder.header("Content-Length", Long.toString(contentLength)); + requestBuilder.removeHeader("Transfer-Encoding"); + } else { + requestBuilder.header("Transfer-Encoding", "chunked"); + requestBuilder.removeHeader("Content-Length"); + } + + request = requestBuilder.build(); + } else if (HttpMethod.hasRequestBody(request.method())) { + requestBodyOut = Util.emptySink(); + } + + // Create the initial HTTP engine. Retries and redirects need new engine for each attempt. + engine = new HttpEngine(client, request, false, null, null, requestBodyOut, null); + + while (true) { + if (canceled) return null; + + try { + engine.sendRequest(); + + if (request.body() != null) { + BufferedSink sink = engine.getBufferedRequestBody(); + request.body().writeTo(sink); + } + + engine.readResponse(); + } catch (IOException e) { + HttpEngine retryEngine = engine.recover(e, null); + if (retryEngine != null) { + engine = retryEngine; + continue; + } + + // Give up; recovery is not possible. + throw e; + } + + Response response = engine.getResponse(); + Request followUp = engine.followUpRequest(); + + if (followUp == null) { + engine.releaseConnection(); + return response.newBuilder() + .body(new RealResponseBody(response, engine.getResponseBody())) + .build(); + } + + if (engine.getResponse().isRedirect() && ++redirectionCount > HttpEngine.MAX_REDIRECTS) { + throw new ProtocolException("Too many redirects: " + redirectionCount); + } + + if (!engine.sameConnection(followUp.url())) { + engine.releaseConnection(); + } + + Connection connection = engine.close(); + request = followUp; + engine = new HttpEngine(client, request, false, connection, null, null, response); + } + } + + private static class RealResponseBody extends ResponseBody { + private final Response response; + private final BufferedSource source; + + RealResponseBody(Response response, BufferedSource source) { + this.response = response; + this.source = source; + } + + @Override public MediaType contentType() { + String contentType = response.header("Content-Type"); + return contentType != null ? MediaType.parse(contentType) : null; + } + + @Override public long contentLength() { + return OkHeaders.contentLength(response); + } + + @Override public BufferedSource source() { + return source; + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/Callback.java b/contentstack/src/main/java/com/contentstack/okhttp/Callback.java new file mode 100755 index 00000000..428040c6 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/Callback.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp; + +import java.io.IOException; + +public interface Callback { + /** + * Called when the request could not be executed due to cancellation, a + * connectivity problem or timeout. Because networks can fail during an + * exchange, it is possible that the remote server accepted the request + * before the failure. + */ + void onFailure(Request request, IOException e); + + /** + * Called when the HTTP response was successfully returned by the remote + * server. The callback may proceed to read the response body with {@link + * Response#body}. The response is still live until its response body is + * closed with {@code response.body().close()}. The recipient of the callback + * may even consume the response body on another thread. + * + *

Note that transport-layer success (receiving a HTTP response code, + * headers and body) does not necessarily indicate application-layer + * success: {@code response} may still indicate an unhappy HTTP response + * code like 404 or 500. + */ + void onResponse(Response response) throws IOException; +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/Challenge.java b/contentstack/src/main/java/com/contentstack/okhttp/Challenge.java new file mode 100755 index 00000000..f76b5f47 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/Challenge.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp; + +import com.contentstack.okhttp.internal.Util; + +/** An RFC 2617 challenge. */ +public final class Challenge { + private final String scheme; + private final String realm; + + public Challenge(String scheme, String realm) { + this.scheme = scheme; + this.realm = realm; + } + + /** Returns the authentication scheme, like {@code Basic}. */ + public String getScheme() { + return scheme; + } + + /** Returns the protection space. */ + public String getRealm() { + return realm; + } + + @Override public boolean equals(Object o) { + return o instanceof Challenge + && Util.equal(scheme, ((Challenge) o).scheme) + && Util.equal(realm, ((Challenge) o).realm); + } + + @Override public int hashCode() { + int result = 29; + result = 31 * result + (realm != null ? realm.hashCode() : 0); + result = 31 * result + (scheme != null ? scheme.hashCode() : 0); + return result; + } + + @Override public String toString() { + return scheme + " realm=\"" + realm + "\""; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/Connection.java b/contentstack/src/main/java/com/contentstack/okhttp/Connection.java new file mode 100755 index 00000000..26115125 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/Connection.java @@ -0,0 +1,419 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp; + +import com.contentstack.okhttp.internal.Platform; +import com.contentstack.okhttp.internal.Util; +import com.contentstack.okhttp.internal.http.HttpConnection; +import com.contentstack.okhttp.internal.http.HttpEngine; +import com.contentstack.okhttp.internal.http.HttpTransport; +import com.contentstack.okhttp.internal.http.OkHeaders; +import com.contentstack.okhttp.internal.http.SpdyTransport; +import com.contentstack.okhttp.internal.http.Transport; +import com.contentstack.okhttp.internal.spdy.SpdyConnection; + +import java.io.IOException; +import java.net.Proxy; +import java.net.Socket; +import java.net.URL; + +import javax.net.ssl.SSLSocket; + +import static com.contentstack.okhttp.internal.Util.getEffectivePort; +import static java.net.HttpURLConnection.HTTP_OK; +import static java.net.HttpURLConnection.HTTP_PROXY_AUTH; + +/** + * The sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection. May be + * used for multiple HTTP request/response exchanges. Connections may be direct + * to the origin server or via a proxy. + * + *

Typically instances of this class are created, connected and exercised + * automatically by the HTTP client. Applications may use this class to monitor + * HTTP connections as members of a {@linkplain ConnectionPool connection pool}. + * + *

Do not confuse this class with the misnamed {@code HttpURLConnection}, + * which isn't so much a connection as a single request/response exchange. + * + *

Modern TLS

+ * There are tradeoffs when selecting which options to include when negotiating + * a secure connection to a remote host. Newer TLS options are quite useful: + *
    + *
  • Server Name Indication (SNI) enables one IP address to negotiate secure + * connections for multiple domain names. + *
  • Application Layer Protocol Negotiation (ALPN) enables the HTTPS port + * (443) to be used for different HTTP and SPDY protocols. + *
+ * Unfortunately, older HTTPS servers refuse to connect when such options are + * presented. Rather than avoiding these options entirely, this class allows a + * connection to be attempted with modern options and then retried without them + * should the attempt fail. + */ +public final class Connection { + private final ConnectionPool pool; + private final Route route; + + private Socket socket; + private boolean connected = false; + private HttpConnection httpConnection; + private SpdyConnection spdyConnection; + private Protocol protocol = Protocol.HTTP_1_1; + private long idleStartTimeNs; + private Handshake handshake; + private int recycleCount; + + /** + * The object that owns this connection. Null if it is shared (for SPDY), + * belongs to a pool, or has been discarded. Guarded by {@code pool}, which + * clears the owner when an incoming connection is recycled. + */ + private Object owner; + + public Connection(ConnectionPool pool, Route route) { + this.pool = pool; + this.route = route; + } + + Object getOwner() { + synchronized (pool) { + return owner; + } + } + + void setOwner(Object owner) { + if (isSpdy()) return; // SPDY connections are shared. + synchronized (pool) { + if (this.owner != null) throw new IllegalStateException("Connection already has an owner!"); + this.owner = owner; + } + } + + /** + * Attempts to clears the owner of this connection. Returns true if the owner + * was cleared and the connection can be pooled or reused. This will return + * false if the connection cannot be pooled or reused, such as if it was + * closed with {@link #closeIfOwnedBy}. + */ + boolean clearOwner() { + synchronized (pool) { + if (owner == null) { + // No owner? Don't reuse this connection. + return false; + } + + owner = null; + return true; + } + } + + /** + * Closes this connection if it is currently owned by {@code owner}. This also + * strips the ownership of the connection so it cannot be pooled or reused. + */ + void closeIfOwnedBy(Object owner) throws IOException { + if (isSpdy()) throw new IllegalStateException(); + synchronized (pool) { + if (this.owner != owner) { + return; // Wrong owner. Perhaps a late disconnect? + } + + this.owner = null; // Drop the owner so the connection won't be reused. + } + + // Don't close() inside the synchronized block. + socket.close(); + } + + void connect(int connectTimeout, int readTimeout, int writeTimeout, Request tunnelRequest) + throws IOException { + if (connected) throw new IllegalStateException("already connected"); + + if (route.proxy.type() == Proxy.Type.DIRECT || route.proxy.type() == Proxy.Type.HTTP) { + socket = route.address.socketFactory.createSocket(); + } else { + socket = new Socket(route.proxy); + } + + socket.setSoTimeout(readTimeout); + Platform.get().connectSocket(socket, route.inetSocketAddress, connectTimeout); + + if (route.address.sslSocketFactory != null) { + upgradeToTls(tunnelRequest, readTimeout, writeTimeout); + } else { + httpConnection = new HttpConnection(pool, this, socket); + } + connected = true; + } + + /** + * Connects this connection if it isn't already. This creates tunnels, shares + * the connection with the connection pool, and configures timeouts. + */ + void connectAndSetOwner(OkHttpClient client, Object owner, Request request) throws IOException { + setOwner(owner); + + if (!isConnected()) { + Request tunnelRequest = tunnelRequest(request); + connect(client.getConnectTimeout(), client.getReadTimeout(), + client.getWriteTimeout(), tunnelRequest); + if (isSpdy()) { + client.getConnectionPool().share(this); + } + client.routeDatabase().connected(getRoute()); + } + + setTimeouts(client.getReadTimeout(), client.getWriteTimeout()); + } + + /** + * Returns a request that creates a TLS tunnel via an HTTP proxy, or null if + * no tunnel is necessary. Everything in the tunnel request is sent + * unencrypted to the proxy server, so tunnels include only the minimum set of + * headers. This avoids sending potentially sensitive data like HTTP cookies + * to the proxy unencrypted. + */ + private Request tunnelRequest(Request request) throws IOException { + if (!route.requiresTunnel()) return null; + + String host = request.url().getHost(); + int port = getEffectivePort(request.url()); + String authority = (port == Util.getDefaultPort("https")) ? host : (host + ":" + port); + Request.Builder result = new Request.Builder() + .url(new URL("https", host, port, "/")) + .header("Host", authority) + .header("Proxy-Connection", "Keep-Alive"); // For HTTP/1.0 proxies like Squid. + + // Copy over the User-Agent header if it exists. + String userAgent = request.header("User-Agent"); + if (userAgent != null) { + result.header("User-Agent", userAgent); + } + + // Copy over the Proxy-Authorization header if it exists. + String proxyAuthorization = request.header("Proxy-Authorization"); + if (proxyAuthorization != null) { + result.header("Proxy-Authorization", proxyAuthorization); + } + + return result.build(); + } + + /** + * Create an {@code SSLSocket} and perform the TLS handshake and certificate + * validation. + */ + private void upgradeToTls(Request tunnelRequest, int readTimeout, int writeTimeout) + throws IOException { + Platform platform = Platform.get(); + + // Make an SSL Tunnel on the first message pair of each SSL + proxy connection. + if (tunnelRequest != null) { + makeTunnel(tunnelRequest, readTimeout, writeTimeout); + } + + // Create the wrapper over connected socket. + socket = route.address.sslSocketFactory + .createSocket(socket, route.address.uriHost, route.address.uriPort, true /* autoClose */); + SSLSocket sslSocket = (SSLSocket) socket; + platform.configureTls(sslSocket, route.address.uriHost, route.tlsVersion); + + boolean useNpn = route.supportsNpn(); + if (useNpn) { + platform.setProtocols(sslSocket, route.address.protocols); + } + + // Force handshake. This can throw! + sslSocket.startHandshake(); + + // Verify that the socket's certificates are acceptable for the target host. + if (!route.address.hostnameVerifier.verify(route.address.uriHost, sslSocket.getSession())) { + throw new IOException("Hostname '" + route.address.uriHost + "' was not verified"); + } + + handshake = Handshake.get(sslSocket.getSession()); + + String maybeProtocol; + if (useNpn && (maybeProtocol = platform.getSelectedProtocol(sslSocket)) != null) { + protocol = Protocol.get(maybeProtocol); // Throws IOE on unknown. + } + + if (protocol == Protocol.SPDY_3 || protocol == Protocol.HTTP_2) { + sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream. + spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, socket) + .protocol(protocol).build(); + spdyConnection.sendConnectionPreface(); + } else { + httpConnection = new HttpConnection(pool, this, socket); + } + } + + /** Returns true if {@link #connect} has been attempted on this connection. */ + boolean isConnected() { + return connected; + } + + /** Returns the route used by this connection. */ + public Route getRoute() { + return route; + } + + /** + * Returns the socket that this connection uses, or null if the connection + * is not currently connected. + */ + public Socket getSocket() { + return socket; + } + + /** Returns true if this connection is alive. */ + boolean isAlive() { + return !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown(); + } + + /** + * Returns true if we are confident that we can read data from this + * connection. This is more expensive and more accurate than {@link + * #isAlive()}; callers should check {@link #isAlive()} first. + */ + boolean isReadable() { + if (httpConnection != null) return httpConnection.isReadable(); + return true; // SPDY connections, and connections before connect() are both optimistic. + } + + void resetIdleStartTime() { + if (spdyConnection != null) throw new IllegalStateException("spdyConnection != null"); + this.idleStartTimeNs = System.nanoTime(); + } + + /** Returns true if this connection is idle. */ + boolean isIdle() { + return spdyConnection == null || spdyConnection.isIdle(); + } + + /** + * Returns true if this connection has been idle for longer than + * {@code keepAliveDurationNs}. + */ + boolean isExpired(long keepAliveDurationNs) { + return getIdleStartTimeNs() < System.nanoTime() - keepAliveDurationNs; + } + + /** + * Returns the time in ns when this connection became idle. Undefined if + * this connection is not idle. + */ + long getIdleStartTimeNs() { + return spdyConnection == null ? idleStartTimeNs : spdyConnection.getIdleStartTimeNs(); + } + + public Handshake getHandshake() { + return handshake; + } + + /** Returns the transport appropriate for this connection. */ + Transport newTransport(HttpEngine httpEngine) throws IOException { + return (spdyConnection != null) + ? new SpdyTransport(httpEngine, spdyConnection) + : new HttpTransport(httpEngine, httpConnection); + } + + /** + * Returns true if this is a SPDY connection. Such connections can be used + * in multiple HTTP requests simultaneously. + */ + boolean isSpdy() { + return spdyConnection != null; + } + + /** + * Returns the protocol negotiated by this connection, or {@link + * Protocol#HTTP_1_1} if no protocol has been negotiated. + */ + public Protocol getProtocol() { + return protocol; + } + + /** + * Sets the protocol negotiated by this connection. Typically this is used + * when an HTTP/1.1 request is sent and an HTTP/1.0 response is received. + */ + void setProtocol(Protocol protocol) { + if (protocol == null) throw new IllegalArgumentException("protocol == null"); + this.protocol = protocol; + } + + void setTimeouts(int readTimeoutMillis, int writeTimeoutMillis) throws IOException { + if (!connected) throw new IllegalStateException("setTimeouts - not connected"); + + // Don't set timeouts on shared SPDY connections. + if (httpConnection != null) { + socket.setSoTimeout(readTimeoutMillis); + httpConnection.setTimeouts(readTimeoutMillis, writeTimeoutMillis); + } + } + + void incrementRecycleCount() { + recycleCount++; + } + + /** + * Returns the number of times this connection has been returned to the + * connection pool. + */ + int recycleCount() { + return recycleCount; + } + + /** + * To make an HTTPS connection over an HTTP proxy, send an unencrypted + * CONNECT request to create the proxy connection. This may need to be + * retried if the proxy requires authorization. + */ + private void makeTunnel(Request request, int readTimeout, int writeTimeout) + throws IOException { + HttpConnection tunnelConnection = new HttpConnection(pool, this, socket); + tunnelConnection.setTimeouts(readTimeout, writeTimeout); + URL url = request.url(); + String requestLine = "CONNECT " + url.getHost() + ":" + url.getPort() + " HTTP/1.1"; + while (true) { + tunnelConnection.writeRequest(request.headers(), requestLine); + tunnelConnection.flush(); + Response response = tunnelConnection.readResponse().request(request).build(); + tunnelConnection.emptyResponseBody(); + + switch (response.code()) { + case HTTP_OK: + // Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If that + // happens, then we will have buffered bytes that are needed by the SSLSocket! + if (tunnelConnection.bufferSize() > 0) { + throw new IOException("TLS tunnel buffered too many bytes!"); + } + return; + + case HTTP_PROXY_AUTH: + request = OkHeaders.processAuthHeader( + route.address.authenticator, response, route.proxy); + if (request != null) continue; + throw new IOException("Failed to authenticate with proxy"); + + default: + throw new IOException( + "Unexpected response code for CONNECT: " + response.code()); + } + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/ConnectionPool.java b/contentstack/src/main/java/com/contentstack/okhttp/ConnectionPool.java new file mode 100755 index 00000000..23824ebe --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/ConnectionPool.java @@ -0,0 +1,275 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp; + +import com.contentstack.okhttp.internal.Platform; +import com.contentstack.okhttp.internal.Util; + +import java.net.SocketException; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Manages reuse of HTTP and SPDY connections for reduced network latency. HTTP + * requests that share the same {@link Address} may share a + * {@link Connection}. This class implements the policy of + * which connections to keep open for future use. + * + *

The {@link #getDefault() system-wide default} uses system properties for + * tuning parameters: + *

    + *
  • {@code http.keepAlive} true if HTTP and SPDY connections should be + * pooled at all. Default is true. + *
  • {@code http.maxConnections} maximum number of idle connections to + * each to keep in the pool. Default is 5. + *
  • {@code http.keepAliveDuration} Time in milliseconds to keep the + * connection alive in the pool before closing it. Default is 5 minutes. + * This property isn't used by {@code HttpURLConnection}. + *
+ * + *

The default instance doesn't adjust its configuration as system + * properties are changed. This assumes that the applications that set these + * parameters do so before making HTTP connections, and that this class is + * initialized lazily. + */ +public final class ConnectionPool { + private static final int MAX_CONNECTIONS_TO_CLEANUP = 2; + private static final long DEFAULT_KEEP_ALIVE_DURATION_MS = 5 * 60 * 1000; // 5 min + + private static final ConnectionPool systemDefault; + + static { + String keepAlive = System.getProperty("http.keepAlive"); + String keepAliveDuration = System.getProperty("http.keepAliveDuration"); + String maxIdleConnections = System.getProperty("http.maxConnections"); + long keepAliveDurationMs = keepAliveDuration != null ? Long.parseLong(keepAliveDuration) + : DEFAULT_KEEP_ALIVE_DURATION_MS; + if (keepAlive != null && !Boolean.parseBoolean(keepAlive)) { + systemDefault = new ConnectionPool(0, keepAliveDurationMs); + } else if (maxIdleConnections != null) { + systemDefault = new ConnectionPool(Integer.parseInt(maxIdleConnections), keepAliveDurationMs); + } else { + systemDefault = new ConnectionPool(5, keepAliveDurationMs); + } + } + + /** The maximum number of idle connections for each address. */ + private final int maxIdleConnections; + private final long keepAliveDurationNs; + + private final LinkedList connections = new LinkedList(); + + /** We use a single background thread to cleanup expired connections. */ + private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, + 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(), + Util.threadFactory("OkHttp ConnectionPool", true)); + private final Runnable connectionsCleanupRunnable = new Runnable() { + @Override public void run() { + List expiredConnections = new ArrayList(MAX_CONNECTIONS_TO_CLEANUP); + int idleConnectionCount = 0; + synchronized (ConnectionPool.this) { + for (ListIterator i = connections.listIterator(connections.size()); + i.hasPrevious(); ) { + Connection connection = i.previous(); + if (!connection.isAlive() || connection.isExpired(keepAliveDurationNs)) { + i.remove(); + expiredConnections.add(connection); + if (expiredConnections.size() == MAX_CONNECTIONS_TO_CLEANUP) break; + } else if (connection.isIdle()) { + idleConnectionCount++; + } + } + + for (ListIterator i = connections.listIterator(connections.size()); + i.hasPrevious() && idleConnectionCount > maxIdleConnections; ) { + Connection connection = i.previous(); + if (connection.isIdle()) { + expiredConnections.add(connection); + i.remove(); + --idleConnectionCount; + } + } + } + for (Connection expiredConnection : expiredConnections) { + Util.closeQuietly(expiredConnection.getSocket()); + } + } + }; + + public ConnectionPool(int maxIdleConnections, long keepAliveDurationMs) { + this.maxIdleConnections = maxIdleConnections; + this.keepAliveDurationNs = keepAliveDurationMs * 1000 * 1000; + } + + /** + * Returns a snapshot of the connections in this pool, ordered from newest to + * oldest. Waits for the cleanup callable to run if it is currently scheduled. + */ + List getConnections() { + waitForCleanupCallableToRun(); + synchronized (this) { + return new ArrayList(connections); + } + } + + /** + * Blocks until the executor service has processed all currently enqueued + * jobs. + */ + private void waitForCleanupCallableToRun() { + try { + executorService.submit(new Runnable() { + @Override public void run() { + } + }).get(); + } catch (Exception e) { + throw new AssertionError(); + } + } + + public static ConnectionPool getDefault() { + return systemDefault; + } + + /** Returns total number of connections in the pool. */ + public synchronized int getConnectionCount() { + return connections.size(); + } + + /** Returns total number of spdy connections in the pool. */ + public synchronized int getSpdyConnectionCount() { + int total = 0; + for (Connection connection : connections) { + if (connection.isSpdy()) total++; + } + return total; + } + + /** Returns total number of http connections in the pool. */ + public synchronized int getHttpConnectionCount() { + int total = 0; + for (Connection connection : connections) { + if (!connection.isSpdy()) total++; + } + return total; + } + + /** Returns a recycled connection to {@code address}, or null if no such connection exists. */ + public synchronized Connection get(Address address) { + Connection foundConnection = null; + for (ListIterator i = connections.listIterator(connections.size()); + i.hasPrevious(); ) { + Connection connection = i.previous(); + if (!connection.getRoute().getAddress().equals(address) + || !connection.isAlive() + || System.nanoTime() - connection.getIdleStartTimeNs() >= keepAliveDurationNs) { + continue; + } + i.remove(); + if (!connection.isSpdy()) { + try { + Platform.get().tagSocket(connection.getSocket()); + } catch (SocketException e) { + Util.closeQuietly(connection.getSocket()); + // When unable to tag, skip recycling and close + Platform.get().logW("Unable to tagSocket(): " + e); + continue; + } + } + foundConnection = connection; + break; + } + + if (foundConnection != null && foundConnection.isSpdy()) { + connections.addFirst(foundConnection); // Add it back after iteration. + } + + executorService.execute(connectionsCleanupRunnable); + return foundConnection; + } + + /** + * Gives {@code connection} to the pool. The pool may store the connection, + * or close it, as its policy describes. + * + *

It is an error to use {@code connection} after calling this method. + */ + void recycle(Connection connection) { + if (connection.isSpdy()) { + return; + } + + if (!connection.clearOwner()) { + return; // This connection isn't eligible for reuse. + } + + if (!connection.isAlive()) { + Util.closeQuietly(connection.getSocket()); + return; + } + + try { + Platform.get().untagSocket(connection.getSocket()); + } catch (SocketException e) { + // When unable to remove tagging, skip recycling and close. + Platform.get().logW("Unable to untagSocket(): " + e); + Util.closeQuietly(connection.getSocket()); + return; + } + + synchronized (this) { + connections.addFirst(connection); + connection.incrementRecycleCount(); + connection.resetIdleStartTime(); + } + + executorService.execute(connectionsCleanupRunnable); + } + + /** + * Shares the SPDY connection with the pool. Callers to this method may + * continue to use {@code connection}. + */ + void share(Connection connection) { + if (!connection.isSpdy()) throw new IllegalArgumentException(); + executorService.execute(connectionsCleanupRunnable); + if (connection.isAlive()) { + synchronized (this) { + connections.addFirst(connection); + } + } + } + + /** Close and remove all connections in the pool. */ + public void evictAll() { + List connections; + synchronized (this) { + connections = new ArrayList(this.connections); + this.connections.clear(); + } + + for (int i = 0, size = connections.size(); i < size; i++) { + Util.closeQuietly(connections.get(i).getSocket()); + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/Credentials.java b/contentstack/src/main/java/com/contentstack/okhttp/Credentials.java new file mode 100755 index 00000000..3ef9fa87 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/Credentials.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp; + +import com.contentstack.okio.ByteString; + +import java.io.UnsupportedEncodingException; + +/** Factory for HTTP authorization credentials. */ +public final class Credentials { + private Credentials() { + } + + /** Returns an auth credential for the Basic scheme. */ + public static String basic(String userName, String password) { + try { + String usernameAndPassword = userName + ":" + password; + byte[] bytes = usernameAndPassword.getBytes("ISO-8859-1"); + String encoded = ByteString.of(bytes).base64(); + return "Basic " + encoded; + } catch (UnsupportedEncodingException e) { + throw new AssertionError(); + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/Dispatcher.java b/contentstack/src/main/java/com/contentstack/okhttp/Dispatcher.java new file mode 100755 index 00000000..7a4986cb --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/Dispatcher.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp; + +import com.contentstack.okhttp.Call.AsyncCall; +import com.contentstack.okhttp.internal.Util; +import com.contentstack.okhttp.internal.http.HttpEngine; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Policy on when async requests are executed. + * + *

Each dispatcher uses an {@link ExecutorService} to run calls internally. If you + * supply your own executor, it should be able to run {@linkplain #getMaxRequests the + * configured maximum} number of calls concurrently. + */ +public final class Dispatcher { + private int maxRequests = 64; + private int maxRequestsPerHost = 5; + + /** Executes calls. Created lazily. */ + private ExecutorService executorService; + + /** Ready calls in the order they'll be run. */ + private final Deque readyCalls = new ArrayDeque(); + + /** Running calls. Includes canceled calls that haven't finished yet. */ + private final Deque runningCalls = new ArrayDeque(); + + public Dispatcher(ExecutorService executorService) { + this.executorService = executorService; + } + + public Dispatcher() { + } + + public synchronized ExecutorService getExecutorService() { + if (executorService == null) { + executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, + new LinkedBlockingQueue(), Util.threadFactory("OkHttp Dispatcher", false)); + } + return executorService; + } + + /** + * Set the maximum number of requests to execute concurrently. Above this + * requests queue in memory, waiting for the running calls to complete. + * + *

If more than {@code maxRequests} requests are in flight when this is + * invoked, those requests will remain in flight. + */ + public synchronized void setMaxRequests(int maxRequests) { + if (maxRequests < 1) { + throw new IllegalArgumentException("max < 1: " + maxRequests); + } + this.maxRequests = maxRequests; + promoteCalls(); + } + + public synchronized int getMaxRequests() { + return maxRequests; + } + + /** + * Set the maximum number of requests for each host to execute concurrently. + * This limits requests by the URL's host name. Note that concurrent requests + * to a single IP address may still exceed this limit: multiple hostnames may + * share an IP address or be routed through the same HTTP proxy. + * + *

If more than {@code maxRequestsPerHost} requests are in flight when this + * is invoked, those requests will remain in flight. + */ + public synchronized void setMaxRequestsPerHost(int maxRequestsPerHost) { + if (maxRequestsPerHost < 1) { + throw new IllegalArgumentException("max < 1: " + maxRequestsPerHost); + } + this.maxRequestsPerHost = maxRequestsPerHost; + promoteCalls(); + } + + public synchronized int getMaxRequestsPerHost() { + return maxRequestsPerHost; + } + + synchronized void enqueue(AsyncCall call) { + if (runningCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) { + runningCalls.add(call); + getExecutorService().execute(call); + } else { + readyCalls.add(call); + } + } + + /** Cancel all calls with the tag {@code tag}. */ + public synchronized void cancel(Object tag) { + for (Iterator i = readyCalls.iterator(); i.hasNext(); ) { + if (Util.equal(tag, i.next().tag())) i.remove(); + } + + for (AsyncCall call : runningCalls) { + if (Util.equal(tag, call.tag())) { + call.get().canceled = true; + HttpEngine engine = call.get().engine; + if (engine != null) engine.disconnect(); + } + } + } + + /** Used by {@code AsyncCall#run} to signal completion. */ + synchronized void finished(AsyncCall call) { + if (!runningCalls.remove(call)) throw new AssertionError("AsyncCall wasn't running!"); + promoteCalls(); + } + + private void promoteCalls() { + if (runningCalls.size() >= maxRequests) return; // Already running max capacity. + if (readyCalls.isEmpty()) return; // No ready calls to promote. + + for (Iterator i = readyCalls.iterator(); i.hasNext(); ) { + AsyncCall call = i.next(); + + if (runningCallsForHost(call) < maxRequestsPerHost) { + i.remove(); + runningCalls.add(call); + getExecutorService().execute(call); + } + + if (runningCalls.size() >= maxRequests) return; // Reached max capacity. + } + } + + /** Returns the number of running calls that share a host with {@code call}. */ + private int runningCallsForHost(AsyncCall call) { + int result = 0; + for (AsyncCall c : runningCalls) { + if (c.host().equals(call.host())) result++; + } + return result; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/FormEncodingBuilder.java b/contentstack/src/main/java/com/contentstack/okhttp/FormEncodingBuilder.java new file mode 100755 index 00000000..6ec12709 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/FormEncodingBuilder.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp; + +import com.contentstack.okhttp.internal.Util; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +/** + * Fluent API to build HTML + * 2.0-compliant form data. + */ +public final class FormEncodingBuilder { + private static final MediaType CONTENT_TYPE + = MediaType.parse("application/x-www-form-urlencoded"); + + private final StringBuilder content = new StringBuilder(); + + /** Add new key-value pair. */ + public FormEncodingBuilder add(String name, String value) { + if (content.length() > 0) { + content.append('&'); + } + try { + content.append(URLEncoder.encode(name, "UTF-8")) + .append('=') + .append(URLEncoder.encode(value, "UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + return this; + } + + public RequestBody build() { + if (content.length() == 0) { + throw new IllegalStateException("Form encoded body must have at least one part."); + } + + // Convert to bytes so RequestBody.create() doesn't add a charset to the content-type. + byte[] contentBytes = content.toString().getBytes(Util.UTF_8); + return RequestBody.create(CONTENT_TYPE, contentBytes); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/Handshake.java b/contentstack/src/main/java/com/contentstack/okhttp/Handshake.java new file mode 100755 index 00000000..68e6a905 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/Handshake.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contentstack.okhttp; + +import com.contentstack.okhttp.internal.Util; + +import java.security.Principal; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.List; + +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; + +/** + * A record of a TLS handshake. For HTTPS clients, the client is local + * and the remote server is its peer. + * + *

This value object describes a completed handshake. Use {@link + * javax.net.ssl.SSLSocketFactory} to set policy for new handshakes. + */ +public final class Handshake { + private final String cipherSuite; + private final List peerCertificates; + private final List localCertificates; + + private Handshake( + String cipherSuite, List peerCertificates, List localCertificates) { + this.cipherSuite = cipherSuite; + this.peerCertificates = peerCertificates; + this.localCertificates = localCertificates; + } + + public static Handshake get(SSLSession session) { + String cipherSuite = session.getCipherSuite(); + if (cipherSuite == null) throw new IllegalStateException("cipherSuite == null"); + + Certificate[] peerCertificates; + try { + peerCertificates = session.getPeerCertificates(); + } catch (SSLPeerUnverifiedException ignored) { + peerCertificates = null; + } + List peerCertificatesList = peerCertificates != null + ? Util.immutableList(peerCertificates) + : Collections.emptyList(); + + Certificate[] localCertificates = session.getLocalCertificates(); + List localCertificatesList = localCertificates != null + ? Util.immutableList(localCertificates) + : Collections.emptyList(); + + return new Handshake(cipherSuite, peerCertificatesList, localCertificatesList); + } + + public static Handshake get( + String cipherSuite, List peerCertificates, List localCertificates) { + if (cipherSuite == null) throw new IllegalArgumentException("cipherSuite == null"); + return new Handshake(cipherSuite, Util.immutableList(peerCertificates), + Util.immutableList(localCertificates)); + } + + /** Returns a cipher suite name like "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA". */ + public String cipherSuite() { + return cipherSuite; + } + + /** Returns a possibly-empty list of certificates that identify the remote peer. */ + public List peerCertificates() { + return peerCertificates; + } + + /** Returns the remote peer's principle, or null if that peer is anonymous. */ + public Principal peerPrincipal() { + return !peerCertificates.isEmpty() + ? ((X509Certificate) peerCertificates.get(0)).getSubjectX500Principal() + : null; + } + + /** Returns a possibly-empty list of certificates that identify this peer. */ + public List localCertificates() { + return localCertificates; + } + + /** Returns the local principle, or null if this peer is anonymous. */ + public Principal localPrincipal() { + return !localCertificates.isEmpty() + ? ((X509Certificate) localCertificates.get(0)).getSubjectX500Principal() + : null; + } + + @Override public boolean equals(Object other) { + if (!(other instanceof Handshake)) return false; + Handshake that = (Handshake) other; + return cipherSuite.equals(that.cipherSuite) + && peerCertificates.equals(that.peerCertificates) + && localCertificates.equals(that.localCertificates); + } + + @Override public int hashCode() { + int result = 17; + result = 31 * result + cipherSuite.hashCode(); + result = 31 * result + peerCertificates.hashCode(); + result = 31 * result + localCertificates.hashCode(); + return result; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/Headers.java b/contentstack/src/main/java/com/contentstack/okhttp/Headers.java new file mode 100755 index 00000000..34df08a9 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/Headers.java @@ -0,0 +1,244 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contentstack.okhttp; + +import com.contentstack.okhttp.internal.http.HttpDate; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +/** + * The header fields of a single HTTP message. Values are uninterpreted strings; + * use {@code Request} and {@code Response} for interpreted headers. This class + * maintains the order of the header fields within the HTTP message. + * + *

This class tracks header values line-by-line. A field with multiple comma- + * separated values on the same line will be treated as a field with a single + * value by this class. It is the caller's responsibility to detect and split + * on commas if their field permits multiple values. This simplifies use of + * single-valued fields whose values routinely contain commas, such as cookies + * or dates. + * + *

This class trims whitespace from values. It never returns values with + * leading or trailing whitespace. + * + *

Instances of this class are immutable. Use {@link Builder} to create + * instances. + */ +public final class Headers { + private final String[] namesAndValues; + + private Headers(Builder builder) { + this.namesAndValues = builder.namesAndValues.toArray(new String[builder.namesAndValues.size()]); + } + + private Headers(String[] namesAndValues) { + this.namesAndValues = namesAndValues; + } + + /** Returns the last value corresponding to the specified field, or null. */ + public String get(String name) { + return get(namesAndValues, name); + } + + /** + * Returns the last value corresponding to the specified field parsed as an + * HTTP date, or null if either the field is absent or cannot be parsed as a + * date. + */ + public Date getDate(String name) { + String value = get(name); + return value != null ? HttpDate.parse(value) : null; + } + + /** Returns the number of field values. */ + public int size() { + return namesAndValues.length / 2; + } + + /** Returns the field at {@code position} or null if that is out of range. */ + public String name(int index) { + int nameIndex = index * 2; + if (nameIndex < 0 || nameIndex >= namesAndValues.length) { + return null; + } + return namesAndValues[nameIndex]; + } + + /** Returns the value at {@code index} or null if that is out of range. */ + public String value(int index) { + int valueIndex = index * 2 + 1; + if (valueIndex < 0 || valueIndex >= namesAndValues.length) { + return null; + } + return namesAndValues[valueIndex]; + } + + /** Returns an immutable case-insensitive set of header names. */ + public Set names() { + TreeSet result = new TreeSet(String.CASE_INSENSITIVE_ORDER); + for (int i = 0; i < size(); i++) { + result.add(name(i)); + } + return Collections.unmodifiableSet(result); + } + + /** Returns an immutable list of the header values for {@code name}. */ + public List values(String name) { + List result = null; + for (int i = 0; i < size(); i++) { + if (name.equalsIgnoreCase(name(i))) { + if (result == null) result = new ArrayList(2); + result.add(value(i)); + } + } + return result != null + ? Collections.unmodifiableList(result) + : Collections.emptyList(); + } + + public Builder newBuilder() { + Builder result = new Builder(); + result.namesAndValues.addAll(Arrays.asList(namesAndValues)); + return result; + } + + @Override public String toString() { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < size(); i++) { + result.append(name(i)).append(": ").append(value(i)).append("\n"); + } + return result.toString(); + } + + private static String get(String[] namesAndValues, String name) { + for (int i = namesAndValues.length - 2; i >= 0; i -= 2) { + if (name.equalsIgnoreCase(namesAndValues[i])) { + return namesAndValues[i + 1]; + } + } + return null; + } + + /** + * Returns headers for the alternating header names and values. There must be + * an even number of arguments, and they must alternate between header names + * and values. + */ + public static Headers of(String... namesAndValues) { + if (namesAndValues == null || namesAndValues.length % 2 != 0) { + throw new IllegalArgumentException("Expected alternating header names and values"); + } + + // Make a defensive copy and clean it up. + namesAndValues = namesAndValues.clone(); + for (int i = 0; i < namesAndValues.length; i++) { + if (namesAndValues[i] == null) throw new IllegalArgumentException("Headers cannot be null"); + namesAndValues[i] = namesAndValues[i].trim(); + } + + // Check for malformed headers. + for (int i = 0; i < namesAndValues.length; i += 2) { + String name = namesAndValues[i]; + String value = namesAndValues[i + 1]; + if (name.length() == 0 || name.indexOf('\0') != -1 || value.indexOf('\0') != -1) { + throw new IllegalArgumentException("Unexpected header: " + name + ": " + value); + } + } + + return new Headers(namesAndValues); + } + + public static class Builder { + private final List namesAndValues = new ArrayList(20); + + /** Add an header line containing a field name, a literal colon, and a value. */ + Builder addLine(String line) { + int index = line.indexOf(":", 1); + if (index != -1) { + return addLenient(line.substring(0, index), line.substring(index + 1)); + } else if (line.startsWith(":")) { + // Work around empty header names and header names that start with a + // colon (created by old broken SPDY versions of the response cache). + return addLenient("", line.substring(1)); // Empty header name. + } else { + return addLenient("", line); // No header name. + } + } + + /** Add a field with the specified value. */ + public Builder add(String name, String value) { + if (name == null) throw new IllegalArgumentException("name == null"); + if (value == null) throw new IllegalArgumentException("value == null"); + if (name.length() == 0 || name.indexOf('\0') != -1 || value.indexOf('\0') != -1) { + throw new IllegalArgumentException("Unexpected header: " + name + ": " + value); + } + return addLenient(name, value); + } + + /** + * Add a field with the specified value without any validation. Only + * appropriate for headers from the remote peer. + */ + private Builder addLenient(String name, String value) { + namesAndValues.add(name); + namesAndValues.add(value.trim()); + return this; + } + + public Builder removeAll(String name) { + for (int i = 0; i < namesAndValues.size(); i += 2) { + if (name.equalsIgnoreCase(namesAndValues.get(i))) { + namesAndValues.remove(i); // name + namesAndValues.remove(i); // value + i -= 2; + } + } + return this; + } + + /** + * Set a field with the specified value. If the field is not found, it is + * added. If the field is found, the existing values are replaced. + */ + public Builder set(String name, String value) { + removeAll(name); + add(name, value); + return this; + } + + /** Equivalent to {@code build().get(name)}, but potentially faster. */ + public String get(String name) { + for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) { + if (name.equalsIgnoreCase(namesAndValues.get(i))) { + return namesAndValues.get(i + 1); + } + } + return null; + } + + public Headers build() { + return new Headers(this); + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/HostResolver.java b/contentstack/src/main/java/com/contentstack/okhttp/HostResolver.java new file mode 100755 index 00000000..4c98a2fa --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/HostResolver.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2012 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * Domain name service. Prefer this over {@link InetAddress#getAllByName} to + * make code more testable. + */ +public interface HostResolver { + HostResolver DEFAULT = new HostResolver() { + @Override public InetAddress[] getAllByName(String host) throws UnknownHostException { + if (host == null) throw new UnknownHostException("host == null"); + return InetAddress.getAllByName(host); + } + }; + + InetAddress[] getAllByName(String host) throws UnknownHostException; +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/MediaType.java b/contentstack/src/main/java/com/contentstack/okhttp/MediaType.java new file mode 100755 index 00000000..9c4d52b1 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/MediaType.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp; + +import java.nio.charset.Charset; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * An RFC 2045 Media Type, + * appropriate to describe the content type of an HTTP request or response body. + */ +public final class MediaType { + private static final String TOKEN = "([a-zA-Z0-9-!#$%&'*+.^_`{|}~]+)"; + private static final String QUOTED = "\"([^\"]*)\""; + private static final Pattern TYPE_SUBTYPE = Pattern.compile(TOKEN + "/" + TOKEN); + private static final Pattern PARAMETER = Pattern.compile( + ";\\s*(?:" + TOKEN + "=(?:" + TOKEN + "|" + QUOTED + "))?"); + + private final String mediaType; + private final String type; + private final String subtype; + private final String charset; + + private MediaType(String mediaType, String type, String subtype, String charset) { + this.mediaType = mediaType; + this.type = type; + this.subtype = subtype; + this.charset = charset; + } + + /** + * Returns a media type for {@code string}, or null if {@code string} is not a + * well-formed media type. + */ + public static MediaType parse(String string) { + Matcher typeSubtype = TYPE_SUBTYPE.matcher(string); + if (!typeSubtype.lookingAt()) return null; + String type = typeSubtype.group(1).toLowerCase(Locale.US); + String subtype = typeSubtype.group(2).toLowerCase(Locale.US); + + String charset = null; + Matcher parameter = PARAMETER.matcher(string); + for (int s = typeSubtype.end(); s < string.length(); s = parameter.end()) { + parameter.region(s, string.length()); + if (!parameter.lookingAt()) return null; // This is not a well-formed media type. + + String name = parameter.group(1); + if (name == null || !name.equalsIgnoreCase("charset")) continue; + String charsetParameter = parameter.group(2) != null + ? parameter.group(2) // Value is a token. + : parameter.group(3); // Value is a quoted string. + if (charset != null && !charsetParameter.equalsIgnoreCase(charset)) { + throw new IllegalArgumentException("Multiple different charsets: " + string); + } + charset = charsetParameter; + } + + return new MediaType(string, type, subtype, charset); + } + + /** + * Returns the high-level media type, such as "text", "image", "audio", + * "video", or "application". + */ + public String type() { + return type; + } + + /** + * Returns a specific media subtype, such as "plain" or "png", "mpeg", + * "mp4" or "xml". + */ + public String subtype() { + return subtype; + } + + /** + * Returns the charset of this media type, or null if this media type doesn't + * specify a charset. + */ + public Charset charset() { + return charset != null ? Charset.forName(charset) : null; + } + + /** + * Returns the charset of this media type, or {@code defaultValue} if this + * media type doesn't specify a charset. + */ + public Charset charset(Charset defaultValue) { + return charset != null ? Charset.forName(charset) : defaultValue; + } + + /** + * Returns the encoded media type, like "text/plain; charset=utf-8", + * appropriate for use in a Content-Type header. + */ + @Override public String toString() { + return mediaType; + } + + @Override public boolean equals(Object o) { + return o instanceof MediaType && ((MediaType) o).mediaType.equals(mediaType); + } + + @Override public int hashCode() { + return mediaType.hashCode(); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/MultipartBuilder.java b/contentstack/src/main/java/com/contentstack/okhttp/MultipartBuilder.java new file mode 100755 index 00000000..8aadd39a --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/MultipartBuilder.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp; + +import com.contentstack.okhttp.internal.Util; +import com.contentstack.okio.Buffer; +import com.contentstack.okio.BufferedSink; +import com.contentstack.okio.ByteString; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * Fluent API to build RFC + * 2387-compliant request bodies. + */ +public final class MultipartBuilder { + /** + * The "mixed" subtype of "multipart" is intended for use when the body + * parts are independent and need to be bundled in a particular order. Any + * "multipart" subtypes that an implementation does not recognize must be + * treated as being of subtype "mixed". + */ + public static final MediaType MIXED = MediaType.parse("multipart/mixed"); + + /** + * The "multipart/alternative" type is syntactically identical to + * "multipart/mixed", but the semantics are different. In particular, each + * of the body parts is an "alternative" version of the same information. + */ + public static final MediaType ALTERNATIVE = MediaType.parse("multipart/alternative"); + + /** + * This type is syntactically identical to "multipart/mixed", but the + * semantics are different. In particular, in a digest, the default {@code + * Content-Type} value for a body part is changed from "text/plain" to + * "message/rfc822". + */ + public static final MediaType DIGEST = MediaType.parse("multipart/digest"); + + /** + * This type is syntactically identical to "multipart/mixed", but the + * semantics are different. In particular, in a parallel entity, the order + * of body parts is not significant. + */ + public static final MediaType PARALLEL = MediaType.parse("multipart/parallel"); + + /** + * The media-type multipart/form-data follows the rules of all multipart + * MIME data streams as outlined in RFC 2046. In forms, there are a series + * of fields to be supplied by the user who fills out the form. Each field + * has a name. Within a given form, the names are unique. + */ + public static final MediaType FORM = MediaType.parse("multipart/form-data"); + + private static final byte[] COLONSPACE = { ':', ' ' }; + private static final byte[] CRLF = { '\r', '\n' }; + private static final byte[] DASHDASH = { '-', '-' }; + + private final ByteString boundary; + private MediaType type = MIXED; + private long length = 0; + + // Parallel lists of nullable headings (boundary + headers) and non-null bodies. + private final List partHeadings = new ArrayList(); + private final List partBodies = new ArrayList(); + + /** Creates a new multipart builder that uses a random boundary token. */ + public MultipartBuilder() { + this(UUID.randomUUID().toString()); + } + + /** + * Creates a new multipart builder that uses {@code boundary} to separate + * parts. Prefer the no-argument constructor to defend against injection + * attacks. + */ + public MultipartBuilder(String boundary) { + this.boundary = ByteString.encodeUtf8(boundary); + } + + /** + * Set the MIME type. Expected values for {@code type} are {@link #MIXED} (the + * default), {@link #ALTERNATIVE}, {@link #DIGEST}, {@link #PARALLEL} and + * {@link #FORM}. + */ + public MultipartBuilder type(MediaType type) { + if (type == null) { + throw new NullPointerException("type == null"); + } + if (!type.type().equals("multipart")) { + throw new IllegalArgumentException("multipart != " + type); + } + this.type = type; + return this; + } + + /** Add a part to the body. */ + public MultipartBuilder addPart(RequestBody body) { + return addPart(null, body); + } + + /** Add a part to the body. */ + public MultipartBuilder addPart(Headers headers, RequestBody body) { + if (body == null) { + throw new NullPointerException("body == null"); + } + if (headers != null && headers.get("Content-Type") != null) { + throw new IllegalArgumentException("Unexpected header: Content-Type"); + } + if (headers != null && headers.get("Content-Length") != null) { + throw new IllegalArgumentException("Unexpected header: Content-Length"); + } + + Buffer heading = createPartHeading(headers, body, partHeadings.isEmpty()); + partHeadings.add(heading); + partBodies.add(body); + + long bodyContentLength = body.contentLength(); + if (bodyContentLength == -1) { + length = -1; + } else if (length != -1) { + length += heading.size() + bodyContentLength; + } + + return this; + } + + /** + * Appends a quoted-string to a StringBuilder. + * + *

RFC 2388 is rather vague about how one should escape special characters + * in form-data parameters, and as it turns out Firefox and Chrome actually + * do rather different things, and both say in their comments that they're + * not really sure what the right approach is. We go with Chrome's behavior + * (which also experimentally seems to match what IE does), but if you + * actually want to have a good chance of things working, please avoid + * double-quotes, newlines, percent signs, and the like in your field names. + */ + private static StringBuilder appendQuotedString(StringBuilder target, String key) { + target.append('"'); + for (int i = 0, len = key.length(); i < len; i++) { + char ch = key.charAt(i); + switch (ch) { + case '\n': + target.append("%0A"); + break; + case '\r': + target.append("%0D"); + break; + case '"': + target.append("%22"); + break; + default: + target.append(ch); + break; + } + } + target.append('"'); + return target; + } + + /** Add a form data part to the body. */ + public MultipartBuilder addFormDataPart(String name, String value) { + return addFormDataPart(name, null, RequestBody.create(null, value)); + } + + /** Add a form data part to the body. */ + public MultipartBuilder addFormDataPart(String name, String filename, RequestBody value) { + if (name == null) { + throw new NullPointerException("name == null"); + } + StringBuilder disposition = new StringBuilder("form-data; name="); + appendQuotedString(disposition, name); + + if (filename != null) { + disposition.append("; filename="); + appendQuotedString(disposition, filename); + } + + return addPart(Headers.of("Content-Disposition", disposition.toString()), value); + } + + /** Creates a part "heading" from the boundary and any real or generated headers. */ + private Buffer createPartHeading(Headers headers, RequestBody body, boolean isFirst) { + Buffer sink = new Buffer(); + + if (!isFirst) { + sink.write(CRLF); + } + sink.write(DASHDASH); + sink.write(boundary); + sink.write(CRLF); + + if (headers != null) { + for (int i = 0; i < headers.size(); i++) { + sink.writeUtf8(headers.name(i)) + .write(COLONSPACE) + .writeUtf8(headers.value(i)) + .write(CRLF); + } + } + + MediaType contentType = body.contentType(); + if (contentType != null) { + sink.writeUtf8("Content-Type: ") + .writeUtf8(contentType.toString()) + .write(CRLF); + } + + long contentLength = body.contentLength(); + if (contentLength != -1) { + sink.writeUtf8("Content-Length: ") + .writeUtf8(Long.toString(contentLength)) + .write(CRLF); + } + + sink.write(CRLF); + + return sink; + } + + /** Assemble the specified parts into a request body. */ + public RequestBody build() { + if (partHeadings.isEmpty()) { + throw new IllegalStateException("Multipart body must have at least one part."); + } + return new MultipartRequestBody(type, boundary, partHeadings, partBodies, length); + } + + private static final class MultipartRequestBody extends RequestBody { + private final ByteString boundary; + private final MediaType contentType; + private final List partHeadings; + private final List partBodies; + private final long length; + + public MultipartRequestBody(MediaType type, ByteString boundary, List partHeadings, + List partBodies, long length) { + if (type == null) throw new NullPointerException("type == null"); + + this.boundary = boundary; + this.contentType = MediaType.parse(type + "; boundary=" + boundary.utf8()); + this.partHeadings = Util.immutableList(partHeadings); + this.partBodies = Util.immutableList(partBodies); + if (length != -1) { + // Add the length of the final boundary. + length += CRLF.length + DASHDASH.length + boundary.size() + DASHDASH.length + CRLF.length; + } + this.length = length; + } + + @Override public long contentLength() { + return length; + } + + @Override public MediaType contentType() { + return contentType; + } + + @Override public void writeTo(BufferedSink sink) throws IOException { + for (int i = 0, size = partHeadings.size(); i < size; i++) { + sink.writeAll(partHeadings.get(i).clone()); + partBodies.get(i).writeTo(sink); + } + + sink.write(CRLF); + sink.write(DASHDASH); + sink.write(boundary); + sink.write(DASHDASH); + sink.write(CRLF); + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/OkHttpClient.java b/contentstack/src/main/java/com/contentstack/okhttp/OkHttpClient.java new file mode 100755 index 00000000..65c6b53f --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/OkHttpClient.java @@ -0,0 +1,548 @@ +/* + * Copyright (C) 2012 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp; + +import com.contentstack.okhttp.internal.Internal; +import com.contentstack.okhttp.internal.InternalCache; +import com.contentstack.okhttp.internal.RouteDatabase; +import com.contentstack.okhttp.internal.Util; +import com.contentstack.okhttp.internal.http.AuthenticatorAdapter; +import com.contentstack.okhttp.internal.http.HttpEngine; +import com.contentstack.okhttp.internal.http.Transport; +import com.contentstack.okhttp.internal.tls.OkHostnameVerifier; + +import java.io.IOException; +import java.net.CookieHandler; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.URLConnection; +import java.security.GeneralSecurityException; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.net.SocketFactory; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; + +/** + * Configures and creates HTTP connections. Most applications can use a single + * OkHttpClient for all of their HTTP requests - benefiting from a shared + * response cache, thread pool, connection re-use, etc. + * + *

Instances of OkHttpClient are intended to be fully configured before they're + * shared - once shared they should be treated as immutable and can safely be used + * to concurrently open new connections. If required, threads can call + * {@link #clone()} to make a shallow copy of the OkHttpClient that can be + * safely modified with further configuration changes. + */ +public class OkHttpClient implements Cloneable { + static { + Internal.instance = new Internal() { + @Override public Transport newTransport( + Connection connection, HttpEngine httpEngine) throws IOException { + return connection.newTransport(httpEngine); + } + + @Override public boolean clearOwner(Connection connection) { + return connection.clearOwner(); + } + + @Override public void closeIfOwnedBy(Connection connection, Object owner) throws IOException { + connection.closeIfOwnedBy(owner); + } + + @Override public int recycleCount(Connection connection) { + return connection.recycleCount(); + } + + @Override public void setProtocol(Connection connection, Protocol protocol) { + connection.setProtocol(protocol); + } + + @Override public void setOwner(Connection connection, HttpEngine httpEngine) { + connection.setOwner(httpEngine); + } + + @Override public boolean isReadable(Connection pooled) { + return pooled.isReadable(); + } + + @Override public void addLine(Headers.Builder builder, String line) { + builder.addLine(line); + } + + @Override public void setCache(OkHttpClient client, InternalCache internalCache) { + client.setInternalCache(internalCache); + } + + @Override public InternalCache internalCache(OkHttpClient client) { + return client.internalCache(); + } + + @Override public void recycle(ConnectionPool pool, Connection connection) { + pool.recycle(connection); + } + + @Override public RouteDatabase routeDatabase(OkHttpClient client) { + return client.routeDatabase(); + } + + @Override public void connectAndSetOwner(OkHttpClient client, Connection connection, + HttpEngine owner, Request request) throws IOException { + connection.connectAndSetOwner(client, owner, request); + } + }; + } + + /** Lazily-initialized. */ + private static SSLSocketFactory defaultSslSocketFactory; + + private final RouteDatabase routeDatabase; + private Dispatcher dispatcher; + private Proxy proxy; + private List protocols; + private ProxySelector proxySelector; + private CookieHandler cookieHandler; + + /** Non-null if this client is caching; possibly by {@code cache}. */ + private InternalCache internalCache; + private Cache cache; + + private SocketFactory socketFactory; + private SSLSocketFactory sslSocketFactory; + private HostnameVerifier hostnameVerifier; + private Authenticator authenticator; + private ConnectionPool connectionPool; + private HostResolver hostResolver; + private boolean followSslRedirects = true; + private boolean followRedirects = true; + private int connectTimeout; + private int readTimeout; + private int writeTimeout; + + public OkHttpClient() { + routeDatabase = new RouteDatabase(); + dispatcher = new Dispatcher(); + } + + private OkHttpClient(OkHttpClient okHttpClient) { + this.routeDatabase = okHttpClient.routeDatabase(); + this.dispatcher = okHttpClient.getDispatcher(); + this.proxy = okHttpClient.getProxy(); + this.protocols = okHttpClient.getProtocols(); + this.proxySelector = okHttpClient.getProxySelector(); + this.cookieHandler = okHttpClient.getCookieHandler(); + this.cache = okHttpClient.getCache(); + this.internalCache = cache != null ? cache.internalCache : okHttpClient.internalCache; + this.socketFactory = okHttpClient.getSocketFactory(); + this.sslSocketFactory = okHttpClient.getSslSocketFactory(); + this.hostnameVerifier = okHttpClient.getHostnameVerifier(); + this.authenticator = okHttpClient.getAuthenticator(); + this.connectionPool = okHttpClient.getConnectionPool(); + this.followSslRedirects = okHttpClient.getFollowSslRedirects(); + this.followRedirects = okHttpClient.getFollowRedirects(); + this.connectTimeout = okHttpClient.getConnectTimeout(); + this.readTimeout = okHttpClient.getReadTimeout(); + this.writeTimeout = okHttpClient.getWriteTimeout(); + } + + /** + * Sets the default connect timeout for new connections. A value of 0 means no timeout. + * + * @see URLConnection#setConnectTimeout(int) + */ + public final void setConnectTimeout(long timeout, TimeUnit unit) { + if (timeout < 0) throw new IllegalArgumentException("timeout < 0"); + if (unit == null) throw new IllegalArgumentException("unit == null"); + long millis = unit.toMillis(timeout); + if (millis > Integer.MAX_VALUE) throw new IllegalArgumentException("Timeout too large."); + connectTimeout = (int) millis; + } + + /** Default connect timeout (in milliseconds). */ + public final int getConnectTimeout() { + return connectTimeout; + } + + /** + * Sets the default read timeout for new connections. A value of 0 means no timeout. + * + * @see URLConnection#setReadTimeout(int) + */ + public final void setReadTimeout(long timeout, TimeUnit unit) { + if (timeout < 0) throw new IllegalArgumentException("timeout < 0"); + if (unit == null) throw new IllegalArgumentException("unit == null"); + long millis = unit.toMillis(timeout); + if (millis > Integer.MAX_VALUE) throw new IllegalArgumentException("Timeout too large."); + readTimeout = (int) millis; + } + + /** Default read timeout (in milliseconds). */ + public final int getReadTimeout() { + return readTimeout; + } + + /** + * Sets the default write timeout for new connections. A value of 0 means no timeout. + */ + public final void setWriteTimeout(long timeout, TimeUnit unit) { + if (timeout < 0) throw new IllegalArgumentException("timeout < 0"); + if (unit == null) throw new IllegalArgumentException("unit == null"); + long millis = unit.toMillis(timeout); + if (millis > Integer.MAX_VALUE) throw new IllegalArgumentException("Timeout too large."); + writeTimeout = (int) millis; + } + + /** Default write timeout (in milliseconds). */ + public final int getWriteTimeout() { + return writeTimeout; + } + + /** + * Sets the HTTP proxy that will be used by connections created by this + * client. This takes precedence over {@link #setProxySelector}, which is + * only honored when this proxy is null (which it is by default). To disable + * proxy use completely, call {@code setProxy(Proxy.NO_PROXY)}. + */ + public final OkHttpClient setProxy(Proxy proxy) { + this.proxy = proxy; + return this; + } + + public final Proxy getProxy() { + return proxy; + } + + /** + * Sets the proxy selection policy to be used if no {@link #setProxy proxy} + * is specified explicitly. The proxy selector may return multiple proxies; + * in that case they will be tried in sequence until a successful connection + * is established. + * + *

If unset, the {@link ProxySelector#getDefault() system-wide default} + * proxy selector will be used. + */ + public final OkHttpClient setProxySelector(ProxySelector proxySelector) { + this.proxySelector = proxySelector; + return this; + } + + public final ProxySelector getProxySelector() { + return proxySelector; + } + + /** + * Sets the cookie handler to be used to read outgoing cookies and write + * incoming cookies. + * + *

If unset, the {@link CookieHandler#getDefault() system-wide default} + * cookie handler will be used. + */ + public final OkHttpClient setCookieHandler(CookieHandler cookieHandler) { + this.cookieHandler = cookieHandler; + return this; + } + + public final CookieHandler getCookieHandler() { + return cookieHandler; + } + + /** Sets the response cache to be used to read and write cached responses. */ + final void setInternalCache(InternalCache internalCache) { + this.internalCache = internalCache; + this.cache = null; + } + + final InternalCache internalCache() { + return internalCache; + } + + public final OkHttpClient setCache(Cache cache) { + this.cache = cache; + this.internalCache = null; + return this; + } + + public final Cache getCache() { + return cache; + } + + /** + * Sets the socket factory used to create connections. + * + *

If unset, the {@link SocketFactory#getDefault() system-wide default} + * socket factory will be used. + */ + public final OkHttpClient setSocketFactory(SocketFactory socketFactory) { + this.socketFactory = socketFactory; + return this; + } + + public final SocketFactory getSocketFactory() { + return socketFactory; + } + + /** + * Sets the socket factory used to secure HTTPS connections. + * + *

If unset, a lazily created SSL socket factory will be used. + */ + public final OkHttpClient setSslSocketFactory(SSLSocketFactory sslSocketFactory) { + this.sslSocketFactory = sslSocketFactory; + return this; + } + + public final SSLSocketFactory getSslSocketFactory() { + return sslSocketFactory; + } + + /** + * Sets the verifier used to confirm that response certificates apply to + * requested hostnames for HTTPS connections. + * + *

If unset, the + * {@link javax.net.ssl.HttpsURLConnection#getDefaultHostnameVerifier() + * system-wide default} hostname verifier will be used. + */ + public final OkHttpClient setHostnameVerifier(HostnameVerifier hostnameVerifier) { + this.hostnameVerifier = hostnameVerifier; + return this; + } + + public final HostnameVerifier getHostnameVerifier() { + return hostnameVerifier; + } + + /** + * Sets the authenticator used to respond to challenges from the remote web + * server or proxy server. + * + *

If unset, the {@link java.net.Authenticator#setDefault system-wide default} + * authenticator will be used. + */ + public final OkHttpClient setAuthenticator(Authenticator authenticator) { + this.authenticator = authenticator; + return this; + } + + public final Authenticator getAuthenticator() { + return authenticator; + } + + /** + * Sets the connection pool used to recycle HTTP and HTTPS connections. + * + *

If unset, the {@link ConnectionPool#getDefault() system-wide + * default} connection pool will be used. + */ + public final OkHttpClient setConnectionPool(ConnectionPool connectionPool) { + this.connectionPool = connectionPool; + return this; + } + + public final ConnectionPool getConnectionPool() { + return connectionPool; + } + + /** + * Configure this client to follow redirects from HTTPS to HTTP and from HTTP + * to HTTPS. + * + *

If unset, protocol redirects will be followed. This is different than + * the built-in {@code HttpURLConnection}'s default. + */ + public final OkHttpClient setFollowSslRedirects(boolean followProtocolRedirects) { + this.followSslRedirects = followProtocolRedirects; + return this; + } + + public final boolean getFollowSslRedirects() { + return followSslRedirects; + } + + /** + * Configure this client to follow redirects. + * + *

If unset, redirects will not be followed. This is the equivalent as the + * built-in {@code HttpURLConnection}'s default. + */ + public final void setFollowRedirects(boolean followRedirects) { + this.followRedirects = followRedirects; + } + + public final boolean getFollowRedirects() { + return followRedirects; + } + + final RouteDatabase routeDatabase() { + return routeDatabase; + } + + /** + * Sets the dispatcher used to set policy and execute asynchronous requests. + * Must not be null. + */ + public final OkHttpClient setDispatcher(Dispatcher dispatcher) { + if (dispatcher == null) throw new IllegalArgumentException("dispatcher == null"); + this.dispatcher = dispatcher; + return this; + } + + public final Dispatcher getDispatcher() { + return dispatcher; + } + + /** + * Configure the protocols used by this client to communicate with remote + * servers. By default this client will prefer the most efficient transport + * available, falling back to more ubiquitous protocols. Applications should + * only call this method to avoid specific compatibility problems, such as web + * servers that behave incorrectly when SPDY is enabled. + * + *

The following protocols are currently supported: + *

+ * + *

This is an evolving set. Future releases may drop + * support for transitional protocols (like h2-13), in favor of their + * successors (h2). The http/1.1 transport will never be dropped. + * + *

If multiple protocols are specified, NPN or + * ALPN + * will be used to negotiate a transport. + * + * @param protocols the protocols to use, in order of preference. The list + * must contain {@link Protocol#HTTP_1_1}. It must not contain null. + */ + public final OkHttpClient setProtocols(List protocols) { + protocols = Util.immutableList(protocols); + if (!protocols.contains(Protocol.HTTP_1_1)) { + throw new IllegalArgumentException("protocols doesn't contain http/1.1: " + protocols); + } + if (protocols.contains(null)) { + throw new IllegalArgumentException("protocols must not contain null"); + } + this.protocols = Util.immutableList(protocols); + return this; + } + + public final List getProtocols() { + return protocols; + } + + /* + * Sets the {@code HostResolver} that will be used by this client to resolve + * hostnames to IP addresses. + */ + public OkHttpClient setHostResolver(HostResolver hostResolver) { + this.hostResolver = hostResolver; + return this; + } + + public HostResolver getHostResolver() { + return hostResolver; + } + + /** + * Prepares the {@code request} to be executed at some point in the future. + */ + public Call newCall(Request request) { + return new Call(this, request); + } + + /** + * Cancels all scheduled tasks tagged with {@code tag}. Requests that are already + * complete cannot be canceled. + */ + public OkHttpClient cancel(Object tag) { + getDispatcher().cancel(tag); + return this; + } + + /** + * Returns a shallow copy of this OkHttpClient that uses the system-wide + * default for each field that hasn't been explicitly configured. + */ + final OkHttpClient copyWithDefaults() { + OkHttpClient result = new OkHttpClient(this); + if (result.proxySelector == null) { + result.proxySelector = ProxySelector.getDefault(); + } + if (result.cookieHandler == null) { + result.cookieHandler = CookieHandler.getDefault(); + } + if (result.socketFactory == null) { + result.socketFactory = SocketFactory.getDefault(); + } + if (result.sslSocketFactory == null) { + result.sslSocketFactory = getDefaultSSLSocketFactory(); + } + if (result.hostnameVerifier == null) { + result.hostnameVerifier = OkHostnameVerifier.INSTANCE; + } + if (result.authenticator == null) { + result.authenticator = AuthenticatorAdapter.INSTANCE; + } + if (result.connectionPool == null) { + result.connectionPool = ConnectionPool.getDefault(); + } + if (result.protocols == null) { + result.protocols = Util.immutableList(Protocol.HTTP_2, Protocol.SPDY_3, Protocol.HTTP_1_1); + } + if (result.hostResolver == null) { + result.hostResolver = HostResolver.DEFAULT; + } + return result; + } + + /** + * Java and Android programs default to using a single global SSL context, + * accessible to HTTP clients as {@link SSLSocketFactory#getDefault()}. If we + * used the shared SSL context, when OkHttp enables NPN for its SPDY-related + * stuff, it would also enable NPN for other usages, which might crash them + * because NPN is enabled when it isn't expected to be. + * + *

This code avoids that by defaulting to an OkHttp-created SSL context. + * The drawback of this approach is that apps that customize the global SSL + * context will lose these customizations. + */ + private synchronized SSLSocketFactory getDefaultSSLSocketFactory() { + if (defaultSslSocketFactory == null) { + try { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, null, null); + defaultSslSocketFactory = sslContext.getSocketFactory(); + } catch (GeneralSecurityException e) { + throw new AssertionError(); // The system has no TLS. Just give up. + } + } + return defaultSslSocketFactory; + } + + /** Returns a shallow copy of this OkHttpClient. */ + @Override public final OkHttpClient clone() { + try { + return (OkHttpClient) super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(); + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/OkUrlFactory.java b/contentstack/src/main/java/com/contentstack/okhttp/OkUrlFactory.java new file mode 100755 index 00000000..278c1396 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/OkUrlFactory.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp; + +import com.contentstack.okhttp.internal.InternalCache; +import com.contentstack.okhttp.internal.huc.CacheAdapter; +import com.contentstack.okhttp.internal.huc.HttpURLConnectionImpl; +import com.contentstack.okhttp.internal.huc.HttpsURLConnectionImpl; + +import java.net.HttpURLConnection; +import java.net.Proxy; +import java.net.ResponseCache; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; + +public final class OkUrlFactory implements URLStreamHandlerFactory, Cloneable { + private final OkHttpClient client; + + public OkUrlFactory(OkHttpClient client) { + this.client = client; + } + + public OkHttpClient client() { + return client; + } + + /** Sets the response cache to be used to read and write cached responses. */ + OkUrlFactory setResponseCache(ResponseCache responseCache) { + client.setInternalCache(responseCache != null ? new CacheAdapter(responseCache) : null); + return this; + } + + ResponseCache getResponseCache() { + InternalCache cache = client.internalCache(); + return cache instanceof CacheAdapter ? ((CacheAdapter) cache).getDelegate() : null; + } + + /** + * Returns a copy of this stream handler factory that includes a shallow copy + * of the internal {@linkplain OkHttpClient HTTP client}. + */ + @Override public OkUrlFactory clone() { + return new OkUrlFactory(client.clone()); + } + + public HttpURLConnection open(URL url) { + return open(url, client.getProxy()); + } + + HttpURLConnection open(URL url, Proxy proxy) { + String protocol = url.getProtocol(); + OkHttpClient copy = client.copyWithDefaults(); + copy.setProxy(proxy); + + if (protocol.equals("http")) return new HttpURLConnectionImpl(url, copy); + if (protocol.equals("https")) return new HttpsURLConnectionImpl(url, copy); + throw new IllegalArgumentException("Unexpected protocol: " + protocol); + } + + /** + * Creates a URLStreamHandler as a {@link java.net.URL#setURLStreamHandlerFactory}. + * + *

This code configures OkHttp to handle all HTTP and HTTPS connections + * created with {@link java.net.URL#openConnection()}:

   {@code
+   *
+   *   OkHttpClient okHttpClient = new OkHttpClient();
+   *   URL.setURLStreamHandlerFactory(new OkUrlFactory(okHttpClient));
+   * }
+ */ + @Override public URLStreamHandler createURLStreamHandler(final String protocol) { + if (!protocol.equals("http") && !protocol.equals("https")) return null; + + return new URLStreamHandler() { + @Override protected URLConnection openConnection(URL url) { + return open(url); + } + + @Override protected URLConnection openConnection(URL url, Proxy proxy) { + return open(url, proxy); + } + + @Override protected int getDefaultPort() { + if (protocol.equals("http")) return 80; + if (protocol.equals("https")) return 443; + throw new AssertionError(); + } + }; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/Protocol.java b/contentstack/src/main/java/com/contentstack/okhttp/Protocol.java new file mode 100755 index 00000000..986768ba --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/Protocol.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp; + +import java.io.IOException; + +/** + * Protocols that OkHttp implements for NPN and + * ALPN + * selection. + * + *

Protocol vs Scheme

+ * Despite its name, {@link java.net.URL#getProtocol()} returns the + * {@linkplain java.net.URI#getScheme() scheme} (http, https, etc.) of the URL, not + * the protocol (http/1.1, spdy/3.1, etc.). OkHttp uses the word protocol + * to identify how HTTP messages are framed. + */ +public enum Protocol { + /** + * An obsolete plaintext framing that does not use persistent sockets by + * default. + */ + HTTP_1_0("http/1.0"), + + /** + * A plaintext framing that includes persistent connections. + * + *

This version of OkHttp implements RFC 2616, and tracks + * revisions to that spec. + */ + HTTP_1_1("http/1.1"), + + /** + * Chromium's binary-framed protocol that includes header compression, + * multiplexing multiple requests on the same socket, and server-push. + * HTTP/1.1 semantics are layered on SPDY/3. + * + *

This version of OkHttp implements SPDY 3 draft + * 3.1. Future releases of OkHttp may use this identifier for a newer draft + * of the SPDY spec. + */ + SPDY_3("spdy/3.1"), + + /** + * The IETF's binary-framed protocol that includes header compression, + * multiplexing multiple requests on the same socket, and server-push. + * HTTP/1.1 semantics are layered on HTTP/2. + * + *

This version of OkHttp implements HTTP/2 draft 12 + * with HPACK draft + * 6. Future releases of OkHttp may use this identifier for a newer draft + * of these specs. + */ + HTTP_2("h2-13"); + + private final String protocol; + + Protocol(String protocol) { + this.protocol = protocol; + } + + /** + * Returns the protocol identified by {@code protocol}. + * @throws IOException if {@code protocol} is unknown. + */ + public static Protocol get(String protocol) throws IOException { + // Unroll the loop over values() to save an allocation. + if (protocol.equals(HTTP_1_0.protocol)) return HTTP_1_0; + if (protocol.equals(HTTP_1_1.protocol)) return HTTP_1_1; + if (protocol.equals(HTTP_2.protocol)) return HTTP_2; + if (protocol.equals(SPDY_3.protocol)) return SPDY_3; + throw new IOException("Unexpected protocol: " + protocol); + } + + /** + * Returns the string used to identify this protocol for ALPN and NPN, like + * "http/1.1", "spdy/3.1" or "h2-13". + */ + @Override public String toString() { + return protocol; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/Request.java b/contentstack/src/main/java/com/contentstack/okhttp/Request.java new file mode 100755 index 00000000..545620d6 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/Request.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp; + +import com.contentstack.okhttp.internal.Platform; +import com.contentstack.okhttp.internal.http.HttpMethod; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.List; + +/** + * An HTTP request. Instances of this class are immutable if their {@link #body} + * is null or itself immutable. + */ +public final class Request { + private final String urlString; + private final String method; + private final Headers headers; + private final RequestBody body; + private final Object tag; + + private volatile URL url; // Lazily initialized. + private volatile URI uri; // Lazily initialized. + private volatile CacheControl cacheControl; // Lazily initialized. + + private Request(Builder builder) { + this.urlString = builder.urlString; + this.method = builder.method; + this.headers = builder.headers.build(); + this.body = builder.body; + this.tag = builder.tag != null ? builder.tag : this; + this.url = builder.url; + } + + public URL url() { + try { + URL result = url; + return result != null ? result : (url = new URL(urlString)); + } catch (MalformedURLException e) { + throw new RuntimeException("Malformed URL: " + urlString, e); + } + } + + public URI uri() throws IOException { + try { + URI result = uri; + return result != null ? result : (uri = Platform.get().toUriLenient(url)); + } catch (URISyntaxException e) { + throw new IOException(e.getMessage()); + } + } + + public String urlString() { + return urlString; + } + + public String method() { + return method; + } + + public Headers headers() { + return headers; + } + + public String header(String name) { + return headers.get(name); + } + + public List headers(String name) { + return headers.values(name); + } + + public RequestBody body() { + return body; + } + + public Object tag() { + return tag; + } + + public Builder newBuilder() { + return new Builder(this); + } + + /** + * Returns the cache control directives for this response. This is never null, + * even if this response contains no {@code Cache-Control} header. + */ + public CacheControl cacheControl() { + CacheControl result = cacheControl; + return result != null ? result : (cacheControl = CacheControl.parse(headers)); + } + + public boolean isHttps() { + return url().getProtocol().equals("https"); + } + + @Override public String toString() { + return "Request{method=" + + method + + ", url=" + + urlString + + ", tag=" + + (tag != this ? tag : null) + + '}'; + } + + public static class Builder { + private String urlString; + private URL url; + private String method; + private Headers.Builder headers; + private RequestBody body; + private Object tag; + + public Builder() { + this.method = "GET"; + this.headers = new Headers.Builder(); + } + + private Builder(Request request) { + this.urlString = request.urlString; + this.url = request.url; + this.method = request.method; + this.body = request.body; + this.tag = request.tag; + this.headers = request.headers.newBuilder(); + } + + public Builder url(String url) { + if (url == null) throw new IllegalArgumentException("url == null"); + urlString = url; + return this; + } + + public Builder url(URL url) { + if (url == null) throw new IllegalArgumentException("url == null"); + this.url = url; + this.urlString = url.toString(); + return this; + } + + /** + * Sets the header named {@code name} to {@code value}. If this request + * already has any headers with that name, they are all replaced. + */ + public Builder header(String name, String value) { + headers.set(name, value); + return this; + } + + /** + * Adds a header with {@code name} and {@code value}. Prefer this method for + * multiply-valued headers like "Cookie". + */ + public Builder addHeader(String name, String value) { + headers.add(name, value); + return this; + } + + public Builder removeHeader(String name) { + headers.removeAll(name); + return this; + } + + /** Removes all headers on this builder and adds {@code headers}. */ + public Builder headers(Headers headers) { + this.headers = headers.newBuilder(); + return this; + } + + public Builder get() { + return method("GET", null); + } + + public Builder head() { + return method("HEAD", null); + } + + public Builder post(RequestBody body) { + return method("POST", body); + } + + public Builder delete() { + return method("DELETE", null); + } + + public Builder put(RequestBody body) { + return method("PUT", body); + } + + public Builder patch(RequestBody body) { + return method("PATCH", body); + } + + public Builder method(String method, RequestBody body) { + if (method == null || method.length() == 0) { + throw new IllegalArgumentException("method == null || method.length() == 0"); + } + if (body != null && !HttpMethod.hasRequestBody(method)) { + throw new IllegalArgumentException("method " + method + " must not have a request body."); + } + this.method = method; + this.body = body; + return this; + } + + /** + * Attaches {@code tag} to the request. It can be used later to cancel the + * request. If the tag is unspecified or null, the request is canceled by + * using the request itself as the tag. + */ + public Builder tag(Object tag) { + this.tag = tag; + return this; + } + + public Request build() { + if (urlString == null) throw new IllegalStateException("url == null"); + return new Request(this); + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/RequestBody.java b/contentstack/src/main/java/com/contentstack/okhttp/RequestBody.java new file mode 100755 index 00000000..111cae92 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/RequestBody.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp; + +import com.contentstack.okhttp.internal.Util; +import com.contentstack.okio.BufferedSink; +import com.contentstack.okio.Okio; +import com.contentstack.okio.Source; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; + +public abstract class RequestBody { + /** Returns the Content-Type header for this body. */ + public abstract MediaType contentType(); + + /** + * Returns the number of bytes that will be written to {@code out} in a call + * to {@link #writeTo}, or -1 if that count is unknown. + */ + public long contentLength() { + return -1; + } + + /** Writes the content of this request to {@code out}. */ + public abstract void writeTo(BufferedSink sink) throws IOException; + + /** + * Returns a new request body that transmits {@code content}. If {@code + * contentType} is non-null and lacks a charset, this will use UTF-8. + */ + public static RequestBody create(MediaType contentType, String content) { + Charset charset = Util.UTF_8; + if (contentType != null) { + charset = contentType.charset(); + if (charset == null) { + charset = Util.UTF_8; + contentType = MediaType.parse(contentType + "; charset=utf-8"); + } + } + byte[] bytes = content.getBytes(charset); + return create(contentType, bytes); + } + + /** Returns a new request body that transmits {@code content}. */ + public static RequestBody create(final MediaType contentType, final byte[] content) { + if (content == null) throw new NullPointerException("content == null"); + + return new RequestBody() { + @Override public MediaType contentType() { + return contentType; + } + + @Override public long contentLength() { + return content.length; + } + + @Override public void writeTo(BufferedSink sink) throws IOException { + sink.write(content); + } + }; + } + + /** Returns a new request body that transmits the content of {@code file}. */ + public static RequestBody create(final MediaType contentType, final File file) { + if (file == null) throw new NullPointerException("content == null"); + + return new RequestBody() { + @Override public MediaType contentType() { + return contentType; + } + + @Override public long contentLength() { + return file.length(); + } + + @Override public void writeTo(BufferedSink sink) throws IOException { + Source source = null; + try { + source = Okio.source(file); + sink.writeAll(source); + } finally { + Util.closeQuietly(source); + } + } + }; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/Response.java b/contentstack/src/main/java/com/contentstack/okhttp/Response.java new file mode 100755 index 00000000..f751f012 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/Response.java @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp; + +import com.contentstack.okhttp.internal.http.OkHeaders; + +import java.util.Collections; +import java.util.List; + +import static com.contentstack.okhttp.internal.http.StatusLine.HTTP_TEMP_REDIRECT; +import static java.net.HttpURLConnection.HTTP_MOVED_PERM; +import static java.net.HttpURLConnection.HTTP_MOVED_TEMP; +import static java.net.HttpURLConnection.HTTP_MULT_CHOICE; +import static java.net.HttpURLConnection.HTTP_PROXY_AUTH; +import static java.net.HttpURLConnection.HTTP_SEE_OTHER; +import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; + +/** + * An HTTP response. Instances of this class are not immutable: the response + * body is a one-shot value that may be consumed only once. All other properties + * are immutable. + */ +public final class Response { + private final Request request; + private final Protocol protocol; + private final int code; + private final String message; + private final Handshake handshake; + private final Headers headers; + private final ResponseBody body; + private Response networkResponse; + private Response cacheResponse; + private final Response priorResponse; + + private volatile CacheControl cacheControl; // Lazily initialized. + + private Response(Builder builder) { + this.request = builder.request; + this.protocol = builder.protocol; + this.code = builder.code; + this.message = builder.message; + this.handshake = builder.handshake; + this.headers = builder.headers.build(); + this.body = builder.body; + this.networkResponse = builder.networkResponse; + this.cacheResponse = builder.cacheResponse; + this.priorResponse = builder.priorResponse; + } + + /** + * The wire-level request that initiated this HTTP response. This is not + * necessarily the same request issued by the application: + *

    + *
  • It may be transformed by the HTTP client. For example, the client + * may copy headers like {@code Content-Length} from the request body. + *
  • It may be the request generated in response to an HTTP redirect or + * authentication challenge. In this case the request URL may be + * different than the initial request URL. + *
+ */ + public Request request() { + return request; + } + + /** + * Returns the HTTP protocol, such as {@link Protocol#HTTP_1_1} or {@link + * Protocol#HTTP_1_0}. + */ + public Protocol protocol() { + return protocol; + } + + /** Returns the HTTP status code. */ + public int code() { + return code; + } + + /** + * Returns true if the code is in [200..300), which means the request was + * successfully received, understood, and accepted. + */ + public boolean isSuccessful() { + return code >= 200 && code < 300; + } + + /** Returns the HTTP status message or null if it is unknown. */ + public String message() { + return message; + } + + /** + * Returns the TLS handshake of the connection that carried this response, or + * null if the response was received without TLS. + */ + public Handshake handshake() { + return handshake; + } + + public List headers(String name) { + return headers.values(name); + } + + public String header(String name) { + return header(name, null); + } + + public String header(String name, String defaultValue) { + String result = headers.get(name); + return result != null ? result : defaultValue; + } + + public Headers headers() { + return headers; + } + + public ResponseBody body() { + return body; + } + + public Builder newBuilder() { + return new Builder(this); + } + + /** Returns true if this response redirects to another resource. */ + public boolean isRedirect() { + switch (code) { + case HTTP_TEMP_REDIRECT: + case HTTP_MULT_CHOICE: + case HTTP_MOVED_PERM: + case HTTP_MOVED_TEMP: + case HTTP_SEE_OTHER: + return true; + default: + return false; + } + } + + /** + * Returns the raw response received from the network. Will be null if this + * response didn't use the network, such as when the response is fully cached. + * The body of the returned response should not be read. + */ + public Response networkResponse() { + return networkResponse; + } + + /** + * Returns the raw response received from the cache. Will be null if this + * response didn't use the cache. For conditional get requests the cache + * response and network response may both be non-null. The body of the + * returned response should not be read. + */ + public Response cacheResponse() { + return cacheResponse; + } + + /** + * Returns the response for the HTTP redirect or authorization challenge that + * triggered this response, or null if this response wasn't triggered by an + * automatic retry. The body of the returned response should not be read + * because it has already been consumed by the redirecting client. + */ + public Response priorResponse() { + return priorResponse; + } + + /** + * Returns the authorization challenges appropriate for this response's code. + * If the response code is 401 unauthorized, this returns the + * "WWW-Authenticate" challenges. If the response code is 407 proxy + * unauthorized, this returns the "Proxy-Authenticate" challenges. Otherwise + * this returns an empty list of challenges. + */ + public List challenges() { + String responseField; + if (code == HTTP_UNAUTHORIZED) { + responseField = "WWW-Authenticate"; + } else if (code == HTTP_PROXY_AUTH) { + responseField = "Proxy-Authenticate"; + } else { + return Collections.emptyList(); + } + return OkHeaders.parseChallenges(headers(), responseField); + } + + /** + * Returns the cache control directives for this response. This is never null, + * even if this response contains no {@code Cache-Control} header. + */ + public CacheControl cacheControl() { + CacheControl result = cacheControl; + return result != null ? result : (cacheControl = CacheControl.parse(headers)); + } + + @Override public String toString() { + return "Response{protocol=" + + protocol + + ", code=" + + code + + ", message=" + + message + + ", url=" + + request.urlString() + + '}'; + } + + public static class Builder { + private Request request; + private Protocol protocol; + private int code = -1; + private String message; + private Handshake handshake; + private Headers.Builder headers; + private ResponseBody body; + private Response networkResponse; + private Response cacheResponse; + private Response priorResponse; + + public Builder() { + headers = new Headers.Builder(); + } + + private Builder(Response response) { + this.request = response.request; + this.protocol = response.protocol; + this.code = response.code; + this.message = response.message; + this.handshake = response.handshake; + this.headers = response.headers.newBuilder(); + this.body = response.body; + this.networkResponse = response.networkResponse; + this.cacheResponse = response.cacheResponse; + this.priorResponse = response.priorResponse; + } + + public Builder request(Request request) { + this.request = request; + return this; + } + + public Builder protocol(Protocol protocol) { + this.protocol = protocol; + return this; + } + + public Builder code(int code) { + this.code = code; + return this; + } + + public Builder message(String message) { + this.message = message; + return this; + } + + public Builder handshake(Handshake handshake) { + this.handshake = handshake; + return this; + } + + /** + * Sets the header named {@code name} to {@code value}. If this request + * already has any headers with that name, they are all replaced. + */ + public Builder header(String name, String value) { + headers.set(name, value); + return this; + } + + /** + * Adds a header with {@code name} and {@code value}. Prefer this method for + * multiply-valued headers like "Set-Cookie". + */ + public Builder addHeader(String name, String value) { + headers.add(name, value); + return this; + } + + public Builder removeHeader(String name) { + headers.removeAll(name); + return this; + } + + /** Removes all headers on this builder and adds {@code headers}. */ + public Builder headers(Headers headers) { + this.headers = headers.newBuilder(); + return this; + } + + public Builder body(ResponseBody body) { + this.body = body; + return this; + } + + public Builder networkResponse(Response networkResponse) { + if (networkResponse != null) checkSupportResponse("networkResponse", networkResponse); + this.networkResponse = networkResponse; + return this; + } + + public Builder cacheResponse(Response cacheResponse) { + if (cacheResponse != null) checkSupportResponse("cacheResponse", cacheResponse); + this.cacheResponse = cacheResponse; + return this; + } + + private void checkSupportResponse(String name, Response response) { + if (response.body != null) { + throw new IllegalArgumentException(name + ".body != null"); + } else if (response.networkResponse != null) { + throw new IllegalArgumentException(name + ".networkResponse != null"); + } else if (response.cacheResponse != null) { + throw new IllegalArgumentException(name + ".cacheResponse != null"); + } else if (response.priorResponse != null) { + throw new IllegalArgumentException(name + ".priorResponse != null"); + } + } + + public Builder priorResponse(Response priorResponse) { + if (priorResponse != null) checkPriorResponse(priorResponse); + this.priorResponse = priorResponse; + return this; + } + + private void checkPriorResponse(Response response) { + if (response.body != null) { + throw new IllegalArgumentException("priorResponse.body != null"); + } + } + + public Response build() { + if (request == null) throw new IllegalStateException("request == null"); + if (protocol == null) throw new IllegalStateException("protocol == null"); + if (code < 0) throw new IllegalStateException("code < 0: " + code); + return new Response(this); + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/ResponseBody.java b/contentstack/src/main/java/com/contentstack/okhttp/ResponseBody.java new file mode 100755 index 00000000..6c610a4a --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/ResponseBody.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp; + +import com.contentstack.okhttp.internal.Util; +import com.contentstack.okio.BufferedSource; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; + +import static com.contentstack.okhttp.internal.Util.UTF_8; + +public abstract class ResponseBody implements Closeable { + /** Multiple calls to {@link #charStream()} must return the same instance. */ + private Reader reader; + + public abstract MediaType contentType(); + + /** + * Returns the number of bytes in that will returned by {@link #bytes}, or + * {@link #byteStream}, or -1 if unknown. + */ + public abstract long contentLength(); + + public final InputStream byteStream() { + return source().inputStream(); + } + + public abstract BufferedSource source(); + + public final byte[] bytes() throws IOException { + long contentLength = contentLength(); + if (contentLength > Integer.MAX_VALUE) { + throw new IOException("Cannot buffer entire body for content length: " + contentLength); + } + + BufferedSource source = source(); + byte[] bytes; + try { + bytes = source.readByteArray(); + } finally { + Util.closeQuietly(source); + } + if (contentLength != -1 && contentLength != bytes.length) { + throw new IOException("Content-Length and stream length disagree"); + } + return bytes; + } + + /** + * Returns the response as a character stream decoded with the charset + * of the Content-Type header. If that header is either absent or lacks a + * charset, this will attempt to decode the response body as UTF-8. + */ + public final Reader charStream() { + Reader r = reader; + return r != null ? r : (reader = new InputStreamReader(byteStream(), charset())); + } + + /** + * Returns the response as a string decoded with the charset of the + * Content-Type header. If that header is either absent or lacks a charset, + * this will attempt to decode the response body as UTF-8. + */ + public final String string() throws IOException { + return new String(bytes(), charset().name()); + } + + private Charset charset() { + MediaType contentType = contentType(); + return contentType != null ? contentType.charset(UTF_8) : UTF_8; + } + + @Override public void close() throws IOException { + source().close(); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/Route.java b/contentstack/src/main/java/com/contentstack/okhttp/Route.java new file mode 100755 index 00000000..a3ef8ba9 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/Route.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp; + +import com.contentstack.okhttp.internal.http.RouteSelector; + +import java.net.InetSocketAddress; +import java.net.Proxy; + +/** + * The concrete route used by a connection to reach an abstract origin server. + * When creating a connection the client has many options: + *
    + *
  • HTTP proxy: a proxy server may be explicitly + * configured for the client. Otherwise the {@linkplain java.net.ProxySelector + * proxy selector} is used. It may return multiple proxies to attempt. + *
  • IP address: whether connecting directly to an origin + * server or a proxy, opening a socket requires an IP address. The DNS + * server may return multiple IP addresses to attempt. + *
  • TLS version: which TLS version to attempt with the + * HTTPS connection. + *
+ * Each route is a specific selection of these options. + */ +public final class Route { + final Address address; + final Proxy proxy; + final InetSocketAddress inetSocketAddress; + final String tlsVersion; + + public Route(Address address, Proxy proxy, InetSocketAddress inetSocketAddress, + String tlsVersion) { + if (address == null) throw new NullPointerException("address == null"); + if (proxy == null) throw new NullPointerException("proxy == null"); + if (inetSocketAddress == null) throw new NullPointerException("inetSocketAddress == null"); + if (tlsVersion == null) throw new NullPointerException("tlsVersion == null"); + this.address = address; + this.proxy = proxy; + this.inetSocketAddress = inetSocketAddress; + this.tlsVersion = tlsVersion; + } + + public Address getAddress() { + return address; + } + + /** + * Returns the {@link Proxy} of this route. + * + * Warning: This may disagree with {@link Address#getProxy} + * when it is null. When the address's proxy is null, the proxy selector is + * used. + */ + public Proxy getProxy() { + return proxy; + } + + public InetSocketAddress getSocketAddress() { + return inetSocketAddress; + } + + public String getTlsVersion() { + return tlsVersion; + } + + boolean supportsNpn() { + return !tlsVersion.equals(RouteSelector.SSL_V3); + } + + /** + * Returns true if this route tunnels HTTPS through an HTTP proxy. See RFC 2817, Section 5.2. + */ + public boolean requiresTunnel() { + return address.sslSocketFactory != null && proxy.type() == Proxy.Type.HTTP; + } + + @Override public boolean equals(Object obj) { + if (obj instanceof Route) { + Route other = (Route) obj; + return address.equals(other.address) + && proxy.equals(other.proxy) + && inetSocketAddress.equals(other.inetSocketAddress) + && tlsVersion.equals(other.tlsVersion); + } + return false; + } + + @Override public int hashCode() { + int result = 17; + result = 31 * result + address.hashCode(); + result = 31 * result + proxy.hashCode(); + result = 31 * result + inetSocketAddress.hashCode(); + result = 31 * result + tlsVersion.hashCode(); + return result; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/BitArray.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/BitArray.java new file mode 100755 index 00000000..5cb35520 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/BitArray.java @@ -0,0 +1,177 @@ +/* + * Copyright 2014 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static java.lang.String.format; + +/** A simple bitset which supports left shifting. */ +public interface BitArray { + + void clear(); + + void set(int index); + + void toggle(int index); + + boolean get(int index); + + void shiftLeft(int count); + + /** Bit set that only supports settings bits 0 - 63. */ + public final class FixedCapacity implements BitArray { + long data = 0x0000000000000000L; + + @Override public void clear() { + data = 0x0000000000000000L; + } + + @Override public void set(int index) { + data |= (1L << checkInput(index)); + } + + @Override public void toggle(int index) { + data ^= (1L << checkInput(index)); + } + + @Override public boolean get(int index) { + return ((data >> checkInput(index)) & 1L) == 1; + } + + @Override public void shiftLeft(int count) { + data = data << checkInput(count); + } + + @Override public String toString() { + return Long.toBinaryString(data); + } + + public BitArray toVariableCapacity() { + return new VariableCapacity(this); + } + + private static int checkInput(int index) { + if (index < 0 || index > 63) { + throw new IllegalArgumentException(format("input must be between 0 and 63: %s", index)); + } + return index; + } + } + + /** Bit set that grows as needed. */ + public final class VariableCapacity implements BitArray { + + long[] data; + + // Start offset which allows for cheap shifting. Data is always kept on 64-bit bounds but we + // offset the outward facing index to support shifts without having to move the underlying bits. + private int start; // Valid values are [0..63] + + public VariableCapacity() { + data = new long[1]; + } + + private VariableCapacity(FixedCapacity small) { + data = new long[] {small.data, 0}; + } + + private void growToSize(int size) { + long[] newData = new long[size]; + if (data != null) { + System.arraycopy(data, 0, newData, 0, data.length); + } + data = newData; + } + + private int offsetOf(int index) { + index += start; + int offset = index / 64; + if (offset > data.length - 1) { + growToSize(offset + 1); + } + return offset; + } + + private int shiftOf(int index) { + return (index + start) % 64; + } + + @Override public void clear() { + Arrays.fill(data, 0); + } + + @Override public void set(int index) { + checkInput(index); + int offset = offsetOf(index); + data[offset] |= 1L << shiftOf(index); + } + + @Override public void toggle(int index) { + checkInput(index); + int offset = offsetOf(index); + data[offset] ^= 1L << shiftOf(index); + } + + @Override public boolean get(int index) { + checkInput(index); + int offset = offsetOf(index); + return (data[offset] & (1L << shiftOf(index))) != 0; + } + + @Override public void shiftLeft(int count) { + start -= checkInput(count); + if (start < 0) { + int arrayShift = (start / -64) + 1; + long[] newData = new long[data.length + arrayShift]; + System.arraycopy(data, 0, newData, arrayShift, data.length); + data = newData; + start = 64 + (start % 64); + } + } + + @Override public String toString() { + StringBuilder builder = new StringBuilder("{"); + List ints = toIntegerList(); + for (int i = 0, count = ints.size(); i < count; i++) { + if (i > 0) { + builder.append(','); + } + builder.append(ints.get(i)); + } + return builder.append('}').toString(); + } + + List toIntegerList() { + List ints = new ArrayList(); + for (int i = 0, count = data.length * 64 - start; i < count; i++) { + if (get(i)) { + ints.add(i); + } + } + return ints; + } + + private static int checkInput(int index) { + if (index < 0) { + throw new IllegalArgumentException(format("input must be a positive number: %s", index)); + } + return index; + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/DiskLruCache.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/DiskLruCache.java new file mode 100755 index 00000000..40928c8c --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/DiskLruCache.java @@ -0,0 +1,940 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contentstack.okhttp.internal; + +import com.contentstack.okio.Buffer; +import com.contentstack.okio.BufferedSink; +import com.contentstack.okio.BufferedSource; +import com.contentstack.okio.ForwardingSink; +import com.contentstack.okio.Okio; +import com.contentstack.okio.Sink; +import com.contentstack.okio.Source; +import com.contentstack.okio.Timeout; + +import java.io.Closeable; +import java.io.EOFException; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A cache that uses a bounded amount of space on a filesystem. Each cache + * entry has a string key and a fixed number of values. Each key must match + * the regex [a-z0-9_-]{1,64}. Values are byte sequences, + * accessible as streams or files. Each value must be between {@code 0} and + * {@code Integer.MAX_VALUE} bytes in length. + * + *

The cache stores its data in a directory on the filesystem. This + * directory must be exclusive to the cache; the cache may delete or overwrite + * files from its directory. It is an error for multiple processes to use the + * same cache directory at the same time. + * + *

This cache limits the number of bytes that it will store on the + * filesystem. When the number of stored bytes exceeds the limit, the cache will + * remove entries in the background until the limit is satisfied. The limit is + * not strict: the cache may temporarily exceed it while waiting for files to be + * deleted. The limit does not include filesystem overhead or the cache + * journal so space-sensitive applications should set a conservative limit. + * + *

Clients call {@link #edit} to create or update the values of an entry. An + * entry may have only one editor at one time; if a value is not available to be + * edited then {@link #edit} will return null. + *

    + *
  • When an entry is being created it is necessary to + * supply a full set of values; the empty value should be used as a + * placeholder if necessary. + *
  • When an entry is being edited, it is not necessary + * to supply data for every value; values default to their previous + * value. + *
+ * Every {@link #edit} call must be matched by a call to {@link Editor#commit} + * or {@link Editor#abort}. Committing is atomic: a read observes the full set + * of values as they were before or after the commit, but never a mix of values. + * + *

Clients call {@link #get} to read a snapshot of an entry. The read will + * observe the value at the time that {@link #get} was called. Updates and + * removals after the call do not impact ongoing reads. + * + *

This class is tolerant of some I/O errors. If files are missing from the + * filesystem, the corresponding entries will be dropped from the cache. If + * an error occurs while writing a cache value, the edit will fail silently. + * Callers should handle other problems by catching {@code IOException} and + * responding appropriately. + */ +public final class DiskLruCache implements Closeable { + static final String JOURNAL_FILE = "journal"; + static final String JOURNAL_FILE_TEMP = "journal.tmp"; + static final String JOURNAL_FILE_BACKUP = "journal.bkp"; + static final String MAGIC = "libcore.io.DiskLruCache"; + static final String VERSION_1 = "1"; + static final long ANY_SEQUENCE_NUMBER = -1; + static final Pattern LEGAL_KEY_PATTERN = Pattern.compile("[a-z0-9_-]{1,64}"); + private static final String CLEAN = "CLEAN"; + private static final String DIRTY = "DIRTY"; + private static final String REMOVE = "REMOVE"; + private static final String READ = "READ"; + + /* + * This cache uses a journal file named "journal". A typical journal file + * looks like this: + * libcore.io.DiskLruCache + * 1 + * 100 + * 2 + * + * CLEAN ***REMOVED*** + * DIRTY ***REMOVED*** + * CLEAN ***REMOVED*** ***REMOVED******REMOVED*** + * REMOVE ***REMOVED*** + * DIRTY ***REMOVED*** + * CLEAN ***REMOVED*** ***REMOVED******REMOVED*** + * READ ***REMOVED*** + * READ ***REMOVED*** + * + * The first five lines of the journal form its header. They are the + * constant string "libcore.io.DiskLruCache", the disk cache's version, + * the application's version, the value count, and a blank line. + * + * Each of the subsequent lines in the file is a record of the state of a + * cache entry. Each line contains space-separated values: a state, a key, + * and optional state-specific values. + * o DIRTY lines track that an entry is actively being created or updated. + * Every successful DIRTY action should be followed by a CLEAN or REMOVE + * action. DIRTY lines without a matching CLEAN or REMOVE indicate that + * temporary files may need to be deleted. + * o CLEAN lines track a cache entry that has been successfully published + * and may be read. A publish line is followed by the lengths of each of + * its values. + * o READ lines track accesses for LRU. + * o REMOVE lines track entries that have been deleted. + * + * The journal file is appended to as cache operations occur. The journal may + * occasionally be compacted by dropping redundant lines. A temporary file named + * "journal.tmp" will be used during compaction; that file should be deleted if + * it exists when the cache is opened. + */ + + private final File directory; + private final File journalFile; + private final File journalFileTmp; + private final File journalFileBackup; + private final int appVersion; + private long maxSize; + private final int valueCount; + private long size = 0; + private BufferedSink journalWriter; + private final LinkedHashMap lruEntries = new LinkedHashMap(0, 0.75f, true); + private int redundantOpCount; + + /** + * To differentiate between old and current snapshots, each entry is given + * a sequence number each time an edit is committed. A snapshot is stale if + * its sequence number is not equal to its entry's sequence number. + */ + private long nextSequenceNumber = 0; + + /** This cache uses a single background thread to evict entries. */ + final ThreadPoolExecutor executorService = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue(), Util.threadFactory("OkHttp DiskLruCache", true)); + private final Runnable cleanupRunnable = new Runnable() { + public void run() { + synchronized (DiskLruCache.this) { + if (journalWriter == null) { + return; // Closed. + } + try { + trimToSize(); + if (journalRebuildRequired()) { + rebuildJournal(); + redundantOpCount = 0; + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + }; + + private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { + this.directory = directory; + this.appVersion = appVersion; + this.journalFile = new File(directory, JOURNAL_FILE); + this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP); + this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP); + this.valueCount = valueCount; + this.maxSize = maxSize; + } + + /** + * Opens the cache in {@code directory}, creating a cache if none exists + * there. + * + * @param directory a writable directory + * @param valueCount the number of values per cache entry. Must be positive. + * @param maxSize the maximum number of bytes this cache should use to store + * @throws IOException if reading or writing the cache directory fails + */ + public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) + throws IOException { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + if (valueCount <= 0) { + throw new IllegalArgumentException("valueCount <= 0"); + } + + // If a bkp file exists, use it instead. + File backupFile = new File(directory, JOURNAL_FILE_BACKUP); + if (backupFile.exists()) { + File journalFile = new File(directory, JOURNAL_FILE); + // If journal file also exists just delete backup file. + if (journalFile.exists()) { + backupFile.delete(); + } else { + renameTo(backupFile, journalFile, false); + } + } + + // Prefer to pick up where we left off. + DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + if (cache.journalFile.exists()) { + try { + cache.readJournal(); + cache.processJournal(); + cache.journalWriter = Okio.buffer(Okio.appendingSink(cache.journalFile)); + return cache; + } catch (IOException journalIsCorrupt) { + Platform.get().logW("DiskLruCache " + directory + " is corrupt: " + + journalIsCorrupt.getMessage() + ", removing"); + cache.delete(); + } + } + + // Create a new empty cache. + directory.mkdirs(); + cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + cache.rebuildJournal(); + return cache; + } + + private void readJournal() throws IOException { + BufferedSource source = Okio.buffer(Okio.source(journalFile)); + try { + String magic = source.readUtf8LineStrict(); + String version = source.readUtf8LineStrict(); + String appVersionString = source.readUtf8LineStrict(); + String valueCountString = source.readUtf8LineStrict(); + String blank = source.readUtf8LineStrict(); + if (!MAGIC.equals(magic) + || !VERSION_1.equals(version) + || !Integer.toString(appVersion).equals(appVersionString) + || !Integer.toString(valueCount).equals(valueCountString) + || !"".equals(blank)) { + throw new IOException("unexpected journal header: [" + magic + ", " + version + ", " + + valueCountString + ", " + blank + "]"); + } + + int lineCount = 0; + while (true) { + try { + readJournalLine(source.readUtf8LineStrict()); + lineCount++; + } catch (EOFException endOfJournal) { + break; + } + } + redundantOpCount = lineCount - lruEntries.size(); + } finally { + Util.closeQuietly(source); + } + } + + private void readJournalLine(String line) throws IOException { + int firstSpace = line.indexOf(' '); + if (firstSpace == -1) { + throw new IOException("unexpected journal line: " + line); + } + + int keyBegin = firstSpace + 1; + int secondSpace = line.indexOf(' ', keyBegin); + final String key; + if (secondSpace == -1) { + key = line.substring(keyBegin); + if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) { + lruEntries.remove(key); + return; + } + } else { + key = line.substring(keyBegin, secondSpace); + } + + Entry entry = lruEntries.get(key); + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } + + if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) { + String[] parts = line.substring(secondSpace + 1).split(" "); + entry.readable = true; + entry.currentEditor = null; + entry.setLengths(parts); + } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) { + entry.currentEditor = new Editor(entry); + } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) { + // This work was already done by calling lruEntries.get(). + } else { + throw new IOException("unexpected journal line: " + line); + } + } + + /** + * Computes the initial size and collects garbage as a part of opening the + * cache. Dirty entries are assumed to be inconsistent and will be deleted. + */ + private void processJournal() throws IOException { + deleteIfExists(journalFileTmp); + for (Iterator i = lruEntries.values().iterator(); i.hasNext(); ) { + Entry entry = i.next(); + if (entry.currentEditor == null) { + for (int t = 0; t < valueCount; t++) { + size += entry.lengths[t]; + } + } else { + entry.currentEditor = null; + for (int t = 0; t < valueCount; t++) { + deleteIfExists(entry.cleanFiles[t]); + deleteIfExists(entry.dirtyFiles[t]); + } + i.remove(); + } + } + } + + /** + * Creates a new journal that omits redundant information. This replaces the + * current journal if it exists. + */ + private synchronized void rebuildJournal() throws IOException { + if (journalWriter != null) { + journalWriter.close(); + } + + BufferedSink writer = Okio.buffer(Okio.sink(journalFileTmp)); + try { + writer.writeUtf8(MAGIC).writeByte('\n'); + writer.writeUtf8(VERSION_1).writeByte('\n'); + writer.writeUtf8(Integer.toString(appVersion)).writeByte('\n'); + writer.writeUtf8(Integer.toString(valueCount)).writeByte('\n'); + writer.writeByte('\n'); + + for (Entry entry : lruEntries.values()) { + if (entry.currentEditor != null) { + writer.writeUtf8(DIRTY).writeByte(' '); + writer.writeUtf8(entry.key); + writer.writeByte('\n'); + } else { + writer.writeUtf8(CLEAN).writeByte(' '); + writer.writeUtf8(entry.key); + writer.writeUtf8(entry.getLengths()); + writer.writeByte('\n'); + } + } + } finally { + writer.close(); + } + + if (journalFile.exists()) { + renameTo(journalFile, journalFileBackup, true); + } + renameTo(journalFileTmp, journalFile, false); + journalFileBackup.delete(); + + journalWriter = Okio.buffer(Okio.appendingSink(journalFile)); + } + + private static void deleteIfExists(File file) throws IOException { + // If delete() fails, make sure it's because the file didn't exist! + if (!file.delete() && file.exists()) { + throw new IOException("failed to delete " + file); + } + } + + private static void renameTo(File from, File to, boolean deleteDestination) throws IOException { + if (deleteDestination) { + deleteIfExists(to); + } + if (!from.renameTo(to)) { + throw new IOException(); + } + } + + /** + * Returns a snapshot of the entry named {@code key}, or null if it doesn't + * exist is not currently readable. If a value is returned, it is moved to + * the head of the LRU queue. + */ + public synchronized Snapshot get(String key) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (entry == null) { + return null; + } + + if (!entry.readable) { + return null; + } + + // Open all streams eagerly to guarantee that we see a single published + // snapshot. If we opened streams lazily then the streams could come + // from different edits. + Source[] sources = new Source[valueCount]; + try { + for (int i = 0; i < valueCount; i++) { + sources[i] = Okio.source(entry.cleanFiles[i]); + } + } catch (FileNotFoundException e) { + // A file must have been deleted manually! + for (int i = 0; i < valueCount; i++) { + if (sources[i] != null) { + Util.closeQuietly(sources[i]); + } else { + break; + } + } + return null; + } + + redundantOpCount++; + journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n'); + if (journalRebuildRequired()) { + executorService.execute(cleanupRunnable); + } + + return new Snapshot(key, entry.sequenceNumber, sources, entry.lengths); + } + + /** + * Returns an editor for the entry named {@code key}, or null if another + * edit is in progress. + */ + public Editor edit(String key) throws IOException { + return edit(key, ANY_SEQUENCE_NUMBER); + } + + private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null + || entry.sequenceNumber != expectedSequenceNumber)) { + return null; // Snapshot is stale. + } + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } else if (entry.currentEditor != null) { + return null; // Another edit is in progress. + } + + Editor editor = new Editor(entry); + entry.currentEditor = editor; + + // Flush the journal before creating files to prevent file leaks. + journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n'); + journalWriter.flush(); + return editor; + } + + /** Returns the directory where this cache stores its data. */ + public File getDirectory() { + return directory; + } + + /** + * Returns the maximum number of bytes that this cache should use to store + * its data. + */ + public synchronized long getMaxSize() { + return maxSize; + } + + /** + * Changes the maximum number of bytes the cache can store and queues a job + * to trim the existing store, if necessary. + */ + public synchronized void setMaxSize(long maxSize) { + this.maxSize = maxSize; + executorService.execute(cleanupRunnable); + } + + /** + * Returns the number of bytes currently being used to store the values in + * this cache. This may be greater than the max size if a background + * deletion is pending. + */ + public synchronized long size() { + return size; + } + + private synchronized void completeEdit(Editor editor, boolean success) throws IOException { + Entry entry = editor.entry; + if (entry.currentEditor != editor) { + throw new IllegalStateException(); + } + + // If this edit is creating the entry for the first time, every index must have a value. + if (success && !entry.readable) { + for (int i = 0; i < valueCount; i++) { + if (!editor.written[i]) { + editor.abort(); + throw new IllegalStateException("Newly created entry didn't create value for index " + i); + } + if (!entry.dirtyFiles[i].exists()) { + editor.abort(); + return; + } + } + } + + for (int i = 0; i < valueCount; i++) { + File dirty = entry.dirtyFiles[i]; + if (success) { + if (dirty.exists()) { + File clean = entry.cleanFiles[i]; + dirty.renameTo(clean); + long oldLength = entry.lengths[i]; + long newLength = clean.length(); + entry.lengths[i] = newLength; + size = size - oldLength + newLength; + } + } else { + deleteIfExists(dirty); + } + } + + redundantOpCount++; + entry.currentEditor = null; + if (entry.readable | success) { + entry.readable = true; + journalWriter.writeUtf8(CLEAN).writeByte(' '); + journalWriter.writeUtf8(entry.key); + journalWriter.writeUtf8(entry.getLengths()); + journalWriter.writeByte('\n'); + if (success) { + entry.sequenceNumber = nextSequenceNumber++; + } + } else { + lruEntries.remove(entry.key); + journalWriter.writeUtf8(REMOVE).writeByte(' '); + journalWriter.writeUtf8(entry.key); + journalWriter.writeByte('\n'); + } + journalWriter.flush(); + + if (size > maxSize || journalRebuildRequired()) { + executorService.execute(cleanupRunnable); + } + } + + /** + * We only rebuild the journal when it will halve the size of the journal + * and eliminate at least 2000 ops. + */ + private boolean journalRebuildRequired() { + final int redundantOpCompactThreshold = 2000; + return redundantOpCount >= redundantOpCompactThreshold + && redundantOpCount >= lruEntries.size(); + } + + /** + * Drops the entry for {@code key} if it exists and can be removed. Entries + * actively being edited cannot be removed. + * + * @return true if an entry was removed. + */ + public synchronized boolean remove(String key) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (entry == null || entry.currentEditor != null) { + return false; + } + + for (int i = 0; i < valueCount; i++) { + File file = entry.cleanFiles[i]; + deleteIfExists(file); + size -= entry.lengths[i]; + entry.lengths[i] = 0; + } + + redundantOpCount++; + journalWriter.writeUtf8(REMOVE).writeByte(' ').writeUtf8(key).writeByte('\n'); + lruEntries.remove(key); + + if (journalRebuildRequired()) { + executorService.execute(cleanupRunnable); + } + + return true; + } + + /** Returns true if this cache has been closed. */ + public boolean isClosed() { + return journalWriter == null; + } + + private void checkNotClosed() { + if (journalWriter == null) { + throw new IllegalStateException("cache is closed"); + } + } + + /** Force buffered operations to the filesystem. */ + public synchronized void flush() throws IOException { + checkNotClosed(); + trimToSize(); + journalWriter.flush(); + } + + /** Closes this cache. Stored values will remain on the filesystem. */ + public synchronized void close() throws IOException { + if (journalWriter == null) { + return; // Already closed. + } + // Copying for safe iteration. + for (Object next : lruEntries.values().toArray()) { + Entry entry = (Entry) next; + if (entry.currentEditor != null) { + entry.currentEditor.abort(); + } + } + trimToSize(); + journalWriter.close(); + journalWriter = null; + } + + private void trimToSize() throws IOException { + while (size > maxSize) { + Map.Entry toEvict = lruEntries.entrySet().iterator().next(); + remove(toEvict.getKey()); + } + } + + /** + * Closes the cache and deletes all of its stored values. This will delete + * all files in the cache directory including files that weren't created by + * the cache. + */ + public void delete() throws IOException { + close(); + Util.deleteContents(directory); + } + + private void validateKey(String key) { + Matcher matcher = LEGAL_KEY_PATTERN.matcher(key); + if (!matcher.matches()) { + throw new IllegalArgumentException("keys must match regex [a-z0-9_-]{1,64}: \"" + key + "\""); + } + } + + private static String sourceToString(Source in) throws IOException { + try { + return Okio.buffer(in).readUtf8(); + } finally { + Util.closeQuietly(in); + } + } + + /** A snapshot of the values for an entry. */ + public final class Snapshot implements Closeable { + private final String key; + private final long sequenceNumber; + private final Source[] sources; + private final long[] lengths; + + private Snapshot(String key, long sequenceNumber, Source[] sources, long[] lengths) { + this.key = key; + this.sequenceNumber = sequenceNumber; + this.sources = sources; + this.lengths = lengths; + } + + /** + * Returns an editor for this snapshot's entry, or null if either the + * entry has changed since this snapshot was created or if another edit + * is in progress. + */ + public Editor edit() throws IOException { + return DiskLruCache.this.edit(key, sequenceNumber); + } + + /** Returns the unbuffered stream with the value for {@code index}. */ + public Source getSource(int index) { + return sources[index]; + } + + /** Returns the string value for {@code index}. */ + public String getString(int index) throws IOException { + return sourceToString(getSource(index)); + } + + /** Returns the byte length of the value for {@code index}. */ + public long getLength(int index) { + return lengths[index]; + } + + public void close() { + for (Source in : sources) { + Util.closeQuietly(in); + } + } + } + + private static final Sink NULL_SINK = new Sink() { + @Override public void write(Buffer source, long byteCount) throws IOException { + // Eat all writes silently. Nom nom. + } + + @Override public void flush() throws IOException { + } + + @Override public Timeout timeout() { + return Timeout.NONE; + } + + @Override public void close() throws IOException { + } + }; + + /** Edits the values for an entry. */ + public final class Editor { + private final Entry entry; + private final boolean[] written; + private boolean hasErrors; + private boolean committed; + + private Editor(Entry entry) { + this.entry = entry; + this.written = (entry.readable) ? null : new boolean[valueCount]; + } + + /** + * Returns an unbuffered input stream to read the last committed value, + * or null if no value has been committed. + */ + public Source newSource(int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + if (!entry.readable) { + return null; + } + try { + return Okio.source(entry.cleanFiles[index]); + } catch (FileNotFoundException e) { + return null; + } + } + } + + /** + * Returns the last committed value as a string, or null if no value + * has been committed. + */ + public String getString(int index) throws IOException { + Source source = newSource(index); + return source != null ? sourceToString(source) : null; + } + + /** + * Returns a new unbuffered output stream to write the value at + * {@code index}. If the underlying output stream encounters errors + * when writing to the filesystem, this edit will be aborted when + * {@link #commit} is called. The returned output stream does not throw + * IOExceptions. + */ + public Sink newSink(int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + if (!entry.readable) { + written[index] = true; + } + File dirtyFile = entry.dirtyFiles[index]; + Sink sink; + try { + sink = Okio.sink(dirtyFile); + } catch (FileNotFoundException e) { + // Attempt to recreate the cache directory. + directory.mkdirs(); + try { + sink = Okio.sink(dirtyFile); + } catch (FileNotFoundException e2) { + // We are unable to recover. Silently eat the writes. + return NULL_SINK; + } + } + return new FaultHidingSink(sink); + } + } + + /** Sets the value at {@code index} to {@code value}. */ + public void set(int index, String value) throws IOException { + BufferedSink writer = Okio.buffer(newSink(index)); + writer.writeUtf8(value); + writer.close(); + } + + /** + * Commits this edit so it is visible to readers. This releases the + * edit lock so another edit may be started on the same key. + */ + public void commit() throws IOException { + if (hasErrors) { + completeEdit(this, false); + remove(entry.key); // The previous entry is stale. + } else { + completeEdit(this, true); + } + committed = true; + } + + /** + * Aborts this edit. This releases the edit lock so another edit may be + * started on the same key. + */ + public void abort() throws IOException { + completeEdit(this, false); + } + + public void abortUnlessCommitted() { + if (!committed) { + try { + abort(); + } catch (IOException ignored) { + } + } + } + + private class FaultHidingSink extends ForwardingSink { + public FaultHidingSink(Sink delegate) { + super(delegate); + } + + @Override public void write(Buffer source, long byteCount) throws IOException { + try { + super.write(source, byteCount); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override public void flush() throws IOException { + try { + super.flush(); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override public void close() throws IOException { + try { + super.close(); + } catch (IOException e) { + hasErrors = true; + } + } + } + } + + private final class Entry { + private final String key; + + /** Lengths of this entry's files. */ + private final long[] lengths; + private final File[] cleanFiles; + private final File[] dirtyFiles; + + /** True if this entry has ever been published. */ + private boolean readable; + + /** The ongoing edit or null if this entry is not being edited. */ + private Editor currentEditor; + + /** The sequence number of the most recently committed edit to this entry. */ + private long sequenceNumber; + + private Entry(String key) { + this.key = key; + + lengths = new long[valueCount]; + cleanFiles = new File[valueCount]; + dirtyFiles = new File[valueCount]; + + // The names are repetitive so re-use the same builder to avoid allocations. + StringBuilder fileBuilder = new StringBuilder(key).append('.'); + int truncateTo = fileBuilder.length(); + for (int i = 0; i < valueCount; i++) { + fileBuilder.append(i); + cleanFiles[i] = new File(directory, fileBuilder.toString()); + fileBuilder.append(".tmp"); + dirtyFiles[i] = new File(directory, fileBuilder.toString()); + fileBuilder.setLength(truncateTo); + } + } + + public String getLengths() throws IOException { + StringBuilder result = new StringBuilder(); + for (long size : lengths) { + result.append(' ').append(size); + } + return result.toString(); + } + + /** Set lengths using decimal numbers like "10123". */ + private void setLengths(String[] strings) throws IOException { + if (strings.length != valueCount) { + throw invalidLengths(strings); + } + + try { + for (int i = 0; i < strings.length; i++) { + lengths[i] = Long.parseLong(strings[i]); + } + } catch (NumberFormatException e) { + throw invalidLengths(strings); + } + } + + private IOException invalidLengths(String[] strings) throws IOException { + throw new IOException("unexpected journal line: " + Arrays.toString(strings)); + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/Internal.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/Internal.java new file mode 100755 index 00000000..d5ecbf18 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/Internal.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal; + +import com.contentstack.okhttp.Connection; +import com.contentstack.okhttp.ConnectionPool; +import com.contentstack.okhttp.Headers; +import com.contentstack.okhttp.OkHttpClient; +import com.contentstack.okhttp.Protocol; +import com.contentstack.okhttp.Request; +import com.contentstack.okhttp.internal.http.HttpEngine; +import com.contentstack.okhttp.internal.http.Transport; + +import java.io.IOException; + +/** + * Escalate internal APIs in {@code com.squareup.okhttp} so they can be used + * from OkHttp's implementation packages. The only implementation of this + * interface is in {@link OkHttpClient}. + */ +public abstract class Internal { + public static Internal instance; + + public abstract Transport newTransport(Connection connection, HttpEngine httpEngine) + throws IOException; + + public abstract boolean clearOwner(Connection connection); + + public abstract void closeIfOwnedBy(Connection connection, Object owner) throws IOException; + + public abstract int recycleCount(Connection connection); + + public abstract void setProtocol(Connection connection, Protocol protocol); + + public abstract void setOwner(Connection connection, HttpEngine httpEngine); + + public abstract boolean isReadable(Connection pooled); + + public abstract void addLine(Headers.Builder builder, String line); + + public abstract void setCache(OkHttpClient client, InternalCache internalCache); + + public abstract InternalCache internalCache(OkHttpClient client); + + public abstract void recycle(ConnectionPool pool, Connection connection); + + public abstract RouteDatabase routeDatabase(OkHttpClient client); + + public abstract void connectAndSetOwner(OkHttpClient client, Connection connection, + HttpEngine owner, Request request) throws IOException; +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/InternalCache.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/InternalCache.java new file mode 100755 index 00000000..6ab9ac18 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/InternalCache.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal; + +import com.contentstack.okhttp.Cache; +import com.contentstack.okhttp.Request; +import com.contentstack.okhttp.Response; +import com.contentstack.okhttp.internal.http.CacheRequest; +import com.contentstack.okhttp.internal.http.CacheStrategy; + +import java.io.IOException; + +/** + * OkHttp's internal cache interface. Applications shouldn't implement this: + * instead use {@link Cache}. + */ +public interface InternalCache { + Response get(Request request) throws IOException; + + CacheRequest put(Response response) throws IOException; + + /** + * Remove any cache entries for the supplied {@code request}. This is invoked + * when the client invalidates the cache, such as when making POST requests. + */ + void remove(Request request) throws IOException; + + /** + * Handles a conditional request hit by updating the stored cache response + * with the headers from {@code network}. The cached response body is not + * updated. If the stored response has changed since {@code cached} was + * returned, this does nothing. + */ + void update(Response cached, Response network) throws IOException; + + /** Track an conditional GET that was satisfied by this cache. */ + void trackConditionalCacheHit(); + + /** Track an HTTP response being satisfied with {@code cacheStrategy}. */ + void trackResponse(CacheStrategy cacheStrategy); +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/NamedRunnable.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/NamedRunnable.java new file mode 100755 index 00000000..718a8401 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/NamedRunnable.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contentstack.okhttp.internal; + +/** + * Runnable implementation which always sets its thread name. + */ +public abstract class NamedRunnable implements Runnable { + private final String name; + + public NamedRunnable(String format, Object... args) { + this.name = String.format(format, args); + } + + @Override public final void run() { + String oldName = Thread.currentThread().getName(); + Thread.currentThread().setName(name); + try { + execute(); + } finally { + Thread.currentThread().setName(oldName); + } + } + + protected abstract void execute(); +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/Platform.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/Platform.java new file mode 100755 index 00000000..d9a268c0 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/Platform.java @@ -0,0 +1,416 @@ +/* + * Copyright (C) 2012 Square, Inc. + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal; + +import com.contentstack.okhttp.Protocol; +import com.contentstack.okio.Buffer; + +import java.io.IOException; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.net.ssl.SSLSocket; + +/** + * Access to Platform-specific features necessary for SPDY and advanced TLS. + * + *

ALPN and NPN

+ * This class uses TLS extensions ALPN and NPN to negotiate the upgrade from + * HTTP/1.1 (the default protocol to use with TLS on port 443) to either SPDY + * or HTTP/2. + * + *

NPN (Next Protocol Negotiation) was developed for SPDY. It is widely + * available and we support it on both Android (4.1+) and OpenJDK 7 (via the + * Jetty Alpn-boot library). NPN is not yet available on OpenJDK 8. + * + *

ALPN (Application Layer Protocol Negotiation) is the successor to NPN. It + * has some technical advantages over NPN. ALPN first arrived in Android 4.4, + * but that release suffers a concurrency bug + * so we don't use it. ALPN is supported on OpenJDK 7 and 8 (via the Jetty + * ALPN-boot library). + * + *

On platforms that support both extensions, OkHttp will use both, + * preferring ALPN's result. Future versions of OkHttp will drop support for + * NPN. + */ +public class Platform { + private static final Platform PLATFORM = findPlatform(); + + public static Platform get() { + return PLATFORM; + } + + /** Prefix used on custom headers. */ + public String getPrefix() { + return "OkHttp"; + } + + public void logW(String warning) { + System.out.println(warning); + } + + public void tagSocket(Socket socket) throws SocketException { + } + + public void untagSocket(Socket socket) throws SocketException { + } + + public URI toUriLenient(URL url) throws URISyntaxException { + return url.toURI(); // this isn't as good as the built-in toUriLenient + } + + /** + * Configure the TLS connection to use {@code tlsVersion}. We also bundle + * certain extensions with certain versions. In particular, we enable Server + * Name Indication (SNI) and Next Protocol Negotiation (NPN) with TLSv1 on + * platforms that support them. + */ + public void configureTls(SSLSocket socket, String uriHost, String tlsVersion) { + // We don't call setEnabledProtocols("TLSv1") on the assumption that that's + if (tlsVersion.equals("SSLv3")) { + socket.setEnabledProtocols(new String[] {"SSLv3"}); + } + } + + /** Returns the negotiated protocol, or null if no protocol was negotiated. */ + public String getSelectedProtocol(SSLSocket socket) { + return null; + } + + /** + * Sets client-supported protocols on a socket to send to a server. The + * protocols are only sent if the socket implementation supports ALPN or NPN. + */ + public void setProtocols(SSLSocket socket, List protocols) { + } + + public void connectSocket(Socket socket, InetSocketAddress address, + int connectTimeout) throws IOException { + socket.connect(address, connectTimeout); + } + + /** Attempt to match the host runtime to a capable Platform implementation. */ + private static Platform findPlatform() { + // Attempt to find Android 2.3+ APIs. + Class openSslSocketClass; + Method setUseSessionTickets; + Method setHostname; + try { + try { + openSslSocketClass = Class.forName("com.android.org.conscrypt.OpenSSLSocketImpl"); + } catch (ClassNotFoundException ignored) { + // Older platform before being unbundled. + openSslSocketClass = Class.forName( + "org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl"); + } + + setUseSessionTickets = openSslSocketClass.getMethod("setUseSessionTickets", boolean.class); + setHostname = openSslSocketClass.getMethod("setHostname", String.class); + + // Attempt to find Android 4.0+ APIs. + Method trafficStatsTagSocket = null; + Method trafficStatsUntagSocket = null; + try { + Class trafficStats = Class.forName("android.net.TrafficStats"); + trafficStatsTagSocket = trafficStats.getMethod("tagSocket", Socket.class); + trafficStatsUntagSocket = trafficStats.getMethod("untagSocket", Socket.class); + } catch (ClassNotFoundException ignored) { + } catch (NoSuchMethodException ignored) { + } + + // Attempt to find Android 4.1+ APIs. + Method setNpnProtocols = null; + Method getNpnSelectedProtocol = null; + try { + setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class); + getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol"); + } catch (NoSuchMethodException ignored) { + } + + return new Android(openSslSocketClass, setUseSessionTickets, setHostname, + trafficStatsTagSocket, trafficStatsUntagSocket, setNpnProtocols, + getNpnSelectedProtocol); + } catch (ClassNotFoundException ignored) { + // This isn't an Android runtime. + } catch (NoSuchMethodException ignored) { + // This isn't Android 2.3 or better. + } + + try { // to find the Jetty's ALPN or NPN extension for OpenJDK. + String negoClassName = "org.eclipse.jetty.alpn.ALPN"; + Class negoClass; + try { + negoClass = Class.forName(negoClassName); + } catch (ClassNotFoundException ignored) { // ALPN isn't on the classpath. + negoClassName = "org.eclipse.jetty.npn.NextProtoNego"; + negoClass = Class.forName(negoClassName); + } + Class providerClass = Class.forName(negoClassName + "$Provider"); + Class clientProviderClass = Class.forName(negoClassName + "$ClientProvider"); + Class serverProviderClass = Class.forName(negoClassName + "$ServerProvider"); + Method putMethod = negoClass.getMethod("put", SSLSocket.class, providerClass); + Method getMethod = negoClass.getMethod("get", SSLSocket.class); + return new JdkWithJettyBootPlatform( + putMethod, getMethod, clientProviderClass, serverProviderClass); + } catch (ClassNotFoundException ignored) { // NPN isn't on the classpath. + } catch (NoSuchMethodException ignored) { // The ALPN or NPN version isn't what we expect. + } + + return new Platform(); + } + + /** + * Android 2.3 or better. Version 2.3 supports TLS session tickets and server + * name indication (SNI). Versions 4.1 supports NPN. + */ + private static class Android extends Platform { + // Non-null. + protected final Class openSslSocketClass; + private final Method setUseSessionTickets; + private final Method setHostname; + + // Non-null on Android 4.0+. + private final Method trafficStatsTagSocket; + private final Method trafficStatsUntagSocket; + + // Non-null on Android 4.1+. + private final Method setNpnProtocols; + private final Method getNpnSelectedProtocol; + + private Android(Class openSslSocketClass, Method setUseSessionTickets, Method setHostname, + Method trafficStatsTagSocket, Method trafficStatsUntagSocket, Method setNpnProtocols, + Method getNpnSelectedProtocol) { + this.openSslSocketClass = openSslSocketClass; + this.setUseSessionTickets = setUseSessionTickets; + this.setHostname = setHostname; + this.trafficStatsTagSocket = trafficStatsTagSocket; + this.trafficStatsUntagSocket = trafficStatsUntagSocket; + this.setNpnProtocols = setNpnProtocols; + this.getNpnSelectedProtocol = getNpnSelectedProtocol; + } + + @Override public void connectSocket(Socket socket, InetSocketAddress address, + int connectTimeout) throws IOException { + try { + socket.connect(address, connectTimeout); + } catch (SecurityException se) { + // Before android 4.3, socket.connect could throw a SecurityException + // if opening a socket resulted in an EACCES error. + IOException ioException = new IOException("Exception in connect"); + ioException.initCause(se); + throw ioException; + } + } + + @Override public void configureTls(SSLSocket socket, String uriHost, String tlsVersion) { + super.configureTls(socket, uriHost, tlsVersion); + + if (tlsVersion.equals("TLSv1") && openSslSocketClass.isInstance(socket)) { + try { + setUseSessionTickets.invoke(socket, true); + setHostname.invoke(socket, uriHost); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } + } + } + + @Override public void setProtocols(SSLSocket socket, List protocols) { + if (setNpnProtocols == null) return; + if (!openSslSocketClass.isInstance(socket)) return; + try { + Object[] parameters = { concatLengthPrefixed(protocols) }; + setNpnProtocols.invoke(socket, parameters); + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @Override public String getSelectedProtocol(SSLSocket socket) { + if (getNpnSelectedProtocol == null) return null; + if (!openSslSocketClass.isInstance(socket)) return null; + try { + byte[] npnResult = (byte[]) getNpnSelectedProtocol.invoke(socket); + if (npnResult == null) return null; + return new String(npnResult, Util.UTF_8); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } + } + + @Override public void tagSocket(Socket socket) throws SocketException { + if (trafficStatsTagSocket == null) return; + + try { + trafficStatsTagSocket.invoke(null, socket); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @Override public void untagSocket(Socket socket) throws SocketException { + if (trafficStatsUntagSocket == null) return; + + try { + trafficStatsUntagSocket.invoke(null, socket); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + } + } + + /** + * OpenJDK 7+ with {@code org.mortbay.jetty.npn/npn-boot} or + * {@code org.mortbay.jetty.alpn/alpn-boot} in the boot class path. + */ + private static class JdkWithJettyBootPlatform extends Platform { + private final Method getMethod; + private final Method putMethod; + private final Class clientProviderClass; + private final Class serverProviderClass; + + public JdkWithJettyBootPlatform(Method putMethod, Method getMethod, + Class clientProviderClass, Class serverProviderClass) { + this.putMethod = putMethod; + this.getMethod = getMethod; + this.clientProviderClass = clientProviderClass; + this.serverProviderClass = serverProviderClass; + } + + @Override public void setProtocols(SSLSocket socket, List protocols) { + try { + List names = new ArrayList(protocols.size()); + for (int i = 0, size = protocols.size(); i < size; i++) { + Protocol protocol = protocols.get(i); + if (protocol == Protocol.HTTP_1_0) continue; // No HTTP/1.0 for NPN or ALPN. + names.add(protocol.toString()); + } + Object provider = Proxy.newProxyInstance(Platform.class.getClassLoader(), + new Class[] { clientProviderClass, serverProviderClass }, new JettyNegoProvider(names)); + putMethod.invoke(null, socket, provider); + } catch (InvocationTargetException e) { + throw new AssertionError(e); + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } + } + + @Override public String getSelectedProtocol(SSLSocket socket) { + try { + JettyNegoProvider provider = + (JettyNegoProvider) Proxy.getInvocationHandler(getMethod.invoke(null, socket)); + if (!provider.unsupported && provider.selected == null) { + Logger logger = Logger.getLogger("com.squareup.okhttp.OkHttpClient"); + logger.log(Level.INFO, "NPN/ALPN callback dropped: SPDY and HTTP/2 are disabled. " + + "Is npn-boot or alpn-boot on the boot class path?"); + return null; + } + return provider.unsupported ? null : provider.selected; + } catch (InvocationTargetException e) { + throw new AssertionError(); + } catch (IllegalAccessException e) { + throw new AssertionError(); + } + } + } + + /** + * Handle the methods of NPN or ALPN's ClientProvider and ServerProvider + * without a compile-time dependency on those interfaces. + */ + private static class JettyNegoProvider implements InvocationHandler { + /** This peer's supported protocols. */ + private final List protocols; + /** Set when remote peer notifies NPN or ALPN is unsupported. */ + private boolean unsupported; + /** The protocol the client (NPN) or server (ALPN) selected. */ + private String selected; + + public JettyNegoProvider(List protocols) { + this.protocols = protocols; + } + + @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String methodName = method.getName(); + Class returnType = method.getReturnType(); + if (args == null) { + args = Util.EMPTY_STRING_ARRAY; + } + if (methodName.equals("supports") && boolean.class == returnType) { + return true; // NPN or ALPN is supported. + } else if (methodName.equals("unsupported") && void.class == returnType) { + this.unsupported = true; // Peer doesn't support NPN or ALPN. + return null; + } else if (methodName.equals("protocols") && args.length == 0) { + return protocols; // Server (NPN) or Client (ALPN) advertises these protocols. + } else if ((methodName.equals("selectProtocol") || methodName.equals("select")) + && String.class == returnType && args.length == 1 && args[0] instanceof List) { + List peerProtocols = (List) args[0]; + // Pick the first known protocol the peer advertises. + for (int i = 0, size = peerProtocols.size(); i < size; i++) { + if (protocols.contains(peerProtocols.get(i))) { + return selected = peerProtocols.get(i); + } + } + return selected = protocols.get(0); // On no intersection, try peer's first protocol. + } else if ((methodName.equals("protocolSelected") || methodName.equals("selected")) + && args.length == 1) { + this.selected = (String) args[0]; // Client (NPN) or Server (ALPN) selected this protocol. + return null; + } else { + return method.invoke(this, args); + } + } + } + + /** + * Returns the concatenation of 8-bit, length prefixed protocol names. + * http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04#page-4 + */ + static byte[] concatLengthPrefixed(List protocols) { + Buffer result = new Buffer(); + for (int i = 0, size = protocols.size(); i < size; i++) { + Protocol protocol = protocols.get(i); + if (protocol == Protocol.HTTP_1_0) continue; // No HTTP/1.0 for NPN. + result.writeByte(protocol.toString().length()); + result.writeUtf8(protocol.toString()); + } + return result.readByteArray(); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/RouteDatabase.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/RouteDatabase.java new file mode 100755 index 00000000..c298daac --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/RouteDatabase.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal; + +import com.contentstack.okhttp.Route; + +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * A blacklist of failed routes to avoid when creating a new connection to a + * target address. This is used so that OkHttp can learn from its mistakes: if + * there was a failure attempting to connect to a specific IP address, proxy + * server or TLS mode, that failure is remembered and alternate routes are + * preferred. + */ +public final class RouteDatabase { + private final Set failedRoutes = new LinkedHashSet(); + + /** Records a failure connecting to {@code failedRoute}. */ + public synchronized void failed(Route failedRoute) { + failedRoutes.add(failedRoute); + } + + /** Records success connecting to {@code failedRoute}. */ + public synchronized void connected(Route route) { + failedRoutes.remove(route); + } + + /** Returns true if {@code route} has failed recently and should be avoided. */ + public synchronized boolean shouldPostpone(Route route) { + return failedRoutes.contains(route); + } + + public synchronized int failedRoutesCount() { + return failedRoutes.size(); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/Util.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/Util.java new file mode 100755 index 00000000..72c1237d --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/Util.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contentstack.okhttp.internal; + +import com.contentstack.okhttp.internal.http.RetryableSink; +import com.contentstack.okhttp.internal.spdy.Header; +import com.contentstack.okio.Buffer; +import com.contentstack.okio.ByteString; +import com.contentstack.okio.Source; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URI; +import java.net.URL; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ThreadFactory; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +/** Junk drawer of utility methods. */ +public final class Util { + public static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + public static final String[] EMPTY_STRING_ARRAY = new String[0]; + + /** A cheap and type-safe constant for the US-ASCII Charset. */ + public static final Charset US_ASCII = Charset.forName("US-ASCII"); + + /** A cheap and type-safe constant for the UTF-8 Charset. */ + public static final Charset UTF_8 = Charset.forName("UTF-8"); + + private Util() { + } + + public static int getEffectivePort(URI uri) { + return getEffectivePort(uri.getScheme(), uri.getPort()); + } + + public static int getEffectivePort(URL url) { + return getEffectivePort(url.getProtocol(), url.getPort()); + } + + private static int getEffectivePort(String scheme, int specifiedPort) { + return specifiedPort != -1 ? specifiedPort : getDefaultPort(scheme); + } + + public static int getDefaultPort(String protocol) { + if ("http".equals(protocol)) return 80; + if ("https".equals(protocol)) return 443; + return -1; + } + + public static void checkOffsetAndCount(long arrayLength, long offset, long count) { + if ((offset | count) < 0 || offset > arrayLength || arrayLength - offset < count) { + throw new ArrayIndexOutOfBoundsException(); + } + } + + /** Returns true if two possibly-null objects are equal. */ + public static boolean equal(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + /** + * Closes {@code closeable}, ignoring any checked exceptions. Does nothing + * if {@code closeable} is null. + */ + public static void closeQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } + + /** + * Closes {@code socket}, ignoring any checked exceptions. Does nothing if + * {@code socket} is null. + */ + public static void closeQuietly(Socket socket) { + if (socket != null) { + try { + socket.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } + + /** + * Closes {@code serverSocket}, ignoring any checked exceptions. Does nothing if + * {@code serverSocket} is null. + */ + public static void closeQuietly(ServerSocket serverSocket) { + if (serverSocket != null) { + try { + serverSocket.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } + + /** + * Closes {@code a} and {@code b}. If either close fails, this completes + * the other close and rethrows the first encountered exception. + */ + public static void closeAll(Closeable a, Closeable b) throws IOException { + Throwable thrown = null; + try { + a.close(); + } catch (Throwable e) { + thrown = e; + } + try { + b.close(); + } catch (Throwable e) { + if (thrown == null) thrown = e; + } + if (thrown == null) return; + if (thrown instanceof IOException) throw (IOException) thrown; + if (thrown instanceof RuntimeException) throw (RuntimeException) thrown; + if (thrown instanceof Error) throw (Error) thrown; + throw new AssertionError(thrown); + } + + /** + * Deletes the contents of {@code dir}. Throws an IOException if any file + * could not be deleted, or if {@code dir} is not a readable directory. + */ + public static void deleteContents(File dir) throws IOException { + File[] files = dir.listFiles(); + if (files == null) { + throw new IOException("not a readable directory: " + dir); + } + for (File file : files) { + if (file.isDirectory()) { + deleteContents(file); + } + if (!file.delete()) { + throw new IOException("failed to delete file: " + file); + } + } + } + + /** Reads until {@code in} is exhausted or the timeout has elapsed. */ + public static boolean skipAll(Source in, int timeoutMillis) throws IOException { + long startNanos = System.nanoTime(); + Buffer skipBuffer = new Buffer(); + while (NANOSECONDS.toMillis(System.nanoTime() - startNanos) < timeoutMillis) { + long read = in.read(skipBuffer, 2048); + if (read == -1) return true; // Successfully exhausted the stream. + skipBuffer.clear(); + } + return false; // Ran out of time. + } + + /** Returns a 32 character string containing a hash of {@code s}. */ + public static String hash(String s) { + try { + MessageDigest messageDigest = MessageDigest.getInstance("MD5"); + byte[] md5bytes = messageDigest.digest(s.getBytes("UTF-8")); + return ByteString.of(md5bytes).hex(); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + } + + /** Returns an immutable copy of {@code list}. */ + public static List immutableList(List list) { + return Collections.unmodifiableList(new ArrayList(list)); + } + + /** Returns an immutable list containing {@code elements}. */ + public static List immutableList(T... elements) { + return Collections.unmodifiableList(Arrays.asList(elements.clone())); + } + + public static ThreadFactory threadFactory(final String name, final boolean daemon) { + return new ThreadFactory() { + @Override public Thread newThread(Runnable runnable) { + Thread result = new Thread(runnable, name); + result.setDaemon(daemon); + return result; + } + }; + } + + public static List

headerEntries(String... elements) { + List
result = new ArrayList
(elements.length / 2); + for (int i = 0; i < elements.length; i += 2) { + result.add(new Header(elements[i], elements[i + 1])); + } + return result; + } + + public static RetryableSink emptySink() { + return EMPTY_SINK; + } + + private static final RetryableSink EMPTY_SINK = new RetryableSink(0); +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/http/AuthenticatorAdapter.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/AuthenticatorAdapter.java new file mode 100755 index 00000000..816f7c65 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/AuthenticatorAdapter.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal.http; + +import com.contentstack.okhttp.Authenticator; +import com.contentstack.okhttp.Challenge; +import com.contentstack.okhttp.Credentials; +import com.contentstack.okhttp.Request; +import com.contentstack.okhttp.Response; + +import java.io.IOException; +import java.net.Authenticator.RequestorType; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.PasswordAuthentication; +import java.net.Proxy; +import java.net.URL; +import java.util.List; + +/** Adapts {@link java.net.Authenticator} to {@link Authenticator}. */ +public final class AuthenticatorAdapter implements Authenticator { + /** Uses the global authenticator to get the password. */ + public static final Authenticator INSTANCE = new AuthenticatorAdapter(); + + @Override public Request authenticate(Proxy proxy, Response response) throws IOException { + List challenges = response.challenges(); + Request request = response.request(); + URL url = request.url(); + for (int i = 0, size = challenges.size(); i < size; i++) { + Challenge challenge = challenges.get(i); + if (!"Basic".equalsIgnoreCase(challenge.getScheme())) continue; + + PasswordAuthentication auth = java.net.Authenticator.requestPasswordAuthentication( + url.getHost(), getConnectToInetAddress(proxy, url), url.getPort(), url.getProtocol(), + challenge.getRealm(), challenge.getScheme(), url, RequestorType.SERVER); + if (auth == null) continue; + + String credential = Credentials.basic(auth.getUserName(), new String(auth.getPassword())); + return request.newBuilder() + .header("Authorization", credential) + .build(); + } + return null; + + } + + @Override public Request authenticateProxy(Proxy proxy, Response response) throws IOException { + List challenges = response.challenges(); + Request request = response.request(); + URL url = request.url(); + for (int i = 0, size = challenges.size(); i < size; i++) { + Challenge challenge = challenges.get(i); + if (!"Basic".equalsIgnoreCase(challenge.getScheme())) continue; + + InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address(); + PasswordAuthentication auth = java.net.Authenticator.requestPasswordAuthentication( + proxyAddress.getHostName(), getConnectToInetAddress(proxy, url), proxyAddress.getPort(), + url.getProtocol(), challenge.getRealm(), challenge.getScheme(), url, + RequestorType.PROXY); + if (auth == null) continue; + + String credential = Credentials.basic(auth.getUserName(), new String(auth.getPassword())); + return request.newBuilder() + .header("Proxy-Authorization", credential) + .build(); + } + return null; + } + + private InetAddress getConnectToInetAddress(Proxy proxy, URL url) throws IOException { + return (proxy != null && proxy.type() != Proxy.Type.DIRECT) + ? ((InetSocketAddress) proxy.address()).getAddress() + : InetAddress.getByName(url.getHost()); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/http/CacheRequest.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/CacheRequest.java new file mode 100755 index 00000000..e6333f6e --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/CacheRequest.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal.http; + +import com.contentstack.okio.Sink; + +import java.io.IOException; + +public interface CacheRequest { + Sink body() throws IOException; + void abort(); +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/http/CacheStrategy.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/CacheStrategy.java new file mode 100755 index 00000000..4baddf2a --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/CacheStrategy.java @@ -0,0 +1,282 @@ +package com.contentstack.okhttp.internal.http; + +import com.contentstack.okhttp.CacheControl; +import com.contentstack.okhttp.Request; +import com.contentstack.okhttp.Response; + +import java.net.HttpURLConnection; +import java.util.Date; + +import static java.util.concurrent.TimeUnit.SECONDS; + +/** + * Given a request and cached response, this figures out whether to use the + * network, the cache, or both. + * + *

Selecting a cache strategy may add conditions to the request (like the + * "If-Modified-Since" header for conditional GETs) or warnings to the cached + * response (if the cached data is potentially stale). + */ +public final class CacheStrategy { + /** The request to send on the network, or null if this call doesn't use the network. */ + public final Request networkRequest; + + /** The cached response to return or validate; or null if this call doesn't use a cache. */ + public final Response cacheResponse; + + private CacheStrategy(Request networkRequest, Response cacheResponse) { + this.networkRequest = networkRequest; + this.cacheResponse = cacheResponse; + } + + /** + * Returns true if {@code response} can be stored to later serve another + * request. + */ + public static boolean isCacheable(Response response, Request request) { + // Always go to network for uncacheable response codes (RFC 2616, 13.4), + // This implementation doesn't support caching partial content. + int responseCode = response.code(); + if (responseCode != HttpURLConnection.HTTP_OK + && responseCode != HttpURLConnection.HTTP_NOT_AUTHORITATIVE + && responseCode != HttpURLConnection.HTTP_MULT_CHOICE + && responseCode != HttpURLConnection.HTTP_MOVED_PERM + && responseCode != HttpURLConnection.HTTP_GONE) { + return false; + } + + // Responses to authorized requests aren't cacheable unless they include + // a 'public', 'must-revalidate' or 's-maxage' directive. + CacheControl responseCaching = response.cacheControl(); + if (request.header("Authorization") != null + && !responseCaching.isPublic() + && !responseCaching.mustRevalidate() + && responseCaching.sMaxAgeSeconds() == -1) { + return false; + } + + if (responseCaching.noStore()) { + return false; + } + + return true; + } + + public static class Factory { + final long nowMillis; + final Request request; + final Response cacheResponse; + + /** The server's time when the cached response was served, if known. */ + private Date servedDate; + private String servedDateString; + + /** The last modified date of the cached response, if known. */ + private Date lastModified; + private String lastModifiedString; + + /** + * The expiration date of the cached response, if known. If both this field + * and the max age are set, the max age is preferred. + */ + private Date expires; + + /** + * Extension header set by OkHttp specifying the timestamp when the cached + * HTTP request was first initiated. + */ + private long sentRequestMillis; + + /** + * Extension header set by OkHttp specifying the timestamp when the cached + * HTTP response was first received. + */ + private long receivedResponseMillis; + + /** Etag of the cached response. */ + private String etag; + + /** Age of the cached response. */ + private int ageSeconds = -1; + + public Factory(long nowMillis, Request request, Response cacheResponse) { + this.nowMillis = nowMillis; + this.request = request; + this.cacheResponse = cacheResponse; + + if (cacheResponse != null) { + for (int i = 0; i < cacheResponse.headers().size(); i++) { + String fieldName = cacheResponse.headers().name(i); + String value = cacheResponse.headers().value(i); + if ("Date".equalsIgnoreCase(fieldName)) { + servedDate = HttpDate.parse(value); + servedDateString = value; + } else if ("Expires".equalsIgnoreCase(fieldName)) { + expires = HttpDate.parse(value); + } else if ("Last-Modified".equalsIgnoreCase(fieldName)) { + lastModified = HttpDate.parse(value); + lastModifiedString = value; + } else if ("ETag".equalsIgnoreCase(fieldName)) { + etag = value; + } else if ("Age".equalsIgnoreCase(fieldName)) { + ageSeconds = HeaderParser.parseSeconds(value); + } else if (OkHeaders.SENT_MILLIS.equalsIgnoreCase(fieldName)) { + sentRequestMillis = Long.parseLong(value); + } else if (OkHeaders.RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) { + receivedResponseMillis = Long.parseLong(value); + } + } + } + } + + /** + * Returns a strategy to satisfy {@code request} using the a cached response + * {@code response}. + */ + public CacheStrategy get() { + CacheStrategy candidate = getCandidate(); + + if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) { + // We're forbidden from using the network and the cache is insufficient. + return new CacheStrategy(null, null); + } + + return candidate; + } + + /** Returns a strategy to use assuming the request can use the network. */ + private CacheStrategy getCandidate() { + // No cached response. + if (cacheResponse == null) { + return new CacheStrategy(request, null); + } + + // Drop the cached response if it's missing a required handshake. + if (request.isHttps() && cacheResponse.handshake() == null) { + return new CacheStrategy(request, null); + } + + // If this response shouldn't have been stored, it should never be used + // as a response source. This check should be redundant as long as the + // persistence store is well-behaved and the rules are constant. + if (!isCacheable(cacheResponse, request)) { + return new CacheStrategy(request, null); + } + + CacheControl requestCaching = request.cacheControl(); + if (requestCaching.noCache() || hasConditions(request)) { + return new CacheStrategy(request, null); + } + + long ageMillis = cacheResponseAge(); + long freshMillis = computeFreshnessLifetime(); + + if (requestCaching.maxAgeSeconds() != -1) { + freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds())); + } + + long minFreshMillis = 0; + if (requestCaching.minFreshSeconds() != -1) { + minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds()); + } + + long maxStaleMillis = 0; + CacheControl responseCaching = cacheResponse.cacheControl(); + if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) { + maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds()); + } + + if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) { + Response.Builder builder = cacheResponse.newBuilder(); + if (ageMillis + minFreshMillis >= freshMillis) { + builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\""); + } + long oneDayMillis = 24 * 60 * 60 * 1000L; + if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) { + builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\""); + } + return new CacheStrategy(null, builder.build()); + } + + Request.Builder conditionalRequestBuilder = request.newBuilder(); + + if (lastModified != null) { + conditionalRequestBuilder.header("If-Modified-Since", lastModifiedString); + } else if (servedDate != null) { + conditionalRequestBuilder.header("If-Modified-Since", servedDateString); + } + + if (etag != null) { + conditionalRequestBuilder.header("If-None-Match", etag); + } + + Request conditionalRequest = conditionalRequestBuilder.build(); + return hasConditions(conditionalRequest) + ? new CacheStrategy(conditionalRequest, cacheResponse) + : new CacheStrategy(conditionalRequest, null); + } + + /** + * Returns the number of milliseconds that the response was fresh for, + * starting from the served date. + */ + private long computeFreshnessLifetime() { + CacheControl responseCaching = cacheResponse.cacheControl(); + if (responseCaching.maxAgeSeconds() != -1) { + return SECONDS.toMillis(responseCaching.maxAgeSeconds()); + } else if (expires != null) { + long servedMillis = servedDate != null + ? servedDate.getTime() + : receivedResponseMillis; + long delta = expires.getTime() - servedMillis; + return delta > 0 ? delta : 0; + } else if (lastModified != null + && cacheResponse.request().url().getQuery() == null) { + // As recommended by the HTTP RFC and implemented in Firefox, the + // max age of a document should be defaulted to 10% of the + // document's age at the time it was served. Default expiration + // dates aren't used for URIs containing a query. + long servedMillis = servedDate != null + ? servedDate.getTime() + : sentRequestMillis; + long delta = servedMillis - lastModified.getTime(); + return delta > 0 ? (delta / 10) : 0; + } + return 0; + } + + /** + * Returns the current age of the response, in milliseconds. The calculation + * is specified by RFC 2616, 13.2.3 Age Calculations. + */ + private long cacheResponseAge() { + long apparentReceivedAge = servedDate != null + ? Math.max(0, receivedResponseMillis - servedDate.getTime()) + : 0; + long receivedAge = ageSeconds != -1 + ? Math.max(apparentReceivedAge, SECONDS.toMillis(ageSeconds)) + : apparentReceivedAge; + long responseDuration = receivedResponseMillis - sentRequestMillis; + long residentDuration = nowMillis - receivedResponseMillis; + return receivedAge + responseDuration + residentDuration; + } + + /** + * Returns true if computeFreshnessLifetime used a heuristic. If we used a + * heuristic to serve a cached response older than 24 hours, we are required + * to attach a warning. + */ + private boolean isFreshnessLifetimeHeuristic() { + return cacheResponse.cacheControl().maxAgeSeconds() == -1 && expires == null; + } + + /** + * Returns true if the request contains conditions that save the server from + * sending a response that the client has locally. When a request is enqueued + * with its own conditions, the built-in response cache won't be used. + */ + private static boolean hasConditions(Request request) { + return request.header("If-Modified-Since") != null || request.header("If-None-Match") != null; + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/http/HeaderParser.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/HeaderParser.java new file mode 100755 index 00000000..e12ee2e1 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/HeaderParser.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contentstack.okhttp.internal.http; + +public final class HeaderParser { + /** + * Returns the next index in {@code input} at or after {@code pos} that + * contains a character from {@code characters}. Returns the input length if + * none of the requested characters can be found. + */ + public static int skipUntil(String input, int pos, String characters) { + for (; pos < input.length(); pos++) { + if (characters.indexOf(input.charAt(pos)) != -1) { + break; + } + } + return pos; + } + + /** + * Returns the next non-whitespace character in {@code input} that is white + * space. Result is undefined if input contains newline characters. + */ + public static int skipWhitespace(String input, int pos) { + for (; pos < input.length(); pos++) { + char c = input.charAt(pos); + if (c != ' ' && c != '\t') { + break; + } + } + return pos; + } + + /** + * Returns {@code value} as a positive integer, or 0 if it is negative, or + * -1 if it cannot be parsed. + */ + public static int parseSeconds(String value) { + try { + long seconds = Long.parseLong(value); + if (seconds > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } else if (seconds < 0) { + return 0; + } else { + return (int) seconds; + } + } catch (NumberFormatException e) { + return -1; + } + } + + private HeaderParser() { + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/http/HttpConnection.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/HttpConnection.java new file mode 100755 index 00000000..8ebc25d3 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/HttpConnection.java @@ -0,0 +1,587 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contentstack.okhttp.internal.http; + +import com.contentstack.okhttp.Connection; +import com.contentstack.okhttp.ConnectionPool; +import com.contentstack.okhttp.Headers; +import com.contentstack.okhttp.Response; +import com.contentstack.okhttp.internal.Internal; +import com.contentstack.okhttp.internal.Util; +import com.contentstack.okio.Buffer; +import com.contentstack.okio.BufferedSink; +import com.contentstack.okio.BufferedSource; +import com.contentstack.okio.Okio; +import com.contentstack.okio.Sink; +import com.contentstack.okio.Source; +import com.contentstack.okio.Timeout; + +import java.io.IOException; +import java.net.ProtocolException; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.util.concurrent.TimeUnit; + +/** + * A socket connection that can be used to send HTTP/1.1 messages. This class + * strictly enforces the following lifecycle: + *

    + *
  1. {@link #writeRequest Send request headers}. + *
  2. Open a sink to write the request body. Either {@link + * #newFixedLengthSink fixed-length} or {@link #newChunkedSink chunked}. + *
  3. Write to and then close that stream. + *
  4. {@link #readResponse Read response headers}. + *
  5. Open the HTTP response body input stream. Either {@link + * #newFixedLengthSource fixed-length}, {@link #newChunkedSource chunked} + * or {@link #newUnknownLengthSource unknown length}. + *
  6. Read from and close that stream. + *
+ *

Exchanges that do not have a request body may skip creating and closing + * the request body. Exchanges that do not have a response body must call {@link + * #emptyResponseBody}. + */ +public final class HttpConnection { + private static final int STATE_IDLE = 0; // Idle connections are ready to write request headers. + private static final int STATE_OPEN_REQUEST_BODY = 1; + private static final int STATE_WRITING_REQUEST_BODY = 2; + private static final int STATE_READ_RESPONSE_HEADERS = 3; + private static final int STATE_OPEN_RESPONSE_BODY = 4; + private static final int STATE_READING_RESPONSE_BODY = 5; + private static final int STATE_CLOSED = 6; + + private static final int ON_IDLE_HOLD = 0; + private static final int ON_IDLE_POOL = 1; + private static final int ON_IDLE_CLOSE = 2; + + private final ConnectionPool pool; + private final Connection connection; + private final Socket socket; + private final BufferedSource source; + private final BufferedSink sink; + + private int state = STATE_IDLE; + private int onIdle = ON_IDLE_HOLD; + + public HttpConnection(ConnectionPool pool, Connection connection, Socket socket) + throws IOException { + this.pool = pool; + this.connection = connection; + this.socket = socket; + this.source = Okio.buffer(Okio.source(socket)); + this.sink = Okio.buffer(Okio.sink(socket)); + } + + public void setTimeouts(int readTimeoutMillis, int writeTimeoutMillis) { + if (readTimeoutMillis != 0) { + source.timeout().timeout(readTimeoutMillis, TimeUnit.MILLISECONDS); + } + if (writeTimeoutMillis != 0) { + sink.timeout().timeout(writeTimeoutMillis, TimeUnit.MILLISECONDS); + } + } + + /** + * Configure this connection to put itself back into the connection pool when + * the HTTP response body is exhausted. + */ + public void poolOnIdle() { + onIdle = ON_IDLE_POOL; + + // If we're already idle, go to the pool immediately. + if (state == STATE_IDLE) { + onIdle = ON_IDLE_HOLD; // Set the on idle policy back to the default. + Internal.instance.recycle(pool, connection); + } + } + + /** + * Configure this connection to close itself when the HTTP response body is + * exhausted. + */ + public void closeOnIdle() throws IOException { + onIdle = ON_IDLE_CLOSE; + + // If we're already idle, close immediately. + if (state == STATE_IDLE) { + state = STATE_CLOSED; + connection.getSocket().close(); + } + } + + /** Returns true if this connection is closed. */ + public boolean isClosed() { + return state == STATE_CLOSED; + } + + public void closeIfOwnedBy(Object owner) throws IOException { + Internal.instance.closeIfOwnedBy(connection, owner); + } + + public void flush() throws IOException { + sink.flush(); + } + + /** Returns the number of buffered bytes immediately readable. */ + public long bufferSize() { + return source.buffer().size(); + } + + /** Test for a stale socket. */ + public boolean isReadable() { + try { + int readTimeout = socket.getSoTimeout(); + try { + socket.setSoTimeout(1); + if (source.exhausted()) { + return false; // Stream is exhausted; socket is closed. + } + return true; + } finally { + socket.setSoTimeout(readTimeout); + } + } catch (SocketTimeoutException ignored) { + return true; // Read timed out; socket is good. + } catch (IOException e) { + return false; // Couldn't read; socket is closed. + } + } + + /** Returns bytes of a request header for sending on an HTTP transport. */ + public void writeRequest(Headers headers, String requestLine) throws IOException { + if (state != STATE_IDLE) throw new IllegalStateException("state: " + state); + sink.writeUtf8(requestLine).writeUtf8("\r\n"); + for (int i = 0; i < headers.size(); i ++) { + sink.writeUtf8(headers.name(i)) + .writeUtf8(": ") + .writeUtf8(headers.value(i)) + .writeUtf8("\r\n"); + } + sink.writeUtf8("\r\n"); + state = STATE_OPEN_REQUEST_BODY; + } + + /** Parses bytes of a response header from an HTTP transport. */ + public Response.Builder readResponse() throws IOException { + if (state != STATE_OPEN_REQUEST_BODY && state != STATE_READ_RESPONSE_HEADERS) { + throw new IllegalStateException("state: " + state); + } + + while (true) { + StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict()); + + Response.Builder responseBuilder = new Response.Builder() + .protocol(statusLine.protocol) + .code(statusLine.code) + .message(statusLine.message); + + Headers.Builder headersBuilder = new Headers.Builder(); + readHeaders(headersBuilder); + headersBuilder.add(OkHeaders.SELECTED_PROTOCOL, statusLine.protocol.toString()); + responseBuilder.headers(headersBuilder.build()); + + if (statusLine.code != StatusLine.HTTP_CONTINUE) { + state = STATE_OPEN_RESPONSE_BODY; + return responseBuilder; + } + } + } + + /** Reads headers or trailers into {@code builder}. */ + public void readHeaders(Headers.Builder builder) throws IOException { + // parse the result headers until the first blank line + for (String line; (line = source.readUtf8LineStrict()).length() != 0; ) { + Internal.instance.addLine(builder, line); + } + } + + /** + * Discards the response body so that the connection can be reused and the + * cache entry can be completed. This needs to be done judiciously, since it + * delays the current request in order to speed up a potential future request + * that may never occur. + */ + public boolean discard(Source in, int timeoutMillis) { + try { + int socketTimeout = socket.getSoTimeout(); + socket.setSoTimeout(timeoutMillis); + try { + return Util.skipAll(in, timeoutMillis); + } finally { + socket.setSoTimeout(socketTimeout); + } + } catch (IOException e) { + return false; + } + } + + public Sink newChunkedSink() { + if (state != STATE_OPEN_REQUEST_BODY) throw new IllegalStateException("state: " + state); + state = STATE_WRITING_REQUEST_BODY; + return new ChunkedSink(); + } + + public Sink newFixedLengthSink(long contentLength) { + if (state != STATE_OPEN_REQUEST_BODY) throw new IllegalStateException("state: " + state); + state = STATE_WRITING_REQUEST_BODY; + return new FixedLengthSink(contentLength); + } + + public void writeRequestBody(RetryableSink requestBody) throws IOException { + if (state != STATE_OPEN_REQUEST_BODY) throw new IllegalStateException("state: " + state); + state = STATE_READ_RESPONSE_HEADERS; + requestBody.writeToSocket(sink); + } + + public Source newFixedLengthSource(CacheRequest cacheRequest, long length) + throws IOException { + if (state != STATE_OPEN_RESPONSE_BODY) throw new IllegalStateException("state: " + state); + state = STATE_READING_RESPONSE_BODY; + return new FixedLengthSource(cacheRequest, length); + } + + /** + * Call this to advance past a response body for HTTP responses that do not + * have a response body. + */ + public void emptyResponseBody() throws IOException { + newFixedLengthSource(null, 0L); // Transition to STATE_IDLE. + } + + public Source newChunkedSource(CacheRequest cacheRequest, HttpEngine httpEngine) + throws IOException { + if (state != STATE_OPEN_RESPONSE_BODY) throw new IllegalStateException("state: " + state); + state = STATE_READING_RESPONSE_BODY; + return new ChunkedSource(cacheRequest, httpEngine); + } + + public Source newUnknownLengthSource(CacheRequest cacheRequest) throws IOException { + if (state != STATE_OPEN_RESPONSE_BODY) throw new IllegalStateException("state: " + state); + state = STATE_READING_RESPONSE_BODY; + return new UnknownLengthSource(cacheRequest); + } + + /** An HTTP body with a fixed length known in advance. */ + private final class FixedLengthSink implements Sink { + private boolean closed; + private long bytesRemaining; + + private FixedLengthSink(long bytesRemaining) { + this.bytesRemaining = bytesRemaining; + } + + @Override public Timeout timeout() { + return sink.timeout(); + } + + @Override public void write(Buffer source, long byteCount) throws IOException { + if (closed) throw new IllegalStateException("closed"); + Util.checkOffsetAndCount(source.size(), 0, byteCount); + if (byteCount > bytesRemaining) { + throw new ProtocolException("expected " + bytesRemaining + + " bytes but received " + byteCount); + } + sink.write(source, byteCount); + bytesRemaining -= byteCount; + } + + @Override public void flush() throws IOException { + if (closed) return; // Don't throw; this stream might have been closed on the caller's behalf. + sink.flush(); + } + + @Override public void close() throws IOException { + if (closed) return; + closed = true; + if (bytesRemaining > 0) throw new ProtocolException("unexpected end of stream"); + state = STATE_READ_RESPONSE_HEADERS; + } + } + + private static final String CRLF = "\r\n"; + private static final byte[] HEX_DIGITS = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + private static final byte[] FINAL_CHUNK = new byte[] { '0', '\r', '\n', '\r', '\n' }; + + /** + * An HTTP body with alternating chunk sizes and chunk bodies. It is the + * caller's responsibility to buffer chunks; typically by using a buffered + * sink with this sink. + */ + private final class ChunkedSink implements Sink { + /** Scratch space for up to 16 hex digits, and then a constant CRLF. */ + private final byte[] hex = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '\r', '\n' }; + + private boolean closed; + + @Override public Timeout timeout() { + return sink.timeout(); + } + + @Override public void write(Buffer source, long byteCount) throws IOException { + if (closed) throw new IllegalStateException("closed"); + if (byteCount == 0) return; + + writeHex(byteCount); + sink.write(source, byteCount); + sink.writeUtf8(CRLF); + } + + @Override public synchronized void flush() throws IOException { + if (closed) return; // Don't throw; this stream might have been closed on the caller's behalf. + sink.flush(); + } + + @Override public synchronized void close() throws IOException { + if (closed) return; + closed = true; + sink.write(FINAL_CHUNK); + state = STATE_READ_RESPONSE_HEADERS; + } + + /** + * Equivalent to, but cheaper than writing Long.toHexString().getBytes() + * followed by CRLF. + */ + private void writeHex(long i) throws IOException { + int cursor = 16; + do { + hex[--cursor] = HEX_DIGITS[((int) (i & 0xf))]; + } while ((i >>>= 4) != 0); + sink.write(hex, cursor, hex.length - cursor); + } + } + + private class AbstractSource { + private final CacheRequest cacheRequest; + protected final Sink cacheBody; + protected boolean closed; + + AbstractSource(CacheRequest cacheRequest) throws IOException { + // Some apps return a null body; for compatibility we treat that like a null cache request. + Sink cacheBody = cacheRequest != null ? cacheRequest.body() : null; + if (cacheBody == null) { + cacheRequest = null; + } + + this.cacheBody = cacheBody; + this.cacheRequest = cacheRequest; + } + + /** Copy the last {@code byteCount} bytes of {@code source} to the cache body. */ + protected final void cacheWrite(Buffer source, long byteCount) throws IOException { + if (cacheBody != null) { + cacheBody.write(source.clone(), byteCount); + } + } + + /** + * Closes the cache entry and makes the socket available for reuse. This + * should be invoked when the end of the body has been reached. + */ + protected final void endOfInput(boolean recyclable) throws IOException { + if (state != STATE_READING_RESPONSE_BODY) throw new IllegalStateException("state: " + state); + + if (cacheRequest != null) { + cacheBody.close(); + } + + state = STATE_IDLE; + if (recyclable && onIdle == ON_IDLE_POOL) { + onIdle = ON_IDLE_HOLD; // Set the on idle policy back to the default. + Internal.instance.recycle(pool, connection); + } else if (onIdle == ON_IDLE_CLOSE) { + state = STATE_CLOSED; + connection.getSocket().close(); + } + } + + /** + * Calls abort on the cache entry and disconnects the socket. This + * should be invoked when the connection is closed unexpectedly to + * invalidate the cache entry and to prevent the HTTP connection from + * being reused. HTTP messages are sent in serial so whenever a message + * cannot be read to completion, subsequent messages cannot be read + * either and the connection must be discarded. + * + *

An earlier implementation skipped the remaining bytes, but this + * requires that the entire transfer be completed. If the intention was + * to cancel the transfer, closing the connection is the only solution. + */ + protected final void unexpectedEndOfInput() { + if (cacheRequest != null) { + cacheRequest.abort(); + } + Util.closeQuietly(connection.getSocket()); + state = STATE_CLOSED; + } + } + + /** An HTTP body with a fixed length specified in advance. */ + private class FixedLengthSource extends AbstractSource implements Source { + private long bytesRemaining; + + public FixedLengthSource(CacheRequest cacheRequest, long length) throws IOException { + super(cacheRequest); + bytesRemaining = length; + if (bytesRemaining == 0) { + endOfInput(true); + } + } + + @Override public long read(Buffer sink, long byteCount) + throws IOException { + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + if (closed) throw new IllegalStateException("closed"); + if (bytesRemaining == 0) return -1; + + long read = source.read(sink, Math.min(bytesRemaining, byteCount)); + if (read == -1) { + unexpectedEndOfInput(); // the server didn't supply the promised content length + throw new ProtocolException("unexpected end of stream"); + } + + bytesRemaining -= read; + cacheWrite(sink, read); + if (bytesRemaining == 0) { + endOfInput(true); + } + return read; + } + + @Override public Timeout timeout() { + return source.timeout(); + } + + @Override public void close() throws IOException { + if (closed) return; + + if (bytesRemaining != 0 && !discard(this, Transport.DISCARD_STREAM_TIMEOUT_MILLIS)) { + unexpectedEndOfInput(); + } + + closed = true; + } + } + + /** An HTTP body with alternating chunk sizes and chunk bodies. */ + private class ChunkedSource extends AbstractSource implements Source { + private static final int NO_CHUNK_YET = -1; + private int bytesRemainingInChunk = NO_CHUNK_YET; + private boolean hasMoreChunks = true; + private final HttpEngine httpEngine; + + ChunkedSource(CacheRequest cacheRequest, HttpEngine httpEngine) throws IOException { + super(cacheRequest); + this.httpEngine = httpEngine; + } + + @Override public long read( + Buffer sink, long byteCount) throws IOException { + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + if (closed) throw new IllegalStateException("closed"); + if (!hasMoreChunks) return -1; + + if (bytesRemainingInChunk == 0 || bytesRemainingInChunk == NO_CHUNK_YET) { + readChunkSize(); + if (!hasMoreChunks) return -1; + } + + long read = source.read(sink, Math.min(byteCount, bytesRemainingInChunk)); + if (read == -1) { + unexpectedEndOfInput(); // the server didn't supply the promised chunk length + throw new IOException("unexpected end of stream"); + } + bytesRemainingInChunk -= read; + cacheWrite(sink, read); + return read; + } + + private void readChunkSize() throws IOException { + // read the suffix of the previous chunk + if (bytesRemainingInChunk != NO_CHUNK_YET) { + source.readUtf8LineStrict(); + } + String chunkSizeString = source.readUtf8LineStrict(); + int index = chunkSizeString.indexOf(";"); + if (index != -1) { + chunkSizeString = chunkSizeString.substring(0, index); + } + try { + bytesRemainingInChunk = Integer.parseInt(chunkSizeString.trim(), 16); + } catch (NumberFormatException e) { + throw new ProtocolException("Expected a hex chunk size but was " + chunkSizeString); + } + if (bytesRemainingInChunk == 0) { + hasMoreChunks = false; + Headers.Builder trailersBuilder = new Headers.Builder(); + readHeaders(trailersBuilder); + httpEngine.receiveHeaders(trailersBuilder.build()); + endOfInput(true); + } + } + + @Override public Timeout timeout() { + return source.timeout(); + } + + @Override public void close() throws IOException { + if (closed) return; + if (hasMoreChunks && !discard(this, Transport.DISCARD_STREAM_TIMEOUT_MILLIS)) { + unexpectedEndOfInput(); + } + closed = true; + } + } + + /** An HTTP message body terminated by the end of the underlying stream. */ + class UnknownLengthSource extends AbstractSource implements Source { + private boolean inputExhausted; + + UnknownLengthSource(CacheRequest cacheRequest) throws IOException { + super(cacheRequest); + } + + @Override public long read(Buffer sink, long byteCount) + throws IOException { + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + if (closed) throw new IllegalStateException("closed"); + if (inputExhausted) return -1; + + long read = source.read(sink, byteCount); + if (read == -1) { + inputExhausted = true; + endOfInput(false); + return -1; + } + cacheWrite(sink, read); + return read; + } + + @Override public Timeout timeout() { + return source.timeout(); + } + + @Override public void close() throws IOException { + if (closed) return; + if (!inputExhausted) { + unexpectedEndOfInput(); + } + closed = true; + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/http/HttpDate.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/HttpDate.java new file mode 100755 index 00000000..3a3e62c3 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/HttpDate.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contentstack.okhttp.internal.http; + +import java.text.DateFormat; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +/** + * Best-effort parser for HTTP dates. + */ +public final class HttpDate { + + private static final TimeZone GMT = TimeZone.getTimeZone("GMT"); + + /** + * Most websites serve cookies in the blessed format. Eagerly create the parser to ensure such + * cookies are on the fast path. + */ + private static final ThreadLocal STANDARD_DATE_FORMAT = + new ThreadLocal() { + @Override protected DateFormat initialValue() { + // RFC 2616 specified: RFC 822, updated by RFC 1123 format with fixed GMT. + DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US); + rfc1123.setLenient(false); + rfc1123.setTimeZone(GMT); + return rfc1123; + } + }; + + /** If we fail to parse a date in a non-standard format, try each of these formats in sequence. */ + private static final String[] BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS = new String[] { + // HTTP formats required by RFC2616 but with any timezone. + "EEE, dd MMM yyyy HH:mm:ss zzz", // RFC 822, updated by RFC 1123 with any TZ + "EEEE, dd-MMM-yy HH:mm:ss zzz", // RFC 850, obsoleted by RFC 1036 with any TZ. + "EEE MMM d HH:mm:ss yyyy", // ANSI C's asctime() format + // Alternative formats. + "EEE, dd-MMM-yyyy HH:mm:ss z", + "EEE, dd-MMM-yyyy HH-mm-ss z", + "EEE, dd MMM yy HH:mm:ss z", + "EEE dd-MMM-yyyy HH:mm:ss z", + "EEE dd MMM yyyy HH:mm:ss z", + "EEE dd-MMM-yyyy HH-mm-ss z", + "EEE dd-MMM-yy HH:mm:ss z", + "EEE dd MMM yy HH:mm:ss z", + "EEE,dd-MMM-yy HH:mm:ss z", + "EEE,dd-MMM-yyyy HH:mm:ss z", + "EEE, dd-MM-yyyy HH:mm:ss z", + + /* RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com */ + "EEE MMM d yyyy HH:mm:ss z", + }; + + private static final DateFormat[] BROWSER_COMPATIBLE_DATE_FORMATS = + new DateFormat[BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS.length]; + + /** Returns the date for {@code value}. Returns null if the value couldn't be parsed. */ + public static Date parse(String value) { + if (value.length() == 0) { + return null; + } + + ParsePosition position = new ParsePosition(0); + Date result = STANDARD_DATE_FORMAT.get().parse(value, position); + if (position.getIndex() == value.length()) { + // STANDARD_DATE_FORMAT must match exactly; all text must be consumed, e.g. no ignored + // non-standard trailing "+01:00". Those cases are covered below. + return result; + } + synchronized (BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS) { + for (int i = 0, count = BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS.length; i < count; i++) { + DateFormat format = BROWSER_COMPATIBLE_DATE_FORMATS[i]; + if (format == null) { + format = new SimpleDateFormat(BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS[i], Locale.US); + // Set the timezone to use when interpreting formats that don't have a timezone. GMT is + // specified by RFC 2616. + format.setTimeZone(GMT); + BROWSER_COMPATIBLE_DATE_FORMATS[i] = format; + } + position.setIndex(0); + result = format.parse(value, position); + if (position.getIndex() != 0) { + // Something was parsed. It's possible the entire string was not consumed but we ignore + // that. If any of the BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS ended in "'GMT'" we'd have + // to also check that position.getIndex() == value.length() otherwise parsing might have + // terminated early, ignoring things like "+01:00". Leaving this as != 0 means that any + // trailing junk is ignored. + return result; + } + } + } + return null; + } + + /** Returns the string for {@code value}. */ + public static String format(Date value) { + return STANDARD_DATE_FORMAT.get().format(value); + } + + private HttpDate() { + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/http/HttpEngine.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/HttpEngine.java new file mode 100755 index 00000000..605817dd --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/HttpEngine.java @@ -0,0 +1,831 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contentstack.okhttp.internal.http; + +import com.contentstack.okhttp.Connection; +import com.contentstack.okhttp.Headers; +import com.contentstack.okhttp.MediaType; +import com.contentstack.okhttp.OkHttpClient; +import com.contentstack.okhttp.Protocol; +import com.contentstack.okhttp.Request; +import com.contentstack.okhttp.Response; +import com.contentstack.okhttp.ResponseBody; +import com.contentstack.okhttp.Route; +import com.contentstack.okhttp.internal.Internal; +import com.contentstack.okhttp.internal.InternalCache; +import com.contentstack.okhttp.internal.Util; +import com.contentstack.okio.Buffer; +import com.contentstack.okio.BufferedSink; +import com.contentstack.okio.BufferedSource; +import com.contentstack.okio.GzipSource; +import com.contentstack.okio.Okio; +import com.contentstack.okio.Sink; +import com.contentstack.okio.Source; + +import java.io.IOException; +import java.io.InputStream; +import java.net.CookieHandler; +import java.net.ProtocolException; +import java.net.Proxy; +import java.net.URL; +import java.security.cert.CertificateException; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.SSLHandshakeException; + +import static com.contentstack.okhttp.internal.Util.closeQuietly; +import static com.contentstack.okhttp.internal.http.StatusLine.HTTP_CONTINUE; +import static com.contentstack.okhttp.internal.http.StatusLine.HTTP_TEMP_REDIRECT; +import static java.net.HttpURLConnection.HTTP_MOVED_PERM; +import static java.net.HttpURLConnection.HTTP_MOVED_TEMP; +import static java.net.HttpURLConnection.HTTP_MULT_CHOICE; +import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED; +import static java.net.HttpURLConnection.HTTP_NO_CONTENT; +import static java.net.HttpURLConnection.HTTP_PROXY_AUTH; +import static java.net.HttpURLConnection.HTTP_SEE_OTHER; +import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; + +/** + * Handles a single HTTP request/response pair. Each HTTP engine follows this + * lifecycle: + *

    + *
  1. It is created. + *
  2. The HTTP request message is sent with sendRequest(). Once the request + * is sent it is an error to modify the request headers. After + * sendRequest() has been called the request body can be written to if + * it exists. + *
  3. The HTTP response message is read with readResponse(). After the + * response has been read the response headers and body can be read. + * All responses have a response body input stream, though in some + * instances this stream is empty. + *
+ * + *

The request and response may be served by the HTTP response cache, by the + * network, or by both in the event of a conditional GET. + */ +public final class HttpEngine { + /** + * How many redirects should we follow? Chrome follows 21; Firefox, curl, + * and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5. + */ + public static final int MAX_REDIRECTS = 20; + + private static final ResponseBody EMPTY_BODY = new ResponseBody() { + @Override public MediaType contentType() { + return null; + } + @Override public long contentLength() { + return 0; + } + @Override public BufferedSource source() { + return new Buffer(); + } + }; + + final OkHttpClient client; + + private Connection connection; + private RouteSelector routeSelector; + private Route route; + private final Response priorResponse; + + private Transport transport; + + /** The time when the request headers were written, or -1 if they haven't been written yet. */ + long sentRequestMillis = -1; + + /** + * True if this client added an "Accept-Encoding: gzip" header field and is + * therefore responsible for also decompressing the transfer stream. + */ + private boolean transparentGzip; + + /** + * True if the request body must be completely buffered before transmission; + * false if it can be streamed. Buffering has two advantages: we don't need + * the content-length in advance and we can retransmit if necessary. The + * upside of streaming is that we can save memory. + */ + public final boolean bufferRequestBody; + + /** + * The original application-provided request. Never modified by OkHttp. When + * follow-up requests are necessary, they are derived from this request. + */ + private final Request userRequest; + + /** + * The request to send on the network, or null for no network request. This is + * derived from the user request, and customized to support OkHttp features + * like compression and caching. + */ + private Request networkRequest; + + /** + * The cached response, or null if the cache doesn't exist or cannot be used + * for this request. Conditional caching means this may be non-null even when + * the network request is non-null. Never modified by OkHttp. + */ + private Response cacheResponse; + + /** + * The response read from the network. Null if the network response hasn't + * been read yet, or if the network is not used. Never modified by OkHttp. + */ + private Response networkResponse; + + /** + * The user-visible response. This is derived from either the network + * response, cache response, or both. It is customized to support OkHttp + * features like compression and caching. + */ + private Response userResponse; + + private Sink requestBodyOut; + private BufferedSink bufferedRequestBody; + + /** Null until a response is received from the network or the cache. */ + private Source responseTransferSource; + private BufferedSource responseBody; + private InputStream responseBodyBytes; + + /** The cache request currently being populated from a network response. */ + private CacheRequest storeRequest; + private CacheStrategy cacheStrategy; + + /** + * @param request the HTTP request without a body. The body must be + * written via the engine's request body stream. + * @param connection the connection used for an intermediate response + * immediately prior to this request/response pair, such as a same-host + * redirect. This engine assumes ownership of the connection and must + * release it when it is unneeded. + * @param routeSelector the route selector used for a failed attempt + * immediately preceding this attempt, or null if this request doesn't + * recover from a failure. + */ + public HttpEngine(OkHttpClient client, Request request, boolean bufferRequestBody, + Connection connection, RouteSelector routeSelector, RetryableSink requestBodyOut, + Response priorResponse) { + this.client = client; + this.userRequest = request; + this.bufferRequestBody = bufferRequestBody; + this.connection = connection; + this.routeSelector = routeSelector; + this.requestBodyOut = requestBodyOut; + this.priorResponse = priorResponse; + + if (connection != null) { + Internal.instance.setOwner(connection, this); + this.route = connection.getRoute(); + } else { + this.route = null; + } + } + + /** + * Figures out what the response source will be, and opens a socket to that + * source if necessary. Prepares the request headers and gets ready to start + * writing the request body if it exists. + */ + public void sendRequest() throws IOException { + if (cacheStrategy != null) return; // Already sent. + if (transport != null) throw new IllegalStateException(); + + Request request = networkRequest(userRequest); + + InternalCache responseCache = Internal.instance.internalCache(client); + Response cacheCandidate = responseCache != null + ? responseCache.get(request) + : null; + + long now = System.currentTimeMillis(); + cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get(); + networkRequest = cacheStrategy.networkRequest; + cacheResponse = cacheStrategy.cacheResponse; + + if (responseCache != null) { + responseCache.trackResponse(cacheStrategy); + } + + if (cacheCandidate != null && cacheResponse == null) { + Util.closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it. + } + + if (networkRequest != null) { + // Open a connection unless we inherited one from a redirect. + if (connection == null) { + connect(networkRequest); + } + + transport = Internal.instance.newTransport(connection, this); + + // Create a request body if we don't have one already. We'll already have + // one if we're retrying a failed POST. + if (hasRequestBody() && requestBodyOut == null) { + requestBodyOut = transport.createRequestBody(request); + } + + } else { + // We aren't using the network. Recycle a connection we may have inherited from a redirect. + if (connection != null) { + Internal.instance.recycle(client.getConnectionPool(), connection); + connection = null; + } + + if (cacheResponse != null) { + // We have a valid cached response. Promote it to the user response immediately. + this.userResponse = cacheResponse.newBuilder() + .request(userRequest) + .priorResponse(stripBody(priorResponse)) + .cacheResponse(stripBody(cacheResponse)) + .build(); + } else { + // We're forbidden from using the network, and the cache is insufficient. + this.userResponse = new Response.Builder() + .request(userRequest) + .priorResponse(stripBody(priorResponse)) + .protocol(Protocol.HTTP_1_1) + .code(504) + .message("Unsatisfiable Request (only-if-cached)") + .body(EMPTY_BODY) + .build(); + } + + if (userResponse.body() != null) { + initContentStream(userResponse.body().source()); + } + } + } + + private static Response stripBody(Response response) { + return response != null && response.body() != null + ? response.newBuilder().body(null).build() + : response; + } + + /** Connect to the origin server either directly or via a proxy. */ + private void connect(Request request) throws IOException { + if (connection != null) throw new IllegalStateException(); + + if (routeSelector == null) { + routeSelector = RouteSelector.get(request, client); + } + + connection = routeSelector.next(this); + route = connection.getRoute(); + } + + /** + * Called immediately before the transport transmits HTTP request headers. + * This is used to observe the sent time should the request be cached. + */ + public void writingRequestHeaders() { + if (sentRequestMillis != -1) throw new IllegalStateException(); + sentRequestMillis = System.currentTimeMillis(); + } + + boolean hasRequestBody() { + return HttpMethod.hasRequestBody(userRequest.method()) + && !Util.emptySink().equals(requestBodyOut); + } + + /** Returns the request body or null if this request doesn't have a body. */ + public Sink getRequestBody() { + if (cacheStrategy == null) throw new IllegalStateException(); + return requestBodyOut; + } + + public BufferedSink getBufferedRequestBody() { + BufferedSink result = bufferedRequestBody; + if (result != null) return result; + Sink requestBody = getRequestBody(); + return requestBody != null + ? (bufferedRequestBody = Okio.buffer(requestBody)) + : null; + } + + public boolean hasResponse() { + return userResponse != null; + } + + public Request getRequest() { + return userRequest; + } + + public Response getResponse() { + if (userResponse == null) throw new IllegalStateException(); + return userResponse; + } + + public BufferedSource getResponseBody() { + if (userResponse == null) throw new IllegalStateException(); + return responseBody; + } + + public InputStream getResponseBodyBytes() { + InputStream result = responseBodyBytes; + return result != null + ? result + : (responseBodyBytes = Okio.buffer(getResponseBody()).inputStream()); + } + + public Connection getConnection() { + return connection; + } + + /** + * Report and attempt to recover from {@code e}. Returns a new HTTP engine + * that should be used for the retry if {@code e} is recoverable, or null if + * the failure is permanent. Requests with a body can only be recovered if the + * body is buffered. + */ + public HttpEngine recover(IOException e, Sink requestBodyOut) { + if (routeSelector != null && connection != null) { + routeSelector.connectFailed(connection, e); + } + + boolean canRetryRequestBody = requestBodyOut == null || requestBodyOut instanceof RetryableSink; + if (routeSelector == null && connection == null // No connection. + || routeSelector != null && !routeSelector.hasNext() // No more routes to attempt. + || !isRecoverable(e) + || !canRetryRequestBody) { + return null; + } + + Connection connection = close(); + + // For failure recovery, use the same route selector with a new connection. + return new HttpEngine(client, userRequest, bufferRequestBody, connection, routeSelector, + (RetryableSink) requestBodyOut, priorResponse); + } + + public HttpEngine recover(IOException e) { + return recover(e, requestBodyOut); + } + + private boolean isRecoverable(IOException e) { + // If the problem was a CertificateException from the X509TrustManager, + // do not retry, we didn't have an abrupt server-initiated exception. + boolean sslFailure = + e instanceof SSLHandshakeException && e.getCause() instanceof CertificateException; + boolean protocolFailure = e instanceof ProtocolException; + return !sslFailure && !protocolFailure; + } + + /** + * Returns the route used to retrieve the response. Null if we haven't + * connected yet, or if no connection was necessary. + */ + public Route getRoute() { + return route; + } + + private void maybeCache() throws IOException { + InternalCache responseCache = Internal.instance.internalCache(client); + if (responseCache == null) return; + + // Should we cache this response for this request? + if (!CacheStrategy.isCacheable(userResponse, networkRequest)) { + if (HttpMethod.invalidatesCache(networkRequest.method())) { + try { + responseCache.remove(networkRequest); + } catch (IOException ignored) { + // The cache cannot be written. + } + } + return; + } + + // Offer this request to the cache. + storeRequest = responseCache.put(stripBody(userResponse)); + } + + /** + * Configure the socket connection to be either pooled or closed when it is + * either exhausted or closed. If it is unneeded when this is called, it will + * be released immediately. + */ + public void releaseConnection() throws IOException { + if (transport != null && connection != null) { + transport.releaseConnectionOnIdle(); + } + connection = null; + } + + /** + * Immediately closes the socket connection if it's currently held by this + * engine. Use this to interrupt an in-flight request from any thread. It's + * the caller's responsibility to close the request body and response body + * streams; otherwise resources may be leaked. + */ + public void disconnect() { + if (transport != null) { + try { + transport.disconnect(this); + } catch (IOException ignored) { + } + } + } + + /** + * Release any resources held by this engine. If a connection is still held by + * this engine, it is returned. + */ + public Connection close() { + if (bufferedRequestBody != null) { + // This also closes the wrapped requestBodyOut. + closeQuietly(bufferedRequestBody); + } else if (requestBodyOut != null) { + Util.closeQuietly(requestBodyOut); + } + + // If this engine never achieved a response body, its connection cannot be reused. + if (responseBody == null) { + if (connection != null) closeQuietly(connection.getSocket()); + connection = null; + return null; + } + + // Close the response body. This will recycle the connection if it is eligible. + closeQuietly(responseBody); + + // Clear the buffer held by the response body input stream adapter. + Util.closeQuietly(responseBodyBytes); + + // Close the connection if it cannot be reused. + if (transport != null && connection != null && !transport.canReuseConnection()) { + closeQuietly(connection.getSocket()); + connection = null; + return null; + } + + // Prevent this engine from disconnecting a connection it no longer owns. + if (connection != null && !Internal.instance.clearOwner(connection)) { + connection = null; + } + + Connection result = connection; + connection = null; + return result; + } + + /** + * Initialize the response content stream from the response transfer source. + * These two sources are the same unless we're doing transparent gzip, in + * which case the content source is decompressed. + * + *

Whenever we do transparent gzip we also strip the corresponding headers. + * We strip the Content-Encoding header to prevent the application from + * attempting to double decompress. We strip the Content-Length header because + * it is the length of the compressed content, but the application is only + * interested in the length of the uncompressed content. + * + *

This method should only be used for non-empty response bodies. Response + * codes like "304 Not Modified" can include "Content-Encoding: gzip" without + * a response body and we will crash if we attempt to decompress the zero-byte + * source. + */ + private void initContentStream(Source transferSource) throws IOException { + responseTransferSource = transferSource; + if (transparentGzip && "gzip".equalsIgnoreCase(userResponse.header("Content-Encoding"))) { + userResponse = userResponse.newBuilder() + .removeHeader("Content-Encoding") + .removeHeader("Content-Length") + .build(); + responseBody = Okio.buffer(new GzipSource(transferSource)); + } else { + responseBody = Okio.buffer(transferSource); + } + } + + /** + * Returns true if the response must have a (possibly 0-length) body. + * See RFC 2616 section 4.3. + */ + public boolean hasResponseBody() { + // HEAD requests never yield a body regardless of the response headers. + if (userRequest.method().equals("HEAD")) { + return false; + } + + int responseCode = userResponse.code(); + if ((responseCode < HTTP_CONTINUE || responseCode >= 200) + && responseCode != HTTP_NO_CONTENT + && responseCode != HTTP_NOT_MODIFIED) { + return true; + } + + // If the Content-Length or Transfer-Encoding headers disagree with the + // response code, the response is malformed. For best compatibility, we + // honor the headers. + if (OkHeaders.contentLength(networkResponse) != -1 + || "chunked".equalsIgnoreCase(networkResponse.header("Transfer-Encoding"))) { + return true; + } + + return false; + } + + /** + * Populates request with defaults and cookies. + * + *

This client doesn't specify a default {@code Accept} header because it + * doesn't know what content publishType the application is interested in. + */ + private Request networkRequest(Request request) throws IOException { + Request.Builder result = request.newBuilder(); + + if (request.header("Host") == null) { + result.header("Host", hostHeader(request.url())); + } + + if ((connection == null || connection.getProtocol() != Protocol.HTTP_1_0) + && request.header("Connection") == null) { + result.header("Connection", "Keep-Alive"); + } + + if (request.header("Accept-Encoding") == null) { + transparentGzip = true; + result.header("Accept-Encoding", "gzip"); + } + + CookieHandler cookieHandler = client.getCookieHandler(); + if (cookieHandler != null) { + // Capture the request headers added so far so that they can be offered to the CookieHandler. + // This is mostly to stay close to the RI; it is unlikely any of the headers above would + // affect cookie choice besides "Host". + Map> headers = OkHeaders.toMultimap(result.build().headers(), null); + + Map> cookies = cookieHandler.get(request.uri(), headers); + + // Add any new cookies to the request. + OkHeaders.addCookies(result, cookies); + } + + return result.build(); + } + + public static String hostHeader(URL url) { + return Util.getEffectivePort(url) != Util.getDefaultPort(url.getProtocol()) + ? url.getHost() + ":" + url.getPort() + : url.getHost(); + } + + /** + * Flushes the remaining request header and body, parses the HTTP response + * headers and starts reading the HTTP response body if it exists. + */ + public void readResponse() throws IOException { + if (userResponse != null) { + return; // Already ready. + } + if (networkRequest == null && cacheResponse == null) { + throw new IllegalStateException("call sendRequest() first!"); + } + if (networkRequest == null) { + return; // No network response to read. + } + + // Flush the request body if there's data outstanding. + if (bufferedRequestBody != null && bufferedRequestBody.buffer().size() > 0) { + bufferedRequestBody.flush(); + } + + if (sentRequestMillis == -1) { + if (OkHeaders.contentLength(networkRequest) == -1 + && requestBodyOut instanceof RetryableSink) { + // We might not learn the Content-Length until the request body has been buffered. + long contentLength = ((RetryableSink) requestBodyOut).contentLength(); + networkRequest = networkRequest.newBuilder() + .header("Content-Length", Long.toString(contentLength)) + .build(); + } + transport.writeRequestHeaders(networkRequest); + } + + if (requestBodyOut != null) { + if (bufferedRequestBody != null) { + // This also closes the wrapped requestBodyOut. + bufferedRequestBody.close(); + } else { + requestBodyOut.close(); + } + if (requestBodyOut instanceof RetryableSink && !Util.emptySink().equals(requestBodyOut)) { + transport.writeRequestBody((RetryableSink) requestBodyOut); + } + } + + transport.flushRequest(); + + networkResponse = transport.readResponseHeaders() + .request(networkRequest) + .handshake(connection.getHandshake()) + .header(OkHeaders.SENT_MILLIS, Long.toString(sentRequestMillis)) + .header(OkHeaders.RECEIVED_MILLIS, Long.toString(System.currentTimeMillis())) + .build(); + Internal.instance.setProtocol(connection, networkResponse.protocol()); + receiveHeaders(networkResponse.headers()); + + // If we have a cache response too, then we're doing a conditional get. + if (cacheResponse != null) { + if (validate(cacheResponse, networkResponse)) { + userResponse = cacheResponse.newBuilder() + .request(userRequest) + .priorResponse(stripBody(priorResponse)) + .headers(combine(cacheResponse.headers(), networkResponse.headers())) + .cacheResponse(stripBody(cacheResponse)) + .networkResponse(stripBody(networkResponse)) + .build(); + transport.emptyTransferStream(); + releaseConnection(); + + // Update the cache after combining headers but before stripping the + // Content-Encoding header (as performed by initContentStream()). + InternalCache responseCache = Internal.instance.internalCache(client); + responseCache.trackConditionalCacheHit(); + responseCache.update(cacheResponse, stripBody(userResponse)); + + if (cacheResponse.body() != null) { + initContentStream(cacheResponse.body().source()); + } + return; + } else { + Util.closeQuietly(cacheResponse.body()); + } + } + + userResponse = networkResponse.newBuilder() + .request(userRequest) + .priorResponse(stripBody(priorResponse)) + .cacheResponse(stripBody(cacheResponse)) + .networkResponse(stripBody(networkResponse)) + .build(); + + if (!hasResponseBody()) { + // Don't call initContentStream() when the response doesn't have any content. + responseTransferSource = transport.getTransferStream(storeRequest); + responseBody = Okio.buffer(responseTransferSource); + return; + } + + maybeCache(); + initContentStream(transport.getTransferStream(storeRequest)); + } + + /** + * Returns true if {@code cached} should be used; false if {@code network} + * response should be used. + */ + private static boolean validate(Response cached, Response network) { + if (network.code() == HTTP_NOT_MODIFIED) { + return true; + } + + // The HTTP spec says that if the network's response is older than our + // cached response, we may return the cache's response. Like Chrome (but + // unlike Firefox), this client prefers to return the newer response. + Date lastModified = cached.headers().getDate("Last-Modified"); + if (lastModified != null) { + Date networkLastModified = network.headers().getDate("Last-Modified"); + if (networkLastModified != null + && networkLastModified.getTime() < lastModified.getTime()) { + return true; + } + } + + return false; + } + + /** + * Combines cached headers with a network headers as defined by RFC 2616, + * 13.5.3. + */ + private static Headers combine(Headers cachedHeaders, Headers networkHeaders) throws IOException { + Headers.Builder result = new Headers.Builder(); + + for (int i = 0; i < cachedHeaders.size(); i++) { + String fieldName = cachedHeaders.name(i); + String value = cachedHeaders.value(i); + if ("Warning".equals(fieldName) && value.startsWith("1")) { + continue; // drop 100-level freshness warnings + } + if (!OkHeaders.isEndToEnd(fieldName) || networkHeaders.get(fieldName) == null) { + result.add(fieldName, value); + } + } + + for (int i = 0; i < networkHeaders.size(); i++) { + String fieldName = networkHeaders.name(i); + if (OkHeaders.isEndToEnd(fieldName)) { + result.add(fieldName, networkHeaders.value(i)); + } + } + + return result.build(); + } + + public void receiveHeaders(Headers headers) throws IOException { + CookieHandler cookieHandler = client.getCookieHandler(); + if (cookieHandler != null) { + cookieHandler.put(userRequest.uri(), OkHeaders.toMultimap(headers, null)); + } + } + + /** + * Figures out the HTTP request to make in response to receiving this engine's + * response. This will either add authentication headers or follow redirects. + * If a follow-up is either unnecessary or not applicable, this returns null. + */ + public Request followUpRequest() throws IOException { + if (userResponse == null) throw new IllegalStateException(); + Proxy selectedProxy = getRoute() != null + ? getRoute().getProxy() + : client.getProxy(); + int responseCode = userResponse.code(); + + switch (responseCode) { + case HTTP_PROXY_AUTH: + if (selectedProxy.type() != Proxy.Type.HTTP) { + throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy"); + } + // fall-through + case HTTP_UNAUTHORIZED: + return OkHeaders.processAuthHeader(client.getAuthenticator(), userResponse, selectedProxy); + + case HTTP_TEMP_REDIRECT: + // "If the 307 status code is received in response to a request other than GET or HEAD, + // the user agent MUST NOT automatically redirect the request" + if (!userRequest.method().equals("GET") && !userRequest.method().equals("HEAD")) { + return null; + } + // fall-through + case HTTP_MULT_CHOICE: + case HTTP_MOVED_PERM: + case HTTP_MOVED_TEMP: + case HTTP_SEE_OTHER: + // Does the client allow redirects? + if (!client.getFollowRedirects()) return null; + + String location = userResponse.header("Location"); + if (location == null) return null; + URL url = new URL(userRequest.url(), location); + + // Don't follow redirects to unsupported protocols. + if (!url.getProtocol().equals("https") && !url.getProtocol().equals("http")) return null; + + // If configured, don't follow redirects between SSL and non-SSL. + boolean sameProtocol = url.getProtocol().equals(userRequest.url().getProtocol()); + if (!sameProtocol && !client.getFollowSslRedirects()) return null; + + // Redirects don't include a request body. + Request.Builder requestBuilder = userRequest.newBuilder(); + if (HttpMethod.hasRequestBody(userRequest.method())) { + requestBuilder.method("GET", null); + requestBuilder.removeHeader("Transfer-Encoding"); + requestBuilder.removeHeader("Content-Length"); + requestBuilder.removeHeader("Content-Type"); + } + + // When redirecting across hosts, drop all authentication headers. This + // is potentially annoying to the application layer since they have no + // way to retain them. + if (!sameConnection(url)) { + requestBuilder.removeHeader("Authorization"); + } + + return requestBuilder.url(url).build(); + + default: + return null; + } + } + + /** + * Returns true if an HTTP request for {@code followUp} can reuse the + * connection used by this engine. + */ + public boolean sameConnection(URL followUp) { + URL url = userRequest.url(); + return url.getHost().equals(followUp.getHost()) + && Util.getEffectivePort(url) == Util.getEffectivePort(followUp) + && url.getProtocol().equals(followUp.getProtocol()); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/http/HttpMethod.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/HttpMethod.java new file mode 100755 index 00000000..c3741495 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/HttpMethod.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal.http; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +public final class HttpMethod { + public static final Set METHODS = new LinkedHashSet(Arrays.asList( + "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "PATCH")); + + public static boolean invalidatesCache(String method) { + return method.equals("POST") + || method.equals("PATCH") + || method.equals("PUT") + || method.equals("DELETE"); + } + + public static boolean hasRequestBody(String method) { + return method.equals("POST") + || method.equals("PUT") + || method.equals("PATCH") + || method.equals("DELETE"); // Permitted as spec is ambiguous. + } + + private HttpMethod() { + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/http/HttpTransport.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/HttpTransport.java new file mode 100755 index 00000000..8584a145 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/HttpTransport.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contentstack.okhttp.internal.http; + +import com.contentstack.okhttp.Request; +import com.contentstack.okhttp.Response; +import com.contentstack.okio.Sink; +import com.contentstack.okio.Source; + +import java.io.IOException; + +public final class HttpTransport implements Transport { + private final HttpEngine httpEngine; + private final HttpConnection httpConnection; + + public HttpTransport(HttpEngine httpEngine, HttpConnection httpConnection) { + this.httpEngine = httpEngine; + this.httpConnection = httpConnection; + } + + @Override public Sink createRequestBody(Request request) throws IOException { + long contentLength = OkHeaders.contentLength(request); + + if (httpEngine.bufferRequestBody) { + if (contentLength > Integer.MAX_VALUE) { + throw new IllegalStateException("Use setFixedLengthStreamingMode() or " + + "setChunkedStreamingMode() for requests larger than 2 GiB."); + } + + if (contentLength != -1) { + // Buffer a request body of a known length. + writeRequestHeaders(request); + return new RetryableSink((int) contentLength); + } else { + // Buffer a request body of an unknown length. Don't write request + // headers until the entire body is ready; otherwise we can't set the + // Content-Length header correctly. + return new RetryableSink(); + } + } + + if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) { + // Stream a request body of unknown length. + writeRequestHeaders(request); + return httpConnection.newChunkedSink(); + } + + if (contentLength != -1) { + // Stream a request body of a known length. + writeRequestHeaders(request); + return httpConnection.newFixedLengthSink(contentLength); + } + + throw new IllegalStateException( + "Cannot stream a request body without chunked encoding or a known content length!"); + } + + @Override public void flushRequest() throws IOException { + httpConnection.flush(); + } + + @Override public void writeRequestBody(RetryableSink requestBody) throws IOException { + httpConnection.writeRequestBody(requestBody); + } + + /** + * Prepares the HTTP headers and sends them to the server. + * + *

For streaming requests with a body, headers must be prepared + * before the output stream has been written to. Otherwise + * the body would need to be buffered! + * + *

For non-streaming requests with a body, headers must be prepared + * after the output stream has been written to and closed. + * This ensures that the {@code Content-Length} header field receives the + * proper value. + */ + public void writeRequestHeaders(Request request) throws IOException { + httpEngine.writingRequestHeaders(); + String requestLine = RequestLine.get(request, + httpEngine.getConnection().getRoute().getProxy().type(), + httpEngine.getConnection().getProtocol()); + httpConnection.writeRequest(request.headers(), requestLine); + } + + @Override public Response.Builder readResponseHeaders() throws IOException { + return httpConnection.readResponse(); + } + + @Override public void releaseConnectionOnIdle() throws IOException { + if (canReuseConnection()) { + httpConnection.poolOnIdle(); + } else { + httpConnection.closeOnIdle(); + } + } + + @Override public boolean canReuseConnection() { + // If the request specified that the connection shouldn't be reused, don't reuse it. + if ("close".equalsIgnoreCase(httpEngine.getRequest().header("Connection"))) { + return false; + } + + // If the response specified that the connection shouldn't be reused, don't reuse it. + if ("close".equalsIgnoreCase(httpEngine.getResponse().header("Connection"))) { + return false; + } + + if (httpConnection.isClosed()) { + return false; + } + + return true; + } + + @Override public void emptyTransferStream() throws IOException { + httpConnection.emptyResponseBody(); + } + + @Override public Source getTransferStream(CacheRequest cacheRequest) throws IOException { + if (!httpEngine.hasResponseBody()) { + return httpConnection.newFixedLengthSource(cacheRequest, 0); + } + + if ("chunked".equalsIgnoreCase(httpEngine.getResponse().header("Transfer-Encoding"))) { + return httpConnection.newChunkedSource(cacheRequest, httpEngine); + } + + long contentLength = OkHeaders.contentLength(httpEngine.getResponse()); + if (contentLength != -1) { + return httpConnection.newFixedLengthSource(cacheRequest, contentLength); + } + + // Wrap the input stream from the connection (rather than just returning + // "socketIn" directly here), so that we can control its use after the + // reference escapes. + return httpConnection.newUnknownLengthSource(cacheRequest); + } + + @Override public void disconnect(HttpEngine engine) throws IOException { + httpConnection.closeIfOwnedBy(engine); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/http/OkHeaders.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/OkHeaders.java new file mode 100755 index 00000000..87315f46 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/OkHeaders.java @@ -0,0 +1,266 @@ +package com.contentstack.okhttp.internal.http; + +import com.contentstack.okhttp.Authenticator; +import com.contentstack.okhttp.Challenge; +import com.contentstack.okhttp.Headers; +import com.contentstack.okhttp.Protocol; +import com.contentstack.okhttp.Request; +import com.contentstack.okhttp.Response; +import com.contentstack.okhttp.internal.Platform; +import com.contentstack.okhttp.internal.Util; + +import java.io.IOException; +import java.net.Proxy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +import static java.net.HttpURLConnection.HTTP_PROXY_AUTH; + +/** Headers and utilities for internal use by OkHttp. */ +public final class OkHeaders { + private static final Comparator FIELD_NAME_COMPARATOR = new Comparator() { + // @FindBugsSuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ") + @Override public int compare(String a, String b) { + if (a == b) { + return 0; + } else if (a == null) { + return -1; + } else if (b == null) { + return 1; + } else { + return String.CASE_INSENSITIVE_ORDER.compare(a, b); + } + } + }; + + static final String PREFIX = Platform.get().getPrefix(); + + /** + * Synthetic response header: the local time when the request was sent. + */ + public static final String SENT_MILLIS = PREFIX + "-Sent-Millis"; + + /** + * Synthetic response header: the local time when the response was received. + */ + public static final String RECEIVED_MILLIS = PREFIX + "-Received-Millis"; + + /** + * Synthetic response header: the selected + * {@link Protocol protocol} ("spdy/3.1", "http/1.1", etc). + */ + public static final String SELECTED_PROTOCOL = PREFIX + "-Selected-Protocol"; + + private OkHeaders() { + } + + public static long contentLength(Request request) { + return contentLength(request.headers()); + } + + public static long contentLength(Response response) { + return contentLength(response.headers()); + } + + public static long contentLength(Headers headers) { + return stringToLong(headers.get("Content-Length")); + } + + private static long stringToLong(String s) { + if (s == null) return -1; + try { + return Long.parseLong(s); + } catch (NumberFormatException e) { + return -1; + } + } + + /** + * Returns an immutable map containing each field to its list of values. + * + * @param valueForNullKey the request line for requests, or the status line + * for responses. If non-null, this value is mapped to the null key. + */ + public static Map> toMultimap(Headers headers, String valueForNullKey) { + Map> result = new TreeMap>(FIELD_NAME_COMPARATOR); + for (int i = 0; i < headers.size(); i++) { + String fieldName = headers.name(i); + String value = headers.value(i); + + List allValues = new ArrayList(); + List otherValues = result.get(fieldName); + if (otherValues != null) { + allValues.addAll(otherValues); + } + allValues.add(value); + result.put(fieldName, Collections.unmodifiableList(allValues)); + } + if (valueForNullKey != null) { + result.put(null, Collections.unmodifiableList(Collections.singletonList(valueForNullKey))); + } + return Collections.unmodifiableMap(result); + } + + public static void addCookies(Request.Builder builder, Map> cookieHeaders) { + for (Map.Entry> entry : cookieHeaders.entrySet()) { + String key = entry.getKey(); + if (("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key)) + && !entry.getValue().isEmpty()) { + builder.addHeader(key, buildCookieHeader(entry.getValue())); + } + } + } + + /** + * Send all cookies in one big header, as recommended by + * RFC 6265. + */ + private static String buildCookieHeader(List cookies) { + if (cookies.size() == 1) return cookies.get(0); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < cookies.size(); i++) { + if (i > 0) sb.append("; "); + sb.append(cookies.get(i)); + } + return sb.toString(); + } + + /** + * Returns true if none of the Vary headers have changed between {@code + * cachedRequest} and {@code newRequest}. + */ + public static boolean varyMatches( + Response cachedResponse, Headers cachedRequest, Request newRequest) { + for (String field : varyFields(cachedResponse)) { + if (!Util.equal(cachedRequest.values(field), newRequest.headers(field))) return false; + } + return true; + } + + /** + * Returns true if a Vary header contains an asterisk. Such responses cannot + * be cached. + */ + public static boolean hasVaryAll(Response response) { + return varyFields(response).contains("*"); + } + + private static Set varyFields(Response response) { + Set result = Collections.emptySet(); + Headers headers = response.headers(); + for (int i = 0; i < headers.size(); i++) { + if (!"Vary".equalsIgnoreCase(headers.name(i))) continue; + + String value = headers.value(i); + if (result.isEmpty()) { + result = new TreeSet(String.CASE_INSENSITIVE_ORDER); + } + for (String varyField : value.split(",")) { + result.add(varyField.trim()); + } + } + return result; + } + + /** + * Returns the subset of the headers in {@code response}'s request that + * impact the content of response's body. + */ + public static Headers varyHeaders(Response response) { + Set varyFields = varyFields(response); + if (varyFields.isEmpty()) return new Headers.Builder().build(); + + // Use the request headers sent over the network, since that's what the + // response varies on. Otherwise OkHttp-supplied headers like + // "Accept-Encoding: gzip" may be lost. + Headers requestHeaders = response.networkResponse().request().headers(); + + Headers.Builder result = new Headers.Builder(); + for (int i = 0; i < requestHeaders.size(); i++) { + String fieldName = requestHeaders.name(i); + if (varyFields.contains(fieldName)) { + result.add(fieldName, requestHeaders.value(i)); + } + } + return result.build(); + } + + /** + * Returns true if {@code fieldName} is an end-to-end HTTP header, as + * defined by RFC 2616, 13.5.1. + */ + static boolean isEndToEnd(String fieldName) { + return !"Connection".equalsIgnoreCase(fieldName) + && !"Keep-Alive".equalsIgnoreCase(fieldName) + && !"Proxy-Authenticate".equalsIgnoreCase(fieldName) + && !"Proxy-Authorization".equalsIgnoreCase(fieldName) + && !"TE".equalsIgnoreCase(fieldName) + && !"Trailers".equalsIgnoreCase(fieldName) + && !"Transfer-Encoding".equalsIgnoreCase(fieldName) + && !"Upgrade".equalsIgnoreCase(fieldName); + } + + /** + * Parse RFC 2617 challenges. This API is only interested in the scheme + * name and realm. + */ + public static List parseChallenges(Headers responseHeaders, String challengeHeader) { + // auth-scheme = token + // auth-param = token "=" ( token | quoted-string ) + // challenge = auth-scheme 1*SP 1#auth-param + // realm = "realm" "=" realm-value + // realm-value = quoted-string + List result = new ArrayList(); + for (int h = 0; h < responseHeaders.size(); h++) { + if (!challengeHeader.equalsIgnoreCase(responseHeaders.name(h))) { + continue; + } + String value = responseHeaders.value(h); + int pos = 0; + while (pos < value.length()) { + int tokenStart = pos; + pos = HeaderParser.skipUntil(value, pos, " "); + + String scheme = value.substring(tokenStart, pos).trim(); + pos = HeaderParser.skipWhitespace(value, pos); + + // This currently only handles schemes with a 'realm' parameter; + // It needs to be fixed to handle any scheme and any parameters + // http://code.google.com/p/android/issues/detail?id=11140 + + if (!value.regionMatches(true, pos, "realm=\"", 0, "realm=\"".length())) { + break; // Unexpected challenge parameter; give up! + } + + pos += "realm=\"".length(); + int realmStart = pos; + pos = HeaderParser.skipUntil(value, pos, "\""); + String realm = value.substring(realmStart, pos); + pos++; // Consume '"' close quote. + pos = HeaderParser.skipUntil(value, pos, ","); + pos++; // Consume ',' comma. + pos = HeaderParser.skipWhitespace(value, pos); + result.add(new Challenge(scheme, realm)); + } + } + return result; + } + + /** + * React to a failed authorization response by looking up new credentials. + * Returns a request for a subsequent attempt, or null if no further attempts + * should be made. + */ + public static Request processAuthHeader(Authenticator authenticator, Response response, + Proxy proxy) throws IOException { + return response.code() == HTTP_PROXY_AUTH + ? authenticator.authenticateProxy(proxy, response) + : authenticator.authenticate(proxy, response); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/http/RequestLine.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/RequestLine.java new file mode 100755 index 00000000..360e8ef7 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/RequestLine.java @@ -0,0 +1,58 @@ +package com.contentstack.okhttp.internal.http; + +import com.contentstack.okhttp.Protocol; +import com.contentstack.okhttp.Request; + +import java.net.HttpURLConnection; +import java.net.Proxy; +import java.net.URL; + +public final class RequestLine { + private RequestLine() { + } + + /** + * Returns the request status line, like "GET / HTTP/1.1". This is exposed + * to the application by {@link HttpURLConnection#getHeaderFields}, so it + * needs to be set even if the transport is SPDY. + */ + static String get(Request request, Proxy.Type proxyType, Protocol protocol) { + StringBuilder result = new StringBuilder(); + result.append(request.method()); + result.append(' '); + + if (includeAuthorityInRequestLine(request, proxyType)) { + result.append(request.url()); + } else { + result.append(requestPath(request.url())); + } + + result.append(' '); + result.append(version(protocol)); + return result.toString(); + } + + /** + * Returns true if the request line should contain the full URL with host + * and port (like "GET http://android.com/foo HTTP/1.1") or only the path + * (like "GET /foo HTTP/1.1"). + */ + private static boolean includeAuthorityInRequestLine(Request request, Proxy.Type proxyType) { + return !request.isHttps() && proxyType == Proxy.Type.HTTP; + } + + /** + * Returns the path to request, like the '/' in 'GET / HTTP/1.1'. Never empty, + * even if the request URL is. Includes the query component if it exists. + */ + public static String requestPath(URL url) { + String pathAndQuery = url.getFile(); + if (pathAndQuery == null) return "/"; + if (!pathAndQuery.startsWith("/")) return "/" + pathAndQuery; + return pathAndQuery; + } + + public static String version(Protocol protocol) { + return protocol == Protocol.HTTP_1_0 ? "HTTP/1.0" : "HTTP/1.1"; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/http/RetryableSink.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/RetryableSink.java new file mode 100755 index 00000000..72448d14 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/RetryableSink.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contentstack.okhttp.internal.http; + +import com.contentstack.okhttp.internal.Util; +import com.contentstack.okio.Buffer; +import com.contentstack.okio.BufferedSink; +import com.contentstack.okio.Sink; +import com.contentstack.okio.Timeout; + +import java.io.IOException; +import java.net.ProtocolException; + +/** + * An HTTP request body that's completely buffered in memory. This allows + * the post body to be transparently re-sent if the HTTP request must be + * sent multiple times. + */ +public final class RetryableSink implements Sink { + private boolean closed; + private final int limit; + private final Buffer content = new Buffer(); + + public RetryableSink(int limit) { + this.limit = limit; + } + + public RetryableSink() { + this(-1); + } + + @Override public void close() throws IOException { + if (closed) return; + closed = true; + if (content.size() < limit) { + throw new ProtocolException( + "content-length promised " + limit + " bytes, but received " + content.size()); + } + } + + @Override public void write(Buffer source, long byteCount) throws IOException { + if (closed) throw new IllegalStateException("closed"); + Util.checkOffsetAndCount(source.size(), 0, byteCount); + if (limit != -1 && content.size() > limit - byteCount) { + throw new ProtocolException("exceeded content-length limit of " + limit + " bytes"); + } + content.write(source, byteCount); + } + + @Override public void flush() throws IOException { + } + + @Override public Timeout timeout() { + return Timeout.NONE; + } + + public long contentLength() throws IOException { + return content.size(); + } + + public void writeToSocket(BufferedSink socketOut) throws IOException { + // Clone the content; otherwise we won't have data to retry. + socketOut.writeAll(content.clone()); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/http/RouteSelector.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/RouteSelector.java new file mode 100755 index 00000000..9619b54b --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/RouteSelector.java @@ -0,0 +1,325 @@ +/* + * Copyright (C) 2012 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal.http; + +import com.contentstack.okhttp.Address; +import com.contentstack.okhttp.Connection; +import com.contentstack.okhttp.ConnectionPool; +import com.contentstack.okhttp.HostResolver; +import com.contentstack.okhttp.OkHttpClient; +import com.contentstack.okhttp.Request; +import com.contentstack.okhttp.Route; +import com.contentstack.okhttp.internal.Internal; +import com.contentstack.okhttp.internal.RouteDatabase; +import com.contentstack.okhttp.internal.Util; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.SocketAddress; +import java.net.URI; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLProtocolException; +import javax.net.ssl.SSLSocketFactory; + +import static com.contentstack.okhttp.internal.Util.getEffectivePort; + +/** + * Selects routes to connect to an origin server. Each connection requires a + * choice of proxy server, IP address, and TLS mode. Connections may also be + * recycled. + */ +public final class RouteSelector { + public static final String TLS_V1 = "TLSv1"; + public static final String SSL_V3 = "SSLv3"; + + private final Address address; + private final URI uri; + private final HostResolver hostResolver; + private final OkHttpClient client; + private final ProxySelector proxySelector; + private final ConnectionPool pool; + private final RouteDatabase routeDatabase; + private final Request request; + + /* The most recently attempted route. */ + private Proxy lastProxy; + private InetSocketAddress lastInetSocketAddress; + + /* State for negotiating the next proxy to use. */ + private boolean hasNextProxy; + private Proxy userSpecifiedProxy; + private Iterator proxySelectorProxies; + + /* State for negotiating the next InetSocketAddress to use. */ + private InetAddress[] socketAddresses; + private int nextSocketAddressIndex; + private int socketPort; + + /* TLS version to attempt with the connection. */ + private String nextTlsVersion; + + /* State for negotiating failed routes */ + private final List postponedRoutes = new ArrayList(); + + private RouteSelector(Address address, URI uri, OkHttpClient client, Request request) { + this.address = address; + this.uri = uri; + this.client = client; + this.proxySelector = client.getProxySelector(); + this.pool = client.getConnectionPool(); + this.routeDatabase = Internal.instance.routeDatabase(client); + this.hostResolver = client.getHostResolver(); + this.request = request; + + resetNextProxy(uri, address.getProxy()); + } + + public static RouteSelector get(Request request, OkHttpClient client) throws IOException { + String uriHost = request.url().getHost(); + if (uriHost == null || uriHost.length() == 0) { + throw new UnknownHostException(request.url().toString()); + } + + SSLSocketFactory sslSocketFactory = null; + HostnameVerifier hostnameVerifier = null; + if (request.isHttps()) { + sslSocketFactory = client.getSslSocketFactory(); + hostnameVerifier = client.getHostnameVerifier(); + } + + Address address = new Address(uriHost, getEffectivePort(request.url()), + client.getSocketFactory(), sslSocketFactory, hostnameVerifier, client.getAuthenticator(), + client.getProxy(), client.getProtocols()); + + return new RouteSelector(address, request.uri(), client, request); + } + + /** + * Returns true if there's another route to attempt. Every address has at + * least one route. + */ + public boolean hasNext() { + return hasNextTlsVersion() + || hasNextInetSocketAddress() + || hasNextProxy() + || hasNextPostponed(); + } + + /** Selects a route to attempt and connects it if it isn't already. */ + public Connection next(HttpEngine owner) throws IOException { + Connection connection = nextUnconnected(); + Internal.instance.connectAndSetOwner(client, connection, owner, request); + return connection; + } + + /** + * Returns the next connection to attempt. + * + * @throws NoSuchElementException if there are no more routes to attempt. + */ + Connection nextUnconnected() throws IOException { + // Always prefer pooled connections over new connections. + for (Connection pooled; (pooled = pool.get(address)) != null; ) { + if (request.method().equals("GET") || Internal.instance.isReadable(pooled)) return pooled; + pooled.getSocket().close(); + } + + // Compute the next route to attempt. + if (!hasNextTlsVersion()) { + if (!hasNextInetSocketAddress()) { + if (!hasNextProxy()) { + if (!hasNextPostponed()) { + throw new NoSuchElementException(); + } + return new Connection(pool, nextPostponed()); + } + lastProxy = nextProxy(); + resetNextInetSocketAddress(lastProxy); + } + lastInetSocketAddress = nextInetSocketAddress(); + resetNextTlsVersion(); + } + + String tlsVersion = nextTlsVersion(); + Route route = new Route(address, lastProxy, lastInetSocketAddress, tlsVersion); + if (routeDatabase.shouldPostpone(route)) { + postponedRoutes.add(route); + // We will only recurse in order to skip previously failed routes. They will be + // tried last. + return nextUnconnected(); + } + + return new Connection(pool, route); + } + + /** + * Clients should invoke this method when they encounter a connectivity + * failure on a connection returned by this route selector. + */ + public void connectFailed(Connection connection, IOException failure) { + // If this is a recycled connection, don't count its failure against the route. + if (Internal.instance.recycleCount(connection) > 0) return; + + Route failedRoute = connection.getRoute(); + if (failedRoute.getProxy().type() != Proxy.Type.DIRECT && proxySelector != null) { + // Tell the proxy selector when we fail to connect on a fresh connection. + proxySelector.connectFailed(uri, failedRoute.getProxy().address(), failure); + } + + routeDatabase.failed(failedRoute); + + // If the previously returned route's problem was not related to TLS, and + // the next route only changes the TLS mode, we shouldn't even attempt it. + // This suppresses it in both this selector and also in the route database. + if (!(failure instanceof SSLHandshakeException) && !(failure instanceof SSLProtocolException)) { + while (hasNextTlsVersion()) { + Route toSuppress = new Route(address, lastProxy, lastInetSocketAddress, nextTlsVersion()); + routeDatabase.failed(toSuppress); + } + } + } + + /** Resets {@link #nextProxy} to the first option. */ + private void resetNextProxy(URI uri, Proxy proxy) { + this.hasNextProxy = true; // This includes NO_PROXY! + if (proxy != null) { + this.userSpecifiedProxy = proxy; + } else { + List proxyList = proxySelector.select(uri); + if (proxyList != null) { + this.proxySelectorProxies = proxyList.iterator(); + } + } + } + + /** Returns true if there's another proxy to try. */ + private boolean hasNextProxy() { + return hasNextProxy; + } + + /** Returns the next proxy to try. May be PROXY.NO_PROXY but never null. */ + private Proxy nextProxy() { + // If the user specifies a proxy, try that and only that. + if (userSpecifiedProxy != null) { + hasNextProxy = false; + return userSpecifiedProxy; + } + + // Try each of the ProxySelector choices until one connection succeeds. If none succeed + // then we'll try a direct connection below. + if (proxySelectorProxies != null) { + while (proxySelectorProxies.hasNext()) { + Proxy candidate = proxySelectorProxies.next(); + if (candidate.type() != Proxy.Type.DIRECT) { + return candidate; + } + } + } + + // Finally try a direct connection. + hasNextProxy = false; + return Proxy.NO_PROXY; + } + + /** Resets {@link #nextInetSocketAddress} to the first option. */ + private void resetNextInetSocketAddress(Proxy proxy) throws UnknownHostException { + socketAddresses = null; // Clear the addresses. Necessary if getAllByName() below throws! + + String socketHost; + if (proxy.type() == Proxy.Type.DIRECT) { + socketHost = uri.getHost(); + socketPort = Util.getEffectivePort(uri); + } else { + SocketAddress proxyAddress = proxy.address(); + if (!(proxyAddress instanceof InetSocketAddress)) { + throw new IllegalArgumentException( + "Proxy.address() is not an " + "InetSocketAddress: " + proxyAddress.getClass()); + } + InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress; + socketHost = proxySocketAddress.getHostName(); + socketPort = proxySocketAddress.getPort(); + } + + // Try each address for best behavior in mixed IPv4/IPv6 environments. + socketAddresses = hostResolver.getAllByName(socketHost); + nextSocketAddressIndex = 0; + } + + /** Returns true if there's another socket address to try. */ + private boolean hasNextInetSocketAddress() { + return socketAddresses != null; + } + + /** Returns the next socket address to try. */ + private InetSocketAddress nextInetSocketAddress() throws UnknownHostException { + InetSocketAddress result = + new InetSocketAddress(socketAddresses[nextSocketAddressIndex++], socketPort); + if (nextSocketAddressIndex == socketAddresses.length) { + socketAddresses = null; // So that hasNextInetSocketAddress() returns false. + nextSocketAddressIndex = 0; + } + + return result; + } + + /** + * Resets {@link #nextTlsVersion} to the first option. For routes that don't + * use SSL, this returns {@link #SSL_V3} so that there is no SSL fallback. + */ + private void resetNextTlsVersion() { + nextTlsVersion = (address.getSslSocketFactory() != null) ? TLS_V1 : SSL_V3; + } + + /** Returns true if there's another TLS version to try. */ + private boolean hasNextTlsVersion() { + return nextTlsVersion != null; + } + + /** Returns the next TLS mode to try. */ + private String nextTlsVersion() { + if (nextTlsVersion == null) { + throw new IllegalStateException("No next TLS version"); + } else if (nextTlsVersion.equals(TLS_V1)) { + nextTlsVersion = SSL_V3; + return TLS_V1; + } else if (nextTlsVersion.equals(SSL_V3)) { + nextTlsVersion = null; // So that hasNextTlsVersion() returns false. + return SSL_V3; + } else { + throw new AssertionError(); + } + } + + /** Returns true if there is another postponed route to try. */ + private boolean hasNextPostponed() { + return !postponedRoutes.isEmpty(); + } + + /** Returns the next postponed route to try. */ + private Route nextPostponed() { + return postponedRoutes.remove(0); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/http/SpdyTransport.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/SpdyTransport.java new file mode 100755 index 00000000..c037b5a3 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/SpdyTransport.java @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contentstack.okhttp.internal.http; + +import com.contentstack.okhttp.Headers; +import com.contentstack.okhttp.Protocol; +import com.contentstack.okhttp.Request; +import com.contentstack.okhttp.Response; +import com.contentstack.okhttp.internal.Util; +import com.contentstack.okhttp.internal.spdy.ErrorCode; +import com.contentstack.okhttp.internal.spdy.Header; +import com.contentstack.okhttp.internal.spdy.SpdyConnection; +import com.contentstack.okhttp.internal.spdy.SpdyStream; +import com.contentstack.okio.Buffer; +import com.contentstack.okio.ByteString; +import com.contentstack.okio.Sink; +import com.contentstack.okio.Source; +import com.contentstack.okio.Timeout; + +import java.io.IOException; +import java.net.ProtocolException; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +public final class SpdyTransport implements Transport { + /** See http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1#TOC-3.2.1-Request. */ + private static final List SPDY_3_PROHIBITED_HEADERS = Util.immutableList( + ByteString.encodeUtf8("connection"), + ByteString.encodeUtf8("host"), + ByteString.encodeUtf8("keep-alive"), + ByteString.encodeUtf8("proxy-connection"), + ByteString.encodeUtf8("transfer-encoding")); + + /** See http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-8.1.3. */ + private static final List HTTP_2_PROHIBITED_HEADERS = Util.immutableList( + ByteString.encodeUtf8("connection"), + ByteString.encodeUtf8("host"), + ByteString.encodeUtf8("keep-alive"), + ByteString.encodeUtf8("proxy-connection"), + ByteString.encodeUtf8("te"), + ByteString.encodeUtf8("transfer-encoding"), + ByteString.encodeUtf8("encoding"), + ByteString.encodeUtf8("upgrade")); + + private final HttpEngine httpEngine; + private final SpdyConnection spdyConnection; + private SpdyStream stream; + + public SpdyTransport(HttpEngine httpEngine, SpdyConnection spdyConnection) { + this.httpEngine = httpEngine; + this.spdyConnection = spdyConnection; + } + + @Override public Sink createRequestBody(Request request) throws IOException { + // If bufferRequestBody is set, we must buffer the whole request + writeRequestHeaders(request); + return stream.getSink(); + } + + @Override public void writeRequestHeaders(Request request) throws IOException { + if (stream != null) return; + + httpEngine.writingRequestHeaders(); + boolean hasRequestBody = httpEngine.hasRequestBody(); + boolean hasResponseBody = true; + String version = RequestLine.version(httpEngine.getConnection().getProtocol()); + stream = spdyConnection.newStream( + writeNameValueBlock(request, spdyConnection.getProtocol(), version), hasRequestBody, + hasResponseBody); + stream.readTimeout().timeout(httpEngine.client.getReadTimeout(), TimeUnit.MILLISECONDS); + } + + @Override public void writeRequestBody(RetryableSink requestBody) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override public void flushRequest() throws IOException { + stream.getSink().close(); + } + + @Override public Response.Builder readResponseHeaders() throws IOException { + return readNameValueBlock(stream.getResponseHeaders(), spdyConnection.getProtocol()); + } + + /** + * Returns a list of alternating names and values containing a SPDY request. + * Names are all lowercase. No names are repeated. If any name has multiple + * values, they are concatenated using "\0" as a delimiter. + */ + public static List

writeNameValueBlock(Request request, Protocol protocol, + String version) { + Headers headers = request.headers(); + List
result = new ArrayList
(headers.size() + 10); + result.add(new Header(Header.TARGET_METHOD, request.method())); + result.add(new Header(Header.TARGET_PATH, RequestLine.requestPath(request.url()))); + String host = HttpEngine.hostHeader(request.url()); + if (Protocol.SPDY_3 == protocol) { + result.add(new Header(Header.VERSION, version)); + result.add(new Header(Header.TARGET_HOST, host)); + } else if (Protocol.HTTP_2 == protocol) { + result.add(new Header(Header.TARGET_AUTHORITY, host)); // Optional in HTTP/2 + } else { + throw new AssertionError(); + } + result.add(new Header(Header.TARGET_SCHEME, request.url().getProtocol())); + + Set names = new LinkedHashSet(); + for (int i = 0; i < headers.size(); i++) { + // header names must be lowercase. + ByteString name = ByteString.encodeUtf8(headers.name(i).toLowerCase(Locale.US)); + String value = headers.value(i); + + // Drop headers that are forbidden when layering HTTP over SPDY. + if (isProhibitedHeader(protocol, name)) continue; + + // They shouldn't be set, but if they are, drop them. We've already written them! + if (name.equals(Header.TARGET_METHOD) + || name.equals(Header.TARGET_PATH) + || name.equals(Header.TARGET_SCHEME) + || name.equals(Header.TARGET_AUTHORITY) + || name.equals(Header.TARGET_HOST) + || name.equals(Header.VERSION)) { + continue; + } + + // If we haven't seen this name before, add the pair to the end of the list... + if (names.add(name)) { + result.add(new Header(name, value)); + continue; + } + + // ...otherwise concatenate the existing values and this value. + for (int j = 0; j < result.size(); j++) { + if (result.get(j).name.equals(name)) { + String concatenated = joinOnNull(result.get(j).value.utf8(), value); + result.set(j, new Header(name, concatenated)); + break; + } + } + } + return result; + } + + private static String joinOnNull(String first, String second) { + return new StringBuilder(first).append('\0').append(second).toString(); + } + + /** Returns headers for a name value block containing a SPDY response. */ + public static Response.Builder readNameValueBlock(List
headerBlock, + Protocol protocol) throws IOException { + String status = null; + String version = "HTTP/1.1"; // :version present only in spdy/3. + + Headers.Builder headersBuilder = new Headers.Builder(); + headersBuilder.set(OkHeaders.SELECTED_PROTOCOL, protocol.toString()); + for (int i = 0; i < headerBlock.size(); i++) { + ByteString name = headerBlock.get(i).name; + String values = headerBlock.get(i).value.utf8(); + for (int start = 0; start < values.length(); ) { + int end = values.indexOf('\0', start); + if (end == -1) { + end = values.length(); + } + String value = values.substring(start, end); + if (name.equals(Header.RESPONSE_STATUS)) { + status = value; + } else if (name.equals(Header.VERSION)) { + version = value; + } else if (!isProhibitedHeader(protocol, name)) { // Don't write forbidden headers! + headersBuilder.add(name.utf8(), value); + } + start = end + 1; + } + } + if (status == null) throw new ProtocolException("Expected ':status' header not present"); + if (version == null) throw new ProtocolException("Expected ':version' header not present"); + + StatusLine statusLine = StatusLine.parse(version + " " + status); + return new Response.Builder() + .protocol(protocol) + .code(statusLine.code) + .message(statusLine.message) + .headers(headersBuilder.build()); + } + + @Override public void emptyTransferStream() { + // Do nothing. + } + + @Override public Source getTransferStream(CacheRequest cacheRequest) throws IOException { + return new SpdySource(stream, cacheRequest); + } + + @Override public void releaseConnectionOnIdle() { + } + + @Override public void disconnect(HttpEngine engine) throws IOException { + stream.close(ErrorCode.CANCEL); + } + + @Override public boolean canReuseConnection() { + return true; // SpdyConnection.isClosed() ? + } + + /** When true, this header should not be emitted or consumed. */ + private static boolean isProhibitedHeader(Protocol protocol, ByteString name) { + if (protocol == Protocol.SPDY_3) { + return SPDY_3_PROHIBITED_HEADERS.contains(name); + } else if (protocol == Protocol.HTTP_2) { + return HTTP_2_PROHIBITED_HEADERS.contains(name); + } else { + throw new AssertionError(protocol); + } + } + + /** An HTTP message body terminated by the end of the underlying stream. */ + private static class SpdySource implements Source { + private final SpdyStream stream; + private final Source source; + private final CacheRequest cacheRequest; + private final Sink cacheBody; + + private boolean inputExhausted; + private boolean closed; + + SpdySource(SpdyStream stream, CacheRequest cacheRequest) throws IOException { + this.stream = stream; + this.source = stream.getSource(); + + // Some apps return a null body; for compatibility we treat that like a null cache request. + Sink cacheBody = cacheRequest != null ? cacheRequest.body() : null; + if (cacheBody == null) { + cacheRequest = null; + } + + this.cacheBody = cacheBody; + this.cacheRequest = cacheRequest; + } + + @Override public long read(Buffer buffer, long byteCount) + throws IOException { + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + if (closed) throw new IllegalStateException("closed"); + if (inputExhausted) return -1; + + long read = source.read(buffer, byteCount); + if (read == -1) { + inputExhausted = true; + if (cacheRequest != null) { + cacheBody.close(); + } + return -1; + } + + if (cacheBody != null) { + // Get buffer.copyTo(cacheBody, read); + cacheBody.write(buffer.clone(), read); + } + + return read; + } + + @Override public Timeout timeout() { + return source.timeout(); + } + + @Override public void close() throws IOException { + if (closed) return; + + if (!inputExhausted && cacheBody != null) { + discardStream(); // Could make inputExhausted true! + } + + closed = true; + + if (!inputExhausted) { + stream.closeLater(ErrorCode.CANCEL); + if (cacheRequest != null) { + cacheRequest.abort(); + } + } + } + + private boolean discardStream() { + long oldTimeoutNanos = stream.readTimeout().timeoutNanos(); + stream.readTimeout().timeout(DISCARD_STREAM_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + try { + Util.skipAll(this, DISCARD_STREAM_TIMEOUT_MILLIS); + return true; + } catch (IOException e) { + return false; + } finally { + stream.readTimeout().timeout(oldTimeoutNanos, TimeUnit.NANOSECONDS); + } + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/http/StatusLine.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/StatusLine.java new file mode 100755 index 00000000..54a1515d --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/StatusLine.java @@ -0,0 +1,90 @@ +package com.contentstack.okhttp.internal.http; + +import com.contentstack.okhttp.Protocol; +import com.contentstack.okhttp.Response; + +import java.io.IOException; +import java.net.ProtocolException; + +/** An HTTP response status line like "HTTP/1.1 200 OK". */ +public final class StatusLine { + /** Numeric status code, 307: Temporary Redirect. */ + public static final int HTTP_TEMP_REDIRECT = 307; + public static final int HTTP_CONTINUE = 100; + + public final Protocol protocol; + public final int code; + public final String message; + + public StatusLine(Protocol protocol, int code, String message) { + this.protocol = protocol; + this.code = code; + this.message = message; + } + + public static StatusLine get(Response response) { + return new StatusLine(response.protocol(), response.code(), response.message()); + } + + public static StatusLine parse(String statusLine) throws IOException { + // H T T P / 1 . 1 2 0 0 T e m p o r a r y R e d i r e c t + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 + + // Parse protocol like "HTTP/1.1" followed by a space. + int codeStart; + Protocol protocol; + if (statusLine.startsWith("HTTP/1.")) { + if (statusLine.length() < 9 || statusLine.charAt(8) != ' ') { + throw new ProtocolException("Unexpected status line: " + statusLine); + } + int httpMinorVersion = statusLine.charAt(7) - '0'; + codeStart = 9; + if (httpMinorVersion == 0) { + protocol = Protocol.HTTP_1_0; + } else if (httpMinorVersion == 1) { + protocol = Protocol.HTTP_1_1; + } else { + throw new ProtocolException("Unexpected status line: " + statusLine); + } + } else if (statusLine.startsWith("ICY ")) { + // Shoutcast uses ICY instead of "HTTP/1.0". + protocol = Protocol.HTTP_1_0; + codeStart = 4; + } else { + throw new ProtocolException("Unexpected status line: " + statusLine); + } + + // Parse response code like "200". Always 3 digits. + if (statusLine.length() < codeStart + 3) { + throw new ProtocolException("Unexpected status line: " + statusLine); + } + int code; + try { + code = Integer.parseInt(statusLine.substring(codeStart, codeStart + 3)); + } catch (NumberFormatException e) { + throw new ProtocolException("Unexpected status line: " + statusLine); + } + + // Parse an optional response message like "OK" or "Not Modified". If it + // exists, it is separated from the response code by a space. + String message = ""; + if (statusLine.length() > codeStart + 3) { + if (statusLine.charAt(codeStart + 3) != ' ') { + throw new ProtocolException("Unexpected status line: " + statusLine); + } + message = statusLine.substring(codeStart + 4); + } + + return new StatusLine(protocol, code, message); + } + + @Override public String toString() { + StringBuilder result = new StringBuilder(); + result.append(protocol == Protocol.HTTP_1_0 ? "HTTP/1.0" : "HTTP/1.1"); + result.append(' ').append(code); + if (message != null) { + result.append(' ').append(message); + } + return result.toString(); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/http/Transport.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/Transport.java new file mode 100755 index 00000000..b8a9d6e1 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/http/Transport.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contentstack.okhttp.internal.http; + +import com.contentstack.okhttp.Request; +import com.contentstack.okhttp.Response; +import com.contentstack.okio.Sink; +import com.contentstack.okio.Source; + +import java.io.IOException; + +public interface Transport { + /** + * The timeout to use while discarding a stream of input data. Since this is + * used for connection reuse, this timeout should be significantly less than + * the time it takes to establish a new connection. + */ + int DISCARD_STREAM_TIMEOUT_MILLIS = 100; + + /** + * Returns an output stream where the request body can be written. The + * returned stream will of one of two publishType: + *
    + *
  • Direct. Bytes are written to the socket and + * forgotten. This is most efficient, particularly for large request + * bodies. The returned stream may be buffered; the caller must call + * {@link #flushRequest} before reading the response.
  • + *
  • Buffered. Bytes are written to an in memory + * buffer, and must be explicitly flushed with a call to {@link + * #writeRequestBody}. This allows HTTP authorization (401, 407) + * responses to be retransmitted transparently.
  • + *
+ */ + // Don't bother retransmitting the request body? It's quite a corner + // case and there's uncertainty whether Firefox or Chrome do this + Sink createRequestBody(Request request) throws IOException; + + /** This should update the HTTP engine's sentRequestMillis field. */ + void writeRequestHeaders(Request request) throws IOException; + + /** + * Sends the request body returned by {@link #createRequestBody} to the + * remote peer. + */ + void writeRequestBody(RetryableSink requestBody) throws IOException; + + /** Flush the request body to the underlying socket. */ + void flushRequest() throws IOException; + + /** Read response headers and update the cookie manager. */ + Response.Builder readResponseHeaders() throws IOException; + + /** Notify the transport that no response body will be read. */ + void emptyTransferStream() throws IOException; + + // Make this the content stream? + Source getTransferStream(CacheRequest cacheRequest) throws IOException; + + /** + * Configures the response body to pool or close the socket connection when + * the response body is closed. + */ + void releaseConnectionOnIdle() throws IOException; + + void disconnect(HttpEngine engine) throws IOException; + + /** + * Returns true if the socket connection held by this transport can be reused + * for a follow-up exchange. + */ + boolean canReuseConnection(); +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/huc/CacheAdapter.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/huc/CacheAdapter.java new file mode 100755 index 00000000..2b2bb36c --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/huc/CacheAdapter.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal.huc; + +import com.contentstack.okhttp.Request; +import com.contentstack.okhttp.Response; +import com.contentstack.okhttp.internal.InternalCache; +import com.contentstack.okhttp.internal.http.CacheRequest; +import com.contentstack.okhttp.internal.http.CacheStrategy; +import com.contentstack.okio.Okio; +import com.contentstack.okio.Sink; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.CacheResponse; +import java.net.HttpURLConnection; +import java.net.ResponseCache; +import java.net.URI; +import java.util.List; +import java.util.Map; + +/** Adapts {@link ResponseCache} to {@link InternalCache}. */ +public final class CacheAdapter implements InternalCache { + private final ResponseCache delegate; + + public CacheAdapter(ResponseCache delegate) { + this.delegate = delegate; + } + + public ResponseCache getDelegate() { + return delegate; + } + + @Override public Response get(Request request) throws IOException { + CacheResponse javaResponse = getJavaCachedResponse(request); + if (javaResponse == null) { + return null; + } + return JavaApiConverter.createOkResponse(request, javaResponse); + } + + @Override public CacheRequest put(Response response) throws IOException { + URI uri = response.request().uri(); + HttpURLConnection connection = JavaApiConverter.createJavaUrlConnection(response); + final java.net.CacheRequest request = delegate.put(uri, connection); + if (request == null) { + return null; + } + return new CacheRequest() { + @Override public Sink body() throws IOException { + OutputStream body = request.getBody(); + return body != null ? Okio.sink(body) : null; + } + + @Override public void abort() { + request.abort(); + } + }; + } + + @Override public void remove(Request request) throws IOException { + // This method is treated as optional and there is no obvious way of implementing it with + // ResponseCache. Removing items from the cache due to modifications made from this client is + // not essential given that modifications could be made from any other client. We have to assume + // that it's ok to keep using the cached data. Otherwise the server shouldn't declare it as + // cacheable or the client should be careful about caching it. + } + + @Override public void update(Response cached, Response network) throws IOException { + // This method is treated as optional and there is no obvious way of implementing it with + // ResponseCache. Updating headers is useful if the server changes the metadata for a resource + // (e.g. max age) to extend or truncate the life of that resource in the cache. If the metadata + // is not updated the caching behavior may not be optimal, but will obey the metadata sent + // with the original cached response. + } + + @Override public void trackConditionalCacheHit() { + // This method is optional. + } + + @Override public void trackResponse(CacheStrategy cacheStrategy) { + // This method is optional. + } + + /** + * Returns the {@link CacheResponse} from the delegate by converting the + * OkHttp {@link Request} into the arguments required by the {@link ResponseCache}. + */ + private CacheResponse getJavaCachedResponse(Request request) throws IOException { + Map> headers = JavaApiConverter.extractJavaHeaders(request); + return delegate.get(request.uri(), request.method(), headers); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/huc/DelegatingHttpsURLConnection.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/huc/DelegatingHttpsURLConnection.java new file mode 100755 index 00000000..b2fbd442 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/huc/DelegatingHttpsURLConnection.java @@ -0,0 +1,303 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal.huc; + +import com.contentstack.okhttp.Handshake; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.ProtocolException; +import java.net.URL; +import java.security.Permission; +import java.security.Principal; +import java.security.cert.Certificate; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSocketFactory; + +/** + * Implement an HTTPS connection by delegating to an HTTP connection for + * everything but the HTTPS-specific stuff. + */ +abstract class DelegatingHttpsURLConnection extends HttpsURLConnection { + private final HttpURLConnection delegate; + + public DelegatingHttpsURLConnection(HttpURLConnection delegate) { + super(delegate.getURL()); + this.delegate = delegate; + } + + protected abstract Handshake handshake(); + + @Override public abstract void setHostnameVerifier(HostnameVerifier hostnameVerifier); + + @Override public abstract HostnameVerifier getHostnameVerifier(); + + @Override public abstract void setSSLSocketFactory(SSLSocketFactory sslSocketFactory); + + @Override public abstract SSLSocketFactory getSSLSocketFactory(); + + @Override public String getCipherSuite() { + Handshake handshake = handshake(); + return handshake != null ? handshake.cipherSuite() : null; + } + + @Override public Certificate[] getLocalCertificates() { + Handshake handshake = handshake(); + if (handshake == null) return null; + List result = handshake.localCertificates(); + return !result.isEmpty() ? result.toArray(new Certificate[result.size()]) : null; + } + + @Override public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException { + Handshake handshake = handshake(); + if (handshake == null) return null; + List result = handshake.peerCertificates(); + return !result.isEmpty() ? result.toArray(new Certificate[result.size()]) : null; + } + + @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + Handshake handshake = handshake(); + return handshake != null ? handshake.peerPrincipal() : null; + } + + @Override public Principal getLocalPrincipal() { + Handshake handshake = handshake(); + return handshake != null ? handshake.localPrincipal() : null; + } + + @Override public void connect() throws IOException { + connected = true; + delegate.connect(); + } + + @Override public void disconnect() { + delegate.disconnect(); + } + + @Override public InputStream getErrorStream() { + return delegate.getErrorStream(); + } + + @Override public String getRequestMethod() { + return delegate.getRequestMethod(); + } + + @Override public int getResponseCode() throws IOException { + return delegate.getResponseCode(); + } + + @Override public String getResponseMessage() throws IOException { + return delegate.getResponseMessage(); + } + + @Override public void setRequestMethod(String method) throws ProtocolException { + delegate.setRequestMethod(method); + } + + @Override public boolean usingProxy() { + return delegate.usingProxy(); + } + + @Override public boolean getInstanceFollowRedirects() { + return delegate.getInstanceFollowRedirects(); + } + + @Override public void setInstanceFollowRedirects(boolean followRedirects) { + delegate.setInstanceFollowRedirects(followRedirects); + } + + @Override public boolean getAllowUserInteraction() { + return delegate.getAllowUserInteraction(); + } + + @Override public Object getContent() throws IOException { + return delegate.getContent(); + } + + @SuppressWarnings("unchecked") // Spec does not generify + @Override public Object getContent(Class[] types) throws IOException { + return delegate.getContent(types); + } + + @Override public String getContentEncoding() { + return delegate.getContentEncoding(); + } + + @Override public int getContentLength() { + return delegate.getContentLength(); + } + + @Override public String getContentType() { + return delegate.getContentType(); + } + + @Override public long getDate() { + return delegate.getDate(); + } + + @Override public boolean getDefaultUseCaches() { + return delegate.getDefaultUseCaches(); + } + + @Override public boolean getDoInput() { + return delegate.getDoInput(); + } + + @Override public boolean getDoOutput() { + return delegate.getDoOutput(); + } + + @Override public long getExpiration() { + return delegate.getExpiration(); + } + + @Override public String getHeaderField(int pos) { + return delegate.getHeaderField(pos); + } + + @Override public Map> getHeaderFields() { + return delegate.getHeaderFields(); + } + + @Override public Map> getRequestProperties() { + return delegate.getRequestProperties(); + } + + @Override public void addRequestProperty(String field, String newValue) { + delegate.addRequestProperty(field, newValue); + } + + @Override public String getHeaderField(String key) { + return delegate.getHeaderField(key); + } + + @Override public long getHeaderFieldDate(String field, long defaultValue) { + return delegate.getHeaderFieldDate(field, defaultValue); + } + + @Override public int getHeaderFieldInt(String field, int defaultValue) { + return delegate.getHeaderFieldInt(field, defaultValue); + } + + @Override public String getHeaderFieldKey(int position) { + return delegate.getHeaderFieldKey(position); + } + + @Override public long getIfModifiedSince() { + return delegate.getIfModifiedSince(); + } + + @Override public InputStream getInputStream() throws IOException { + return delegate.getInputStream(); + } + + @Override public long getLastModified() { + return delegate.getLastModified(); + } + + @Override public OutputStream getOutputStream() throws IOException { + return delegate.getOutputStream(); + } + + @Override public Permission getPermission() throws IOException { + return delegate.getPermission(); + } + + @Override public String getRequestProperty(String field) { + return delegate.getRequestProperty(field); + } + + @Override public URL getURL() { + return delegate.getURL(); + } + + @Override public boolean getUseCaches() { + return delegate.getUseCaches(); + } + + @Override public void setAllowUserInteraction(boolean newValue) { + delegate.setAllowUserInteraction(newValue); + } + + @Override public void setDefaultUseCaches(boolean newValue) { + delegate.setDefaultUseCaches(newValue); + } + + @Override public void setDoInput(boolean newValue) { + delegate.setDoInput(newValue); + } + + @Override public void setDoOutput(boolean newValue) { + delegate.setDoOutput(newValue); + } + + @Override public void setIfModifiedSince(long newValue) { + delegate.setIfModifiedSince(newValue); + } + + @Override public void setRequestProperty(String field, String newValue) { + delegate.setRequestProperty(field, newValue); + } + + @Override public void setUseCaches(boolean newValue) { + delegate.setUseCaches(newValue); + } + + @Override public void setConnectTimeout(int timeoutMillis) { + delegate.setConnectTimeout(timeoutMillis); + } + + @Override public int getConnectTimeout() { + return delegate.getConnectTimeout(); + } + + @Override public void setReadTimeout(int timeoutMillis) { + delegate.setReadTimeout(timeoutMillis); + } + + @Override public int getReadTimeout() { + return delegate.getReadTimeout(); + } + + @Override public String toString() { + return delegate.toString(); + } + + @Override public void setFixedLengthStreamingMode(int contentLength) { + delegate.setFixedLengthStreamingMode(contentLength); + } + + @Override public void setChunkedStreamingMode(int chunkLength) { + delegate.setChunkedStreamingMode(chunkLength); + } + + public long getContentLengthLong() { + return delegate.getContentLength(); + } + + public long getHeaderFieldLong(String field, long defaultValue) { + // Auto-generated method stub + return delegate.getHeaderFieldDate(field, defaultValue); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/huc/HttpURLConnectionImpl.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/huc/HttpURLConnectionImpl.java new file mode 100755 index 00000000..85e7015e --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/huc/HttpURLConnectionImpl.java @@ -0,0 +1,576 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contentstack.okhttp.internal.huc; + +import com.contentstack.okhttp.Connection; +import com.contentstack.okhttp.Handshake; +import com.contentstack.okhttp.Headers; +import com.contentstack.okhttp.OkHttpClient; +import com.contentstack.okhttp.Protocol; +import com.contentstack.okhttp.Request; +import com.contentstack.okhttp.Response; +import com.contentstack.okhttp.Route; +import com.contentstack.okhttp.internal.Internal; +import com.contentstack.okhttp.internal.Platform; +import com.contentstack.okhttp.internal.Util; +import com.contentstack.okhttp.internal.http.HttpDate; +import com.contentstack.okhttp.internal.http.HttpEngine; +import com.contentstack.okhttp.internal.http.HttpMethod; +import com.contentstack.okhttp.internal.http.OkHeaders; +import com.contentstack.okhttp.internal.http.RetryableSink; +import com.contentstack.okhttp.internal.http.StatusLine; +import com.contentstack.okio.BufferedSink; +import com.contentstack.okio.Sink; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpRetryException; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.ProtocolException; +import java.net.Proxy; +import java.net.SocketPermission; +import java.net.URL; +import java.security.Permission; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * This implementation uses HttpEngine to send requests and receive responses. + * This class may use multiple HttpEngines to follow redirects, authentication + * retries, etc. to retrieve the final response body. + * + *

What does 'connected' mean?

+ * This class inherits a {@code connected} field from the superclass. That field + * is not used to indicate not whether this URLConnection is + * currently connected. Instead, it indicates whether a connection has ever been + * attempted. Once a connection has been attempted, certain properties (request + * header fields, request method, etc.) are immutable. + */ +public class HttpURLConnectionImpl extends HttpURLConnection { + + final OkHttpClient client; + + private Headers.Builder requestHeaders = new Headers.Builder(); + + /** Like the superclass field of the same name, but a long and available on all platforms. */ + private long fixedContentLength = -1; + private int redirectionCount; + protected IOException httpEngineFailure; + protected HttpEngine httpEngine; + /** Lazily created (with synthetic headers) on first call to getHeaders(). */ + private Headers responseHeaders; + + /** + * The most recently attempted route. This will be null if we haven't sent a + * request yet, or if the response comes from a cache. + */ + private Route route; + + /** + * The most recently received TLS handshake. This will be null if we haven't + * connected yet, or if the most recent connection was HTTP (and not HTTPS). + */ + Handshake handshake; + + public HttpURLConnectionImpl(URL url, OkHttpClient client) { + super(url); + this.client = client; + } + + @Override public final void connect() throws IOException { + initHttpEngine(); + boolean success; + do { + success = execute(false); + } while (!success); + } + + @Override public final void disconnect() { + // Calling disconnect() before a connection exists should have no effect. + if (httpEngine == null) return; + + httpEngine.disconnect(); + + // This doesn't close the stream because doing so would require all stream + // access to be synchronized. It's expected that the thread using the + // connection will close its streams directly. If it doesn't, the worst + // case is that the GzipSource's Inflater won't be released until it's + // finalized. (This logs a warning on Android.) + } + + /** + * Returns an input stream from the server in the case of error such as the + * requested file (txt, htm, html) is not found on the remote server. + */ + @Override public final InputStream getErrorStream() { + try { + HttpEngine response = getResponse(); + if (response.hasResponseBody() && response.getResponse().code() >= HTTP_BAD_REQUEST) { + return response.getResponseBodyBytes(); + } + return null; + } catch (IOException e) { + return null; + } + } + + private Headers getHeaders() throws IOException { + if (responseHeaders == null) { + Response response = getResponse().getResponse(); + Headers headers = response.headers(); + + responseHeaders = headers.newBuilder() + .add(Platform.get().getPrefix() + "-Response-Source", responseSourceHeader(response)) + .build(); + } + return responseHeaders; + } + + private static String responseSourceHeader(Response response) { + if (response.networkResponse() == null) { + if (response.cacheResponse() == null) { + return "NONE"; + } + return "CACHE " + response.code(); + } + if (response.cacheResponse() == null) { + return "NETWORK " + response.code(); + } + return "CONDITIONAL_CACHE " + response.networkResponse().code(); + } + + /** + * Returns the value of the field at {@code position}. Returns null if there + * are fewer than {@code position} headers. + */ + @Override public final String getHeaderField(int position) { + try { + return getHeaders().value(position); + } catch (IOException e) { + return null; + } + } + + /** + * Returns the value of the field corresponding to the {@code fieldName}, or + * null if there is no such field. If the field has multiple values, the + * last value is returned. + */ + @Override public final String getHeaderField(String fieldName) { + try { + return fieldName == null + ? StatusLine.get(getResponse().getResponse()).toString() + : getHeaders().get(fieldName); + } catch (IOException e) { + return null; + } + } + + @Override public final String getHeaderFieldKey(int position) { + try { + return getHeaders().name(position); + } catch (IOException e) { + return null; + } + } + + @Override public final Map> getHeaderFields() { + try { + return OkHeaders.toMultimap(getHeaders(), + StatusLine.get(getResponse().getResponse()).toString()); + } catch (IOException e) { + return Collections.emptyMap(); + } + } + + @Override public final Map> getRequestProperties() { + if (connected) { + throw new IllegalStateException( + "Cannot access request header fields after connection is set"); + } + + return OkHeaders.toMultimap(requestHeaders.build(), null); + } + + @Override public final InputStream getInputStream() throws IOException { + if (!doInput) { + throw new ProtocolException("This protocol does not support input"); + } + + HttpEngine response = getResponse(); + + // if the requested file does not exist, throw an exception formerly the + // Error page from the server was returned if the requested file was + // text/html this has changed to return FileNotFoundException for all + // file publishType + if (getResponseCode() >= HTTP_BAD_REQUEST) { + throw new FileNotFoundException(url.toString()); + } + + InputStream result = response.getResponseBodyBytes(); + if (result == null) { + throw new ProtocolException("No response body exists; responseCode=" + getResponseCode()); + } + return result; + } + + @Override public final OutputStream getOutputStream() throws IOException { + connect(); + + BufferedSink sink = httpEngine.getBufferedRequestBody(); + if (sink == null) { + throw new ProtocolException("method does not support a request body: " + method); + } else if (httpEngine.hasResponse()) { + throw new ProtocolException("cannot write request body after response has been read"); + } + + return sink.outputStream(); + } + + @Override public final Permission getPermission() throws IOException { + String hostName = getURL().getHost(); + int hostPort = Util.getEffectivePort(getURL()); + if (usingProxy()) { + InetSocketAddress proxyAddress = (InetSocketAddress) client.getProxy().address(); + hostName = proxyAddress.getHostName(); + hostPort = proxyAddress.getPort(); + } + return new SocketPermission(hostName + ":" + hostPort, "connect, resolve"); + } + + @Override public final String getRequestProperty(String field) { + if (field == null) return null; + return requestHeaders.get(field); + } + + @Override public void setConnectTimeout(int timeoutMillis) { + client.setConnectTimeout(timeoutMillis, TimeUnit.MILLISECONDS); + } + + @Override + public void setInstanceFollowRedirects(boolean followRedirects) { + client.setFollowRedirects(followRedirects); + } + + @Override public int getConnectTimeout() { + return client.getConnectTimeout(); + } + + @Override public void setReadTimeout(int timeoutMillis) { + client.setReadTimeout(timeoutMillis, TimeUnit.MILLISECONDS); + } + + @Override public int getReadTimeout() { + return client.getReadTimeout(); + } + + private void initHttpEngine() throws IOException { + if (httpEngineFailure != null) { + throw httpEngineFailure; + } else if (httpEngine != null) { + return; + } + + connected = true; + try { + if (doOutput) { + if (method.equals("GET")) { + // they are requesting a stream to write to. This implies a POST method + method = "POST"; + } else if (!HttpMethod.hasRequestBody(method)) { + // If the request method is neither POST nor PUT nor PATCH, then you're not writing + throw new ProtocolException(method + " does not support writing"); + } + } + // If the user set content length to zero, we know there will not be a request body. + RetryableSink requestBody = doOutput && fixedContentLength == 0 ? Util.emptySink() : null; + httpEngine = newHttpEngine(method, null, requestBody, null); + } catch (IOException e) { + httpEngineFailure = e; + throw e; + } + } + + private HttpEngine newHttpEngine(String method, Connection connection, + RetryableSink requestBody, Response priorResponse) { + Request.Builder builder = new Request.Builder() + .url(getURL()) + .method(method, null /* No body; that's passed separately. */); + Headers headers = requestHeaders.build(); + for (int i = 0; i < headers.size(); i++) { + builder.addHeader(headers.name(i), headers.value(i)); + } + + boolean bufferRequestBody = false; + if (HttpMethod.hasRequestBody(method)) { + // Specify how the request body is terminated. + if (fixedContentLength != -1) { + builder.header("Content-Length", Long.toString(fixedContentLength)); + } else if (chunkLength > 0) { + builder.header("Transfer-Encoding", "chunked"); + } else { + bufferRequestBody = true; + } + + // Add a content type for the request body, if one isn't already present. + if (headers.get("Content-Type") == null) { + builder.header("Content-Type", "application/x-www-form-urlencoded"); + } + } + + if (headers.get("User-Agent") == null) { + builder.header("User-Agent", defaultUserAgent()); + } + + Request request = builder.build(); + + // If we're currently not using caches, make sure the engine's client doesn't have one. + OkHttpClient engineClient = client; + if (Internal.instance.internalCache(engineClient) != null && !getUseCaches()) { + engineClient = client.clone().setCache(null); + } + + return new HttpEngine(engineClient, request, bufferRequestBody, connection, null, requestBody, + priorResponse); + } + + private String defaultUserAgent() { + String agent = System.getProperty("http.agent"); + return agent != null ? agent : ("Java" + System.getProperty("java.version")); + } + + /** + * Aggressively tries to get the final HTTP response, potentially making + * many HTTP requests in the process in order to cope with redirects and + * authentication. + */ + private HttpEngine getResponse() throws IOException { + initHttpEngine(); + + if (httpEngine.hasResponse()) { + return httpEngine; + } + + while (true) { + if (!execute(true)) { + continue; + } + + Response response = httpEngine.getResponse(); + Request followUp = httpEngine.followUpRequest(); + + if (followUp == null) { + httpEngine.releaseConnection(); + return httpEngine; + } + + if (response.isRedirect() && ++redirectionCount > HttpEngine.MAX_REDIRECTS) { + throw new ProtocolException("Too many redirects: " + redirectionCount); + } + + // The first request was insufficient. Prepare for another... + url = followUp.url(); + requestHeaders = followUp.headers().newBuilder(); + + // Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM redirect + // should keep the same method, Chrome, Firefox and the RI all issue GETs + // when following any redirect. + Sink requestBody = httpEngine.getRequestBody(); + if (!followUp.method().equals(method)) { + requestBody = null; + } + + if (requestBody != null && !(requestBody instanceof RetryableSink)) { + throw new HttpRetryException("Cannot retry streamed HTTP body", responseCode); + } + + if (!httpEngine.sameConnection(followUp.url())) { + httpEngine.releaseConnection(); + } + + Connection connection = httpEngine.close(); + httpEngine = newHttpEngine(followUp.method(), connection, (RetryableSink) requestBody, + response); + } + } + + /** + * Sends a request and optionally reads a response. Returns true if the + * request was successfully executed, and false if the request can be + * retried. Throws an exception if the request failed permanently. + */ + private boolean execute(boolean readResponse) throws IOException { + try { + httpEngine.sendRequest(); + route = httpEngine.getRoute(); + handshake = httpEngine.getConnection() != null + ? httpEngine.getConnection().getHandshake() + : null; + if (readResponse) { + httpEngine.readResponse(); + } + + return true; + } catch (IOException e) { + HttpEngine retryEngine = httpEngine.recover(e); + if (retryEngine != null) { + httpEngine = retryEngine; + return false; + } + + // Give up; recovery is not possible. + httpEngineFailure = e; + throw e; + } + } + + /** + * Returns true if either: + *
    + *
  • A specific proxy was explicitly configured for this connection. + *
  • The response has already been retrieved, and a proxy was {@link + * java.net.ProxySelector selected} in order to get it. + *
+ * + *

Warning: This method may return false before attempting + * to connect and true afterwards. + */ + @Override public final boolean usingProxy() { + Proxy proxy = route != null + ? route.getProxy() + : client.getProxy(); + return proxy != null && proxy.type() != Proxy.Type.DIRECT; + } + + @Override public String getResponseMessage() throws IOException { + return getResponse().getResponse().message(); + } + + @Override public final int getResponseCode() throws IOException { + return getResponse().getResponse().code(); + } + + @Override public final void setRequestProperty(String field, String newValue) { + if (connected) { + throw new IllegalStateException("Cannot set request property after connection is made"); + } + if (field == null) { + throw new NullPointerException("field == null"); + } + if (newValue == null) { + // Silently ignore null header values for backwards compatibility with older + // android versions as well as with other URLConnection implementations. + // + // Some implementations send a malformed HTTP header when faced with + // such requests, we respect the spec and ignore the header. + Platform.get().logW("Ignoring header " + field + " because its value was null."); + return; + } + + // Deprecate use of X-Android-Transports header? + if ("X-Android-Transports".equals(field) || "X-Android-Protocols".equals(field)) { + setProtocols(newValue, false /* append */); + } else { + requestHeaders.set(field, newValue); + } + } + + @Override public void setIfModifiedSince(long newValue) { + super.setIfModifiedSince(newValue); + if (ifModifiedSince != 0) { + requestHeaders.set("If-Modified-Since", HttpDate.format(new Date(ifModifiedSince))); + } else { + requestHeaders.removeAll("If-Modified-Since"); + } + } + + @Override public final void addRequestProperty(String field, String value) { + if (connected) { + throw new IllegalStateException("Cannot add request property after connection is made"); + } + if (field == null) { + throw new NullPointerException("field == null"); + } + if (value == null) { + // Silently ignore null header values for backwards compatibility with older + // android versions as well as with other URLConnection implementations. + // + // Some implementations send a malformed HTTP header when faced with + // such requests, we respect the spec and ignore the header. + Platform.get().logW("Ignoring header " + field + " because its value was null."); + return; + } + + // Deprecate use of X-Android-Transports header? + if ("X-Android-Transports".equals(field) || "X-Android-Protocols".equals(field)) { + setProtocols(value, true /* append */); + } else { + requestHeaders.add(field, value); + } + } + + /* + * Splits and validates a comma-separated string of protocols. + * When append == false, we require that the transport list contains "http/1.1". + * Throws {@link IllegalStateException} when one of the protocols isn't + * defined in {@link Protocol OkHttp's protocol enumeration}. + */ + private void setProtocols(String protocolsString, boolean append) { + List protocolsList = new ArrayList(); + if (append) { + protocolsList.addAll(client.getProtocols()); + } + for (String protocol : protocolsString.split(",", -1)) { + try { + protocolsList.add(Protocol.get(protocol)); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + client.setProtocols(protocolsList); + } + + @Override public void setRequestMethod(String method) throws ProtocolException { + if (!HttpMethod.METHODS.contains(method)) { + throw new ProtocolException( + "Expected one of " + HttpMethod.METHODS + " but was " + method); + } + this.method = method; + } + + @Override public void setFixedLengthStreamingMode(int contentLength) { + //setFixedLengthStreamingMode(contentLength); + if (super.connected) throw new IllegalStateException("Already connected"); + if (chunkLength > 0) throw new IllegalStateException("Already in chunked mode"); + if (contentLength < 0) throw new IllegalArgumentException("contentLength < 0"); + this.fixedContentLength = contentLength; + super.fixedContentLength = (int) Math.min(contentLength, Integer.MAX_VALUE); + } + + public long getHeaderFieldLong(String field, long defaultValue) { + // Auto-generated method stub + return getHeaderFieldDate(field, defaultValue); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/huc/HttpsURLConnectionImpl.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/huc/HttpsURLConnectionImpl.java new file mode 100755 index 00000000..b6f9146e --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/huc/HttpsURLConnectionImpl.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal.huc; + +import com.contentstack.okhttp.Handshake; +import com.contentstack.okhttp.OkHttpClient; + +import java.net.URL; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSocketFactory; + +public final class HttpsURLConnectionImpl extends DelegatingHttpsURLConnection { + private final HttpURLConnectionImpl delegate; + + public HttpsURLConnectionImpl(URL url, OkHttpClient client) { + this(new HttpURLConnectionImpl(url, client)); + } + + public HttpsURLConnectionImpl(HttpURLConnectionImpl delegate) { + super(delegate); + this.delegate = delegate; + } + + @Override protected Handshake handshake() { + if (delegate.httpEngine == null) { + throw new IllegalStateException("Connection has not yet been established"); + } + + // If there's a response, get the handshake from there so that caching + // works. Otherwise get the handshake from the connection because we might + // have not connected yet. + return delegate.httpEngine.hasResponse() + ? delegate.httpEngine.getResponse().handshake() + : delegate.handshake; + } + + @Override public void setHostnameVerifier(HostnameVerifier hostnameVerifier) { + delegate.client.setHostnameVerifier(hostnameVerifier); + } + + @Override public HostnameVerifier getHostnameVerifier() { + return delegate.client.getHostnameVerifier(); + } + + @Override public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) { + delegate.client.setSslSocketFactory(sslSocketFactory); + } + + @Override public SSLSocketFactory getSSLSocketFactory() { + return delegate.client.getSslSocketFactory(); + } + + @Override public long getContentLengthLong() { + return 0; + } + + @Override + public void setFixedLengthStreamingMode(int contentLength) { + delegate.setFixedLengthStreamingMode(contentLength); + + } + + @Override public long getHeaderFieldLong(String field, long defaultValue) { + return delegate.getHeaderFieldLong(field, defaultValue); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/huc/JavaApiConverter.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/huc/JavaApiConverter.java new file mode 100755 index 00000000..bd0790d0 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/huc/JavaApiConverter.java @@ -0,0 +1,688 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal.huc; + +import com.contentstack.okhttp.Handshake; +import com.contentstack.okhttp.Headers; +import com.contentstack.okhttp.MediaType; +import com.contentstack.okhttp.Request; +import com.contentstack.okhttp.Response; +import com.contentstack.okhttp.ResponseBody; +import com.contentstack.okhttp.internal.Util; +import com.contentstack.okhttp.internal.http.OkHeaders; +import com.contentstack.okhttp.internal.http.StatusLine; +import com.contentstack.okio.BufferedSource; +import com.contentstack.okio.Okio; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.CacheResponse; +import java.net.HttpURLConnection; +import java.net.ProtocolException; +import java.net.SecureCacheResponse; +import java.net.URI; +import java.net.URLConnection; +import java.security.Principal; +import java.security.cert.Certificate; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSocketFactory; + +/** + * Helper methods that convert between Java and OkHttp representations. + */ +public final class JavaApiConverter { + + private JavaApiConverter() { + } + + /** + * Creates an OkHttp {@link Response} using the supplied {@link URI} and {@link URLConnection} + * to supply the data. The URLConnection is assumed to already be connected. + */ + public static Response createOkResponse(URI uri, URLConnection urlConnection) throws IOException { + HttpURLConnection httpUrlConnection = (HttpURLConnection) urlConnection; + + Response.Builder okResponseBuilder = new Response.Builder(); + + // Request: Create one from the URL connection. + // A connected HttpURLConnection does not permit access to request headers. + Map> requestHeaders = null; + Request okRequest = createOkRequest(uri, httpUrlConnection.getRequestMethod(), requestHeaders); + okResponseBuilder.request(okRequest); + + // Status line + StatusLine statusLine = StatusLine.parse(extractStatusLine(httpUrlConnection)); + okResponseBuilder.protocol(statusLine.protocol); + okResponseBuilder.code(statusLine.code); + okResponseBuilder.message(statusLine.message); + + // Response headers + Headers okHeaders = extractOkResponseHeaders(httpUrlConnection); + okResponseBuilder.headers(okHeaders); + + // Response body + ResponseBody okBody = createOkBody(okHeaders, urlConnection.getInputStream()); + okResponseBuilder.body(okBody); + + // Handle SSL handshake information as needed. + if (httpUrlConnection instanceof HttpsURLConnection) { + HttpsURLConnection httpsUrlConnection = (HttpsURLConnection) httpUrlConnection; + + Certificate[] peerCertificates; + try { + peerCertificates = httpsUrlConnection.getServerCertificates(); + } catch (SSLPeerUnverifiedException e) { + peerCertificates = null; + } + + Certificate[] localCertificates = httpsUrlConnection.getLocalCertificates(); + + Handshake handshake = Handshake.get( + httpsUrlConnection.getCipherSuite(), nullSafeImmutableList(peerCertificates), + nullSafeImmutableList(localCertificates)); + okResponseBuilder.handshake(handshake); + } + + return okResponseBuilder.build(); + } + + /** + * Creates an OkHttp {@link Response} using the supplied {@link Request} and {@link CacheResponse} + * to supply the data. + */ + static Response createOkResponse(Request request, CacheResponse javaResponse) + throws IOException { + Response.Builder okResponseBuilder = new Response.Builder(); + + // Request: Use the one provided. + okResponseBuilder.request(request); + + // Status line: Java has this as one of the headers. + StatusLine statusLine = StatusLine.parse(extractStatusLine(javaResponse)); + okResponseBuilder.protocol(statusLine.protocol); + okResponseBuilder.code(statusLine.code); + okResponseBuilder.message(statusLine.message); + + // Response headers + Headers okHeaders = extractOkHeaders(javaResponse); + okResponseBuilder.headers(okHeaders); + + // Response body + ResponseBody okBody = createOkBody(okHeaders, javaResponse.getBody()); + okResponseBuilder.body(okBody); + + // Handle SSL handshake information as needed. + if (javaResponse instanceof SecureCacheResponse) { + SecureCacheResponse javaSecureCacheResponse = (SecureCacheResponse) javaResponse; + + // Handshake doesn't support null lists. + List peerCertificates; + try { + peerCertificates = javaSecureCacheResponse.getServerCertificateChain(); + } catch (SSLPeerUnverifiedException e) { + peerCertificates = Collections.emptyList(); + } + List localCertificates = javaSecureCacheResponse.getLocalCertificateChain(); + if (localCertificates == null) { + localCertificates = Collections.emptyList(); + } + Handshake handshake = Handshake.get( + javaSecureCacheResponse.getCipherSuite(), peerCertificates, localCertificates); + okResponseBuilder.handshake(handshake); + } + + return okResponseBuilder.build(); + } + + /** + * Creates an OkHttp {@link Request} from the supplied information. + * + *

This method allows a {@code null} value for {@code requestHeaders} for situations + * where a connection is already connected and access to the headers has been lost. + * See {@link java.net.HttpURLConnection#getRequestProperties()} for details. + */ + public static Request createOkRequest( + URI uri, String requestMethod, Map> requestHeaders) { + + Request.Builder builder = new Request.Builder() + .url(uri.toString()) + .method(requestMethod, null); + + if (requestHeaders != null) { + Headers headers = extractOkHeaders(requestHeaders); + builder.headers(headers); + } + return builder.build(); + } + + /** + * Creates a {@link java.net.CacheResponse} of the correct (sub)type using information + * gathered from the supplied {@link Response}. + */ + public static CacheResponse createJavaCacheResponse(final Response response) { + final Headers headers = response.headers(); + final ResponseBody body = response.body(); + if (response.request().isHttps()) { + final Handshake handshake = response.handshake(); + return new SecureCacheResponse() { + @Override + public String getCipherSuite() { + return handshake != null ? handshake.cipherSuite() : null; + } + + @Override + public List getLocalCertificateChain() { + if (handshake == null) return null; + // Java requires null, not an empty list here. + List certificates = handshake.localCertificates(); + return certificates.size() > 0 ? certificates : null; + } + + @Override + public List getServerCertificateChain() throws SSLPeerUnverifiedException { + if (handshake == null) return null; + // Java requires null, not an empty list here. + List certificates = handshake.peerCertificates(); + return certificates.size() > 0 ? certificates : null; + } + + @Override + public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + if (handshake == null) return null; + return handshake.peerPrincipal(); + } + + @Override + public Principal getLocalPrincipal() { + if (handshake == null) return null; + return handshake.localPrincipal(); + } + + @Override + public Map> getHeaders() throws IOException { + // Java requires that the entry with a null key be the status line. + return OkHeaders.toMultimap(headers, StatusLine.get(response).toString()); + } + + @Override + public InputStream getBody() throws IOException { + if (body == null) return null; + return body.byteStream(); + } + }; + } else { + return new CacheResponse() { + @Override + public Map> getHeaders() throws IOException { + // Java requires that the entry with a null key be the status line. + return OkHeaders.toMultimap(headers, StatusLine.get(response).toString()); + } + + @Override + public InputStream getBody() throws IOException { + if (body == null) return null; + return body.byteStream(); + } + }; + } + } + + /** + * Creates an {@link java.net.HttpURLConnection} of the correct subclass from the supplied OkHttp + * {@link Response}. + */ + static HttpURLConnection createJavaUrlConnection(Response okResponse) { + Request request = okResponse.request(); + // Create an object of the correct class in case the ResponseCache uses instanceof. + if (request.isHttps()) { + return new CacheHttpsURLConnection(new CacheHttpURLConnection(okResponse)); + } else { + return new CacheHttpURLConnection(okResponse); + } + } + + + static Map> extractJavaHeaders(Request request) { + return OkHeaders.toMultimap(request.headers(), null); + } + + /** + * Extracts OkHttp headers from the supplied {@link java.net.CacheResponse}. Only real headers are + * extracted. See {@link #extractStatusLine(java.net.CacheResponse)}. + */ + private static Headers extractOkHeaders(CacheResponse javaResponse) throws IOException { + Map> javaResponseHeaders = javaResponse.getHeaders(); + return extractOkHeaders(javaResponseHeaders); + } + + /** + * Extracts OkHttp headers from the supplied {@link java.net.HttpURLConnection}. Only real headers + * are extracted. See {@link #extractStatusLine(java.net.HttpURLConnection)}. + */ + private static Headers extractOkResponseHeaders(HttpURLConnection httpUrlConnection) { + Map> javaResponseHeaders = httpUrlConnection.getHeaderFields(); + return extractOkHeaders(javaResponseHeaders); + } + + /** + * Extracts OkHttp headers from the supplied {@link Map}. Only real headers are + * extracted. Any entry (one with a {@code null} key) is discarded. + */ + // @VisibleForTesting + static Headers extractOkHeaders(Map> javaHeaders) { + Headers.Builder okHeadersBuilder = new Headers.Builder(); + for (Map.Entry> javaHeader : javaHeaders.entrySet()) { + String name = javaHeader.getKey(); + if (name == null) { + // The Java API uses the null key to store the status line in responses. + // Earlier versions of OkHttp would use the null key to store the "request line" in + // requests. e.g. "GET / HTTP 1.1". Although this is no longer the case it must be + // explicitly ignored because Headers.Builder does not support null keys. + continue; + } + for (String value : javaHeader.getValue()) { + okHeadersBuilder.add(name, value); + } + } + return okHeadersBuilder.build(); + } + + /** + * Extracts the status line from the supplied Java API {@link java.net.HttpURLConnection}. + * As per the spec, the status line is held as the header with the null key. Returns {@code null} + * if there is no status line. + */ + private static String extractStatusLine(HttpURLConnection httpUrlConnection) { + // Java specifies that this will be be response header with a null key. + return httpUrlConnection.getHeaderField(null); + } + + /** + * Extracts the status line from the supplied Java API {@link java.net.CacheResponse}. + * As per the spec, the status line is held as the header with the null key. Returns {@code null} + * if there is no status line. + */ + private static String extractStatusLine(CacheResponse javaResponse) throws IOException { + Map> javaResponseHeaders = javaResponse.getHeaders(); + return extractStatusLine(javaResponseHeaders); + } + + // VisibleForTesting + static String extractStatusLine(Map> javaResponseHeaders) { + List values = javaResponseHeaders.get(null); + if (values == null || values.size() == 0) { + return null; + } + return values.get(0); + } + + /** + * Creates an OkHttp Response.Body containing the supplied information. + */ + private static ResponseBody createOkBody(final Headers okHeaders, InputStream body) { + final BufferedSource source = Okio.buffer(Okio.source(body)); + return new ResponseBody() { + @Override public MediaType contentType() { + String contentTypeHeader = okHeaders.get("Content-Type"); + return contentTypeHeader == null ? null : MediaType.parse(contentTypeHeader); + } + @Override public long contentLength() { + return OkHeaders.contentLength(okHeaders); + } + @Override public BufferedSource source() { + return source; + } + }; + } + + /** + * An {@link java.net.HttpURLConnection} that represents an HTTP request at the point where + * the request has been made, and the response headers have been received, but the body content, + * if present, has not been read yet. This intended to provide enough information for + * {@link java.net.ResponseCache} subclasses and no more. + * + *

Much of the method implementations are overrides to delegate to the OkHttp request and + * response, or to deny access to information as a real HttpURLConnection would after connection. + */ + private static final class CacheHttpURLConnection extends HttpURLConnection { + + private final Request request; + private final Response response; + + public CacheHttpURLConnection(Response response) { + super(response.request().url()); + this.request = response.request(); + this.response = response; + + // Configure URLConnection inherited fields. + this.connected = true; + this.doOutput = response.body() == null; + + // Configure HttpUrlConnection inherited fields. + this.method = request.method(); + } + + // HTTP connection lifecycle methods + + @Override + public void connect() throws IOException { + throw throwRequestModificationException(); + } + + @Override + public void disconnect() { + throw throwRequestModificationException(); + } + + // HTTP Request methods + + @Override + public void setRequestProperty(String key, String value) { + throw throwRequestModificationException(); + } + + @Override + public void addRequestProperty(String key, String value) { + throw throwRequestModificationException(); + } + + @Override + public String getRequestProperty(String key) { + return request.header(key); + } + + @Override + public Map> getRequestProperties() { + // This is to preserve RI and compatibility with OkHttp's HttpURLConnectionImpl. There seems + // no good reason why this should fail while getRequestProperty() is ok. + throw throwRequestHeaderAccessException(); + } + + @Override + public void setFixedLengthStreamingMode(int contentLength) { + throw throwRequestModificationException(); + } + + + + /*@Override + public void setFixedLengthStreamingMode(long contentLength) { + throw throwRequestModificationException(); + }*/ + + @Override + public void setChunkedStreamingMode(int chunklen) { + throw throwRequestModificationException(); + } + + @Override + public void setInstanceFollowRedirects(boolean followRedirects) { + throw throwRequestModificationException(); + } + + @Override + public boolean getInstanceFollowRedirects() { + // Return the platform default. + return super.getInstanceFollowRedirects(); + } + + @Override + public void setRequestMethod(String method) throws ProtocolException { + throw throwRequestModificationException(); + } + + @Override + public String getRequestMethod() { + return request.method(); + } + + // HTTP Response methods + + @Override + public String getHeaderFieldKey(int position) { + // Deal with index 0 meaning "status line" + if (position < 0) { + throw new IllegalArgumentException("Invalid header index: " + position); + } + if (position == 0) { + return null; + } + return response.headers().name(position - 1); + } + + @Override + public String getHeaderField(int position) { + // Deal with index 0 meaning "status line" + if (position < 0) { + throw new IllegalArgumentException("Invalid header index: " + position); + } + if (position == 0) { + return StatusLine.get(response).toString(); + } + return response.headers().value(position - 1); + } + + @Override + public String getHeaderField(String fieldName) { + return fieldName == null + ? StatusLine.get(response).toString() + : response.headers().get(fieldName); + } + + @Override + public Map> getHeaderFields() { + return OkHeaders.toMultimap(response.headers(), StatusLine.get(response).toString()); + } + + @Override + public int getResponseCode() throws IOException { + return response.code(); + } + + @Override + public String getResponseMessage() throws IOException { + return response.message(); + } + + @Override + public InputStream getErrorStream() { + return null; + } + + // HTTP miscellaneous methods + + @Override + public boolean usingProxy() { + // It's safe to return false here, even if a proxy is in use. The problem is we don't + // necessarily know if we're going to use a proxy by the time we ask the cache for a response. + return false; + } + + // URLConnection methods + + @Override + public void setConnectTimeout(int timeout) { + throw throwRequestModificationException(); + } + + @Override + public int getConnectTimeout() { + // Impossible to say. + return 0; + } + + @Override + public void setReadTimeout(int timeout) { + throw throwRequestModificationException(); + } + + @Override + public int getReadTimeout() { + // Impossible to say. + return 0; + } + + @Override + public Object getContent() throws IOException { + throw throwResponseBodyAccessException(); + } + + @Override + public Object getContent(Class[] classes) throws IOException { + throw throwResponseBodyAccessException(); + } + + @Override + public InputStream getInputStream() throws IOException { + throw throwResponseBodyAccessException(); + } + + @Override + public OutputStream getOutputStream() throws IOException { + throw throwRequestModificationException(); + } + + @Override + public void setDoInput(boolean doInput) { + throw throwRequestModificationException(); + } + + @Override + public boolean getDoInput() { + return true; + } + + @Override + public void setDoOutput(boolean doOutput) { + throw throwRequestModificationException(); + } + + @Override + public boolean getDoOutput() { + return request.body() != null; + } + + @Override + public void setAllowUserInteraction(boolean allowUserInteraction) { + throw throwRequestModificationException(); + } + + @Override + public boolean getAllowUserInteraction() { + return false; + } + + @Override + public void setUseCaches(boolean useCaches) { + throw throwRequestModificationException(); + } + + @Override + public boolean getUseCaches() { + return super.getUseCaches(); + } + + @Override + public void setIfModifiedSince(long ifModifiedSince) { + throw throwRequestModificationException(); + } + + @Override + public long getIfModifiedSince() { + return 0; + } + + @Override + public boolean getDefaultUseCaches() { + return super.getDefaultUseCaches(); + } + + @Override + public void setDefaultUseCaches(boolean defaultUseCaches) { + super.setDefaultUseCaches(defaultUseCaches); + } + } + + /** An HttpsURLConnection to offer to the cache. */ + private static final class CacheHttpsURLConnection extends DelegatingHttpsURLConnection { + private final CacheHttpURLConnection delegate; + + public CacheHttpsURLConnection(CacheHttpURLConnection delegate) { + super(delegate); + this.delegate = delegate; + } + + @Override protected Handshake handshake() { + return delegate.response.handshake(); + } + + @Override public void setHostnameVerifier(HostnameVerifier hostnameVerifier) { + throw throwRequestModificationException(); + } + + @Override public HostnameVerifier getHostnameVerifier() { + throw throwRequestSslAccessException(); + } + + @Override public void setSSLSocketFactory(SSLSocketFactory socketFactory) { + throw throwRequestModificationException(); + } + + @Override public SSLSocketFactory getSSLSocketFactory() { + throw throwRequestSslAccessException(); + } + + @Override public long getContentLengthLong() { + return delegate.getContentLength(); + } + + @Override public void setFixedLengthStreamingMode(int contentLength) { + delegate.setFixedLengthStreamingMode(contentLength); + } + + @Override public long getHeaderFieldLong(String field, long defaultValue) { + return delegate.getHeaderFieldInt(field, (int) defaultValue); + } + } + + private static RuntimeException throwRequestModificationException() { + throw new UnsupportedOperationException("ResponseCache cannot modify the request."); + } + + private static RuntimeException throwRequestHeaderAccessException() { + throw new UnsupportedOperationException("ResponseCache cannot access request headers"); + } + + private static RuntimeException throwRequestSslAccessException() { + throw new UnsupportedOperationException("ResponseCache cannot access SSL internals"); + } + + private static RuntimeException throwResponseBodyAccessException() { + throw new UnsupportedOperationException("ResponseCache cannot access the response body."); + } + + private static List nullSafeImmutableList(T[] elements) { + return elements == null ? Collections.emptyList() : Util.immutableList(elements); + } + +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/ErrorCode.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/ErrorCode.java new file mode 100755 index 00000000..287c4585 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/ErrorCode.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal.spdy; + +// http://tools.ietf.org/html/draft-ietf-httpbis-http2-13#section-7 +public enum ErrorCode { + /** Not an error! For SPDY stream resets, prefer null over NO_ERROR. */ + NO_ERROR(0, -1, 0), + + PROTOCOL_ERROR(1, 1, 1), + + /** A subtype of PROTOCOL_ERROR used by SPDY. */ + INVALID_STREAM(1, 2, -1), + + /** A subtype of PROTOCOL_ERROR used by SPDY. */ + UNSUPPORTED_VERSION(1, 4, -1), + + /** A subtype of PROTOCOL_ERROR used by SPDY. */ + STREAM_IN_USE(1, 8, -1), + + /** A subtype of PROTOCOL_ERROR used by SPDY. */ + STREAM_ALREADY_CLOSED(1, 9, -1), + + INTERNAL_ERROR(2, 6, 2), + + FLOW_CONTROL_ERROR(3, 7, -1), + + STREAM_CLOSED(5, -1, -1), + + FRAME_TOO_LARGE(6, 11, -1), + + REFUSED_STREAM(7, 3, -1), + + CANCEL(8, 5, -1), + + COMPRESSION_ERROR(9, -1, -1), + + CONNECT_ERROR(10, -1, -1), + + ENHANCE_YOUR_CALM(11, -1, -1), + + INADEQUATE_SECURITY(12, -1, -1), + + INVALID_CREDENTIALS(-1, 10, -1); + + public final int httpCode; + public final int spdyRstCode; + public final int spdyGoAwayCode; + + private ErrorCode(int httpCode, int spdyRstCode, int spdyGoAwayCode) { + this.httpCode = httpCode; + this.spdyRstCode = spdyRstCode; + this.spdyGoAwayCode = spdyGoAwayCode; + } + + public static ErrorCode fromSpdy3Rst(int code) { + for (ErrorCode errorCode : ErrorCode.values()) { + if (errorCode.spdyRstCode == code) return errorCode; + } + return null; + } + + public static ErrorCode fromHttp2(int code) { + for (ErrorCode errorCode : ErrorCode.values()) { + if (errorCode.httpCode == code) return errorCode; + } + return null; + } + + public static ErrorCode fromSpdyGoAway(int code) { + for (ErrorCode errorCode : ErrorCode.values()) { + if (errorCode.spdyGoAwayCode == code) return errorCode; + } + return null; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/FrameReader.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/FrameReader.java new file mode 100755 index 00000000..07acafc2 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/FrameReader.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contentstack.okhttp.internal.spdy; + +import com.contentstack.okio.BufferedSource; +import com.contentstack.okio.ByteString; + +import java.io.Closeable; +import java.io.IOException; +import java.util.List; + +/** Reads transport frames for SPDY/3 or HTTP/2. */ +public interface FrameReader extends Closeable { + void readConnectionPreface() throws IOException; + boolean nextFrame(Handler handler) throws IOException; + + public interface Handler { + void data(boolean inFinished, int streamId, BufferedSource source, int length) + throws IOException; + + /** + * Create or update incoming headers, creating the corresponding streams + * if necessary. Frames that trigger this are SPDY SYN_STREAM, HEADERS, and + * SYN_REPLY, and HTTP/2 HEADERS and PUSH_PROMISE. + * + * @param outFinished true if the receiver should not send further frames. + * @param inFinished true if the sender will not send further frames. + * @param streamId the stream owning these headers. + * @param associatedStreamId the stream that triggered the sender to create + * this stream. + */ + void headers(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId, + List

headerBlock, HeadersMode headersMode); + void rstStream(int streamId, ErrorCode errorCode); + void settings(boolean clearPrevious, Settings settings); + + /** HTTP/2 only. */ + void ackSettings(); + + /** + * Read a connection-level ping from the peer. {@code ack} indicates this + * is a reply. Payload parameters are different between SPDY/3 and HTTP/2. + *

+ * In SPDY/3, only the first {@code payload1} parameter is set. If the + * reader is a client, it is an unsigned even number. Likewise, a server + * will receive an odd number. + *

+ * In HTTP/2, both {@code payload1} and {@code payload2} parameters are + * set. The data is opaque binary, and there are no rules on the content. + */ + void ping(boolean ack, int payload1, int payload2); + + /** + * The peer tells us to stop creating streams. It is safe to replay + * streams with {@code ID > lastGoodStreamId} on a new connection. In- + * flight streams with {@code ID <= lastGoodStreamId} can only be replayed + * on a new connection if they are idempotent. + * + * @param lastGoodStreamId the last stream ID the peer processed before + * sending this message. If {@code lastGoodStreamId} is zero, the peer + * processed no frames. + * @param errorCode reason for closing the connection. + * @param debugData only valid for HTTP/2; opaque debug data to send. + */ + void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData); + + /** + * Notifies that an additional {@code windowSizeIncrement} bytes can be + * sent on {@code streamId}, or the connection if {@code streamId} is zero. + */ + void windowUpdate(int streamId, long windowSizeIncrement); + + /** + * Called when reading a headers or priority frame. This may be used to + * change the stream's weight from the default (16) to a new value. + * + * @param streamId stream which has a priority change. + * @param streamDependency the stream ID this stream is dependent on. + * @param weight relative proportion of priority in [1..256]. + * @param exclusive inserts this stream ID as the sole child of + * {@code streamDependency}. + */ + void priority(int streamId, int streamDependency, int weight, boolean exclusive); + + /** + * HTTP/2 only. Receive a push promise header block. + *

+ * A push promise contains all the headers that pertain to a server-initiated + * request, and a {@code promisedStreamId} to which response frames will be + * delivered. Push promise frames are sent as a part of the response to + * {@code streamId}. + * + * @param streamId client-initiated stream ID. Must be an odd number. + * @param promisedStreamId server-initiated stream ID. Must be an even + * number. + * @param requestHeaders minimally includes {@code :method}, {@code :scheme}, + * {@code :authority}, and (@code :path}. + */ + void pushPromise(int streamId, int promisedStreamId, List

requestHeaders) + throws IOException; + + /** + * HTTP/2 only. Expresses that resources for the connection or a client- + * initiated stream are available from a different network location or + * protocol configuration. + * + *

See alt-svc + * + * @param streamId when a client-initiated stream ID (odd number), the + * origin of this alternate service is the origin of the stream. When + * zero, the origin is specified in the {@code origin} parameter. + * @param origin when present, the + * origin is typically + * represented as a combination of scheme, host and port. When empty, + * the origin is that of the {@code streamId}. + * @param protocol an ALPN protocol, such as {@code h2}. + * @param host an IP address or hostname. + * @param port the IP port associated with the service. + * @param maxAge time in seconds that this alternative is considered fresh. + */ + void alternateService(int streamId, String origin, ByteString protocol, String host, int port, + long maxAge); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/FrameWriter.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/FrameWriter.java new file mode 100755 index 00000000..364858fc --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/FrameWriter.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contentstack.okhttp.internal.spdy; + +import com.contentstack.okio.Buffer; + +import java.io.Closeable; +import java.io.IOException; +import java.util.List; + +/** Writes transport frames for SPDY/3 or HTTP/2. */ +public interface FrameWriter extends Closeable { + /** HTTP/2 only. */ + void connectionPreface() throws IOException; + void ackSettings() throws IOException; + + /** + * HTTP/2 only. Send a push promise header block. + *

+ * A push promise contains all the headers that pertain to a server-initiated + * request, and a {@code promisedStreamId} to which response frames will be + * delivered. Push promise frames are sent as a part of the response to + * {@code streamId}. The {@code promisedStreamId} has a priority of one + * greater than {@code streamId}. + * + * @param streamId client-initiated stream ID. Must be an odd number. + * @param promisedStreamId server-initiated stream ID. Must be an even + * number. + * @param requestHeaders minimally includes {@code :method}, {@code :scheme}, + * {@code :authority}, and (@code :path}. + */ + void pushPromise(int streamId, int promisedStreamId, List

requestHeaders) + throws IOException; + + /** SPDY/3 only. */ + void flush() throws IOException; + void synStream(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId, + List
headerBlock) throws IOException; + void synReply(boolean outFinished, int streamId, List
headerBlock) + throws IOException; + void headers(int streamId, List
headerBlock) throws IOException; + void rstStream(int streamId, ErrorCode errorCode) throws IOException; + + /** + * {@code data.length} may be longer than the max length of the variant's data frame. + * Implementations must send multiple frames as necessary. + * + * @param source the buffer to draw bytes from. May be null if byteCount is 0. + */ + void data(boolean outFinished, int streamId, Buffer source, int byteCount) throws IOException; + + void data(boolean outFinished, int streamId, Buffer source) throws IOException; + + /** Write okhttp's settings to the peer. */ + void settings(Settings okHttpSettings) throws IOException; + + /** + * Send a connection-level ping to the peer. {@code ack} indicates this is + * a reply. Payload parameters are different between SPDY/3 and HTTP/2. + *

+ * In SPDY/3, only the first {@code payload1} parameter is sent. If the + * sender is a client, it is an unsigned odd number. Likewise, a server + * will send an even number. + *

+ * In HTTP/2, both {@code payload1} and {@code payload2} parameters are + * sent. The data is opaque binary, and there are no rules on the content. + */ + void ping(boolean ack, int payload1, int payload2) throws IOException; + + /** + * Tell the peer to stop creating streams and that we last processed + * {@code lastGoodStreamId}, or zero if no streams were processed. + * + * @param lastGoodStreamId the last stream ID processed, or zero if no + * streams were processed. + * @param errorCode reason for closing the connection. + * @param debugData only valid for HTTP/2; opaque debug data to send. + */ + void goAway(int lastGoodStreamId, ErrorCode errorCode, byte[] debugData) throws IOException; + + /** + * Inform peer that an additional {@code windowSizeIncrement} bytes can be + * sent on {@code streamId}, or the connection if {@code streamId} is zero. + */ + void windowUpdate(int streamId, long windowSizeIncrement) throws IOException; +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Header.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Header.java new file mode 100755 index 00000000..9ab8e8cb --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Header.java @@ -0,0 +1,56 @@ +package com.contentstack.okhttp.internal.spdy; + +import com.contentstack.okio.ByteString; + +/** HTTP header: the name is an ASCII string, but the value can be UTF-8. */ +public final class Header { + // Special header names defined in the SPDY and HTTP/2 specs. + public static final ByteString RESPONSE_STATUS = ByteString.encodeUtf8(":status"); + public static final ByteString TARGET_METHOD = ByteString.encodeUtf8(":method"); + public static final ByteString TARGET_PATH = ByteString.encodeUtf8(":path"); + public static final ByteString TARGET_SCHEME = ByteString.encodeUtf8(":scheme"); + public static final ByteString TARGET_AUTHORITY = ByteString.encodeUtf8(":authority"); // HTTP/2 + public static final ByteString TARGET_HOST = ByteString.encodeUtf8(":host"); // spdy/3 + public static final ByteString VERSION = ByteString.encodeUtf8(":version"); // spdy/3 + + /** Name in case-insensitive ASCII encoding. */ + public final ByteString name; + /** Value in UTF-8 encoding. */ + public final ByteString value; + final int hpackSize; + + + public Header(String name, String value) { + this(ByteString.encodeUtf8(name), ByteString.encodeUtf8(value)); + } + + public Header(ByteString name, String value) { + this(name, ByteString.encodeUtf8(value)); + } + + public Header(ByteString name, ByteString value) { + this.name = name; + this.value = value; + this.hpackSize = 32 + name.size() + value.size(); + } + + @Override public boolean equals(Object other) { + if (other instanceof Header) { + Header that = (Header) other; + return this.name.equals(that.name) + && this.value.equals(that.value); + } + return false; + } + + @Override public int hashCode() { + int result = 17; + result = 31 * result + name.hashCode(); + result = 31 * result + value.hashCode(); + return result; + } + + @Override public String toString() { + return String.format("%s: %s", name.utf8(), value.utf8()); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/HeadersMode.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/HeadersMode.java new file mode 100755 index 00000000..61a7e337 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/HeadersMode.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal.spdy; + +public enum HeadersMode { + SPDY_SYN_STREAM, + SPDY_REPLY, + SPDY_HEADERS, + HTTP_20_HEADERS; + + /** Returns true if it is an error these headers to create a new stream. */ + public boolean failIfStreamAbsent() { + return this == SPDY_REPLY || this == SPDY_HEADERS; + } + + /** Returns true if it is an error these headers to update an existing stream. */ + public boolean failIfStreamPresent() { + return this == SPDY_SYN_STREAM; + } + + /** + * Returns true if it is an error these headers to be the initial headers of a + * response. + */ + public boolean failIfHeadersAbsent() { + return this == SPDY_HEADERS; + } + + /** + * Returns true if it is an error these headers to be update existing headers + * of a response. + */ + public boolean failIfHeadersPresent() { + return this == SPDY_REPLY; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/HpackDraft08.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/HpackDraft08.java new file mode 100755 index 00000000..7e35f456 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/HpackDraft08.java @@ -0,0 +1,490 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal.spdy; + +import com.contentstack.okhttp.internal.BitArray; +import com.contentstack.okio.Buffer; +import com.contentstack.okio.BufferedSource; +import com.contentstack.okio.ByteString; +import com.contentstack.okio.Okio; +import com.contentstack.okio.Source; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Read and write HPACK v08. + * + * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-08 + * + * This implementation uses an array for the header table with a bitset for + * references. Dynamic entries are added to the array, starting in the last + * position moving forward. When the array fills, it is doubled. + */ +final class HpackDraft08 { + private static final int PREFIX_4_BITS = 0x0f; + private static final int PREFIX_6_BITS = 0x3f; + private static final int PREFIX_7_BITS = 0x7f; + + private static final Header[] STATIC_HEADER_TABLE = new Header[] { + new Header(Header.TARGET_AUTHORITY, ""), + new Header(Header.TARGET_METHOD, "GET"), + new Header(Header.TARGET_METHOD, "POST"), + new Header(Header.TARGET_PATH, "/"), + new Header(Header.TARGET_PATH, "/index.html"), + new Header(Header.TARGET_SCHEME, "http"), + new Header(Header.TARGET_SCHEME, "https"), + new Header(Header.RESPONSE_STATUS, "200"), + new Header(Header.RESPONSE_STATUS, "204"), + new Header(Header.RESPONSE_STATUS, "206"), + new Header(Header.RESPONSE_STATUS, "304"), + new Header(Header.RESPONSE_STATUS, "400"), + new Header(Header.RESPONSE_STATUS, "404"), + new Header(Header.RESPONSE_STATUS, "500"), + new Header("accept-charset", ""), + new Header("accept-encoding", "gzip, deflate"), + new Header("accept-language", ""), + new Header("accept-ranges", ""), + new Header("accept", ""), + new Header("access-control-allow-origin", ""), + new Header("age", ""), + new Header("allow", ""), + new Header("authorization", ""), + new Header("cache-control", ""), + new Header("content-disposition", ""), + new Header("content-encoding", ""), + new Header("content-language", ""), + new Header("content-length", ""), + new Header("content-location", ""), + new Header("content-range", ""), + new Header("content-type", ""), + new Header("cookie", ""), + new Header("date", ""), + new Header("etag", ""), + new Header("expect", ""), + new Header("expires", ""), + new Header("from", ""), + new Header("host", ""), + new Header("if-match", ""), + new Header("if-modified-since", ""), + new Header("if-none-match", ""), + new Header("if-range", ""), + new Header("if-unmodified-since", ""), + new Header("last-modified", ""), + new Header("link", ""), + new Header("location", ""), + new Header("max-forwards", ""), + new Header("proxy-authenticate", ""), + new Header("proxy-authorization", ""), + new Header("range", ""), + new Header("referer", ""), + new Header("refresh", ""), + new Header("retry-after", ""), + new Header("server", ""), + new Header("set-cookie", ""), + new Header("strict-transport-security", ""), + new Header("transfer-encoding", ""), + new Header("user-agent", ""), + new Header("vary", ""), + new Header("via", ""), + new Header("www-authenticate", "") + }; + + private HpackDraft08() { + } + + // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-08#section-3.2 + static final class Reader { + + private final List

emittedHeaders = new ArrayList
(); + private final BufferedSource source; + + private int maxHeaderTableByteCountSetting; + private int maxHeaderTableByteCount; + // Visible for testing. + Header[] headerTable = new Header[8]; + // Array is populated back to front, so new entries always have lowest index. + int nextHeaderIndex = headerTable.length - 1; + int headerCount = 0; + + /** + * Set bit positions indicate {@code headerTable[pos]} should be emitted. + */ + // Using a BitArray as it has left-shift operator. + BitArray referencedHeaders = new BitArray.FixedCapacity(); + + /** + * Set bit positions indicate {@code headerTable[pos]} was already emitted. + */ + BitArray emittedReferencedHeaders = new BitArray.FixedCapacity(); + int headerTableByteCount = 0; + + Reader(int maxHeaderTableByteCountSetting, Source source) { + this.maxHeaderTableByteCountSetting = maxHeaderTableByteCountSetting; + this.maxHeaderTableByteCount = maxHeaderTableByteCountSetting; + this.source = Okio.buffer(source); + } + + int maxHeaderTableByteCount() { + return maxHeaderTableByteCount; + } + + /** + * Called by the reader when the peer sent a new header table size setting. + * While this establishes the maximum header table size, the + * {@link #maxHeaderTableByteCount} set during processing may limit the + * table size to a smaller amount. + *

Evicts entries or clears the table as needed. + */ + void maxHeaderTableByteCountSetting(int newMaxHeaderTableByteCountSetting) { + this.maxHeaderTableByteCountSetting = newMaxHeaderTableByteCountSetting; + this.maxHeaderTableByteCount = maxHeaderTableByteCountSetting; + adjustHeaderTableByteCount(); + } + + private void adjustHeaderTableByteCount() { + if (maxHeaderTableByteCount < headerTableByteCount) { + if (maxHeaderTableByteCount == 0) { + clearHeaderTable(); + } else { + evictToRecoverBytes(headerTableByteCount - maxHeaderTableByteCount); + } + } + } + + private void clearHeaderTable() { + clearReferenceSet(); + Arrays.fill(headerTable, null); + nextHeaderIndex = headerTable.length - 1; + headerCount = 0; + headerTableByteCount = 0; + } + + /** Returns the count of entries evicted. */ + private int evictToRecoverBytes(int bytesToRecover) { + int entriesToEvict = 0; + if (bytesToRecover > 0) { + // determine how many headers need to be evicted. + for (int j = headerTable.length - 1; j >= nextHeaderIndex && bytesToRecover > 0; j--) { + bytesToRecover -= headerTable[j].hpackSize; + headerTableByteCount -= headerTable[j].hpackSize; + headerCount--; + entriesToEvict++; + } + referencedHeaders.shiftLeft(entriesToEvict); + emittedReferencedHeaders.shiftLeft(entriesToEvict); + System.arraycopy(headerTable, nextHeaderIndex + 1, headerTable, + nextHeaderIndex + 1 + entriesToEvict, headerCount); + nextHeaderIndex += entriesToEvict; + } + return entriesToEvict; + } + + /** + * Read {@code byteCount} bytes of headers from the source stream into the + * set of emitted headers. This implementation does not propagate the never + * indexed flag of a header. + */ + void readHeaders() throws IOException { + while (!source.exhausted()) { + int b = source.readByte() & 0xff; + if (b == 0x80) { // 10000000 + throw new IOException("index == 0"); + } else if ((b & 0x80) == 0x80) { // 1NNNNNNN + int index = readInt(b, PREFIX_7_BITS); + readIndexedHeader(index - 1); + } else if (b == 0x40) { // 01000000 + readLiteralHeaderWithIncrementalIndexingNewName(); + } else if ((b & 0x40) == 0x40) { // 01NNNNNN + int index = readInt(b, PREFIX_6_BITS); + readLiteralHeaderWithIncrementalIndexingIndexedName(index - 1); + } else if ((b & 0x20) == 0x20) { // 001NNNNN + if ((b & 0x10) == 0x10) { // 0011NNNN + if ((b & 0x0f) != 0) throw new IOException("Invalid header table state change " + b); + clearReferenceSet(); // 00110000 + } else { // 0010NNNN + maxHeaderTableByteCount = readInt(b, PREFIX_4_BITS); + if (maxHeaderTableByteCount < 0 + || maxHeaderTableByteCount > maxHeaderTableByteCountSetting) { + throw new IOException("Invalid header table byte count " + maxHeaderTableByteCount); + } + adjustHeaderTableByteCount(); + } + } else if (b == 0x10 || b == 0) { // 000?0000 - Ignore never indexed bit. + readLiteralHeaderWithoutIndexingNewName(); + } else { // 000?NNNN - Ignore never indexed bit. + int index = readInt(b, PREFIX_4_BITS); + readLiteralHeaderWithoutIndexingIndexedName(index - 1); + } + } + } + + private void clearReferenceSet() { + referencedHeaders.clear(); + emittedReferencedHeaders.clear(); + } + + void emitReferenceSet() { + for (int i = headerTable.length - 1; i != nextHeaderIndex; --i) { + if (referencedHeaders.get(i) && !emittedReferencedHeaders.get(i)) { + emittedHeaders.add(headerTable[i]); + } + } + } + + /** + * Returns all headers emitted since they were last cleared, then clears the + * emitted headers. + */ + List

getAndReset() { + List
result = new ArrayList
(emittedHeaders); + emittedHeaders.clear(); + emittedReferencedHeaders.clear(); + return result; + } + + private void readIndexedHeader(int index) throws IOException { + if (isStaticHeader(index)) { + index -= headerCount; + if (index > STATIC_HEADER_TABLE.length - 1) { + throw new IOException("Header index too large " + (index + 1)); + } + Header staticEntry = STATIC_HEADER_TABLE[index]; + if (maxHeaderTableByteCount == 0) { + emittedHeaders.add(staticEntry); + } else { + insertIntoHeaderTable(-1, staticEntry); + } + } else { + int headerTableIndex = headerTableIndex(index); + if (!referencedHeaders.get(headerTableIndex)) { // When re-referencing, emit immediately. + emittedHeaders.add(headerTable[headerTableIndex]); + emittedReferencedHeaders.set(headerTableIndex); + } + referencedHeaders.toggle(headerTableIndex); + } + } + + // referencedHeaders is relative to nextHeaderIndex + 1. + private int headerTableIndex(int index) { + return nextHeaderIndex + 1 + index; + } + + private void readLiteralHeaderWithoutIndexingIndexedName(int index) throws IOException { + ByteString name = getName(index); + ByteString value = readByteString(); + emittedHeaders.add(new Header(name, value)); + } + + private void readLiteralHeaderWithoutIndexingNewName() throws IOException { + ByteString name = checkLowercase(readByteString()); + ByteString value = readByteString(); + emittedHeaders.add(new Header(name, value)); + } + + private void readLiteralHeaderWithIncrementalIndexingIndexedName(int nameIndex) + throws IOException { + ByteString name = getName(nameIndex); + ByteString value = readByteString(); + insertIntoHeaderTable(-1, new Header(name, value)); + } + + private void readLiteralHeaderWithIncrementalIndexingNewName() throws IOException { + ByteString name = checkLowercase(readByteString()); + ByteString value = readByteString(); + insertIntoHeaderTable(-1, new Header(name, value)); + } + + private ByteString getName(int index) { + if (isStaticHeader(index)) { + return STATIC_HEADER_TABLE[index - headerCount].name; + } else { + return headerTable[headerTableIndex(index)].name; + } + } + + private boolean isStaticHeader(int index) { + return index >= headerCount; + } + + /** index == -1 when new. */ + private void insertIntoHeaderTable(int index, Header entry) { + int delta = entry.hpackSize; + if (index != -1) { // Index -1 == new header. + delta -= headerTable[headerTableIndex(index)].hpackSize; + } + + // if the new or replacement header is too big, drop all entries. + if (delta > maxHeaderTableByteCount) { + clearHeaderTable(); + // emit the large header to the callback. + emittedHeaders.add(entry); + return; + } + + // Evict headers to the required length. + int bytesToRecover = (headerTableByteCount + delta) - maxHeaderTableByteCount; + int entriesEvicted = evictToRecoverBytes(bytesToRecover); + + if (index == -1) { // Adding a value to the header table. + if (headerCount + 1 > headerTable.length) { // Need to grow the header table. + Header[] doubled = new Header[headerTable.length * 2]; + System.arraycopy(headerTable, 0, doubled, headerTable.length, headerTable.length); + if (doubled.length == 64) { + referencedHeaders = ((BitArray.FixedCapacity) referencedHeaders).toVariableCapacity(); + emittedReferencedHeaders = + ((BitArray.FixedCapacity) emittedReferencedHeaders).toVariableCapacity(); + } + referencedHeaders.shiftLeft(headerTable.length); + emittedReferencedHeaders.shiftLeft(headerTable.length); + nextHeaderIndex = headerTable.length - 1; + headerTable = doubled; + } + index = nextHeaderIndex--; + referencedHeaders.set(index); + headerTable[index] = entry; + headerCount++; + } else { // Replace value at same position. + index += headerTableIndex(index) + entriesEvicted; + referencedHeaders.set(index); + headerTable[index] = entry; + } + headerTableByteCount += delta; + } + + private int readByte() throws IOException { + return source.readByte() & 0xff; + } + + int readInt(int firstByte, int prefixMask) throws IOException { + int prefix = firstByte & prefixMask; + if (prefix < prefixMask) { + return prefix; // This was a single byte value. + } + + // This is a multibyte value. Read 7 bits at a time. + int result = prefixMask; + int shift = 0; + while (true) { + int b = readByte(); + if ((b & 0x80) != 0) { // Equivalent to (b >= 128) since b is in [0..255]. + result += (b & 0x7f) << shift; + shift += 7; + } else { + result += b << shift; // Last byte. + break; + } + } + return result; + } + + /** Reads a potentially Huffman encoded byte string. */ + ByteString readByteString() throws IOException { + int firstByte = readByte(); + boolean huffmanDecode = (firstByte & 0x80) == 0x80; // 1NNNNNNN + int length = readInt(firstByte, PREFIX_7_BITS); + + if (huffmanDecode) { + return ByteString.of(Huffman.get().decode(source.readByteArray(length))); + } else { + return source.readByteString(length); + } + } + } + + private static final Map NAME_TO_FIRST_INDEX = nameToFirstIndex(); + + private static Map nameToFirstIndex() { + Map result = new LinkedHashMap(STATIC_HEADER_TABLE.length); + for (int i = 0; i < STATIC_HEADER_TABLE.length; i++) { + if (!result.containsKey(STATIC_HEADER_TABLE[i].name)) { + result.put(STATIC_HEADER_TABLE[i].name, i); + } + } + return Collections.unmodifiableMap(result); + } + + static final class Writer { + private final Buffer out; + + Writer(Buffer out) { + this.out = out; + } + + /** This does not use "never indexed" semantics for sensitive headers. */ + // https://tools.ietf.org/html/draft-ietf-httpbis-header-compression-08#section-4.3.3 + void writeHeaders(List
headerBlock) throws IOException { + for (int i = 0, size = headerBlock.size(); i < size; i++) { + ByteString name = headerBlock.get(i).name.toAsciiLowercase(); + Integer staticIndex = NAME_TO_FIRST_INDEX.get(name); + if (staticIndex != null) { + // Literal Header Field without Indexing - Indexed Name. + writeInt(staticIndex + 1, PREFIX_4_BITS, 0); + writeByteString(headerBlock.get(i).value); + } else { + out.writeByte(0x00); // Literal Header without Indexing - New Name. + writeByteString(name); + writeByteString(headerBlock.get(i).value); + } + } + } + + // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-08#section-4.1.1 + void writeInt(int value, int prefixMask, int bits) throws IOException { + // Write the raw value for a single byte value. + if (value < prefixMask) { + out.writeByte(bits | value); + return; + } + + // Write the mask to start a multibyte value. + out.writeByte(bits | prefixMask); + value -= prefixMask; + + // Write 7 bits at a time 'til we're done. + while (value >= 0x80) { + int b = value & 0x7f; + out.writeByte(b | 0x80); + value >>>= 7; + } + out.writeByte(value); + } + + void writeByteString(ByteString data) throws IOException { + writeInt(data.size(), PREFIX_7_BITS, 0); + out.write(data); + } + } + + /** + * An HTTP/2 response cannot contain uppercase header characters and must + * be treated as malformed. + */ + private static ByteString checkLowercase(ByteString name) throws IOException { + for (int i = 0, length = name.size(); i < length; i++) { + byte c = name.getByte(i); + if (c >= 'A' && c <= 'Z') { + throw new IOException("PROTOCOL_ERROR response malformed: mixed case name: " + name.utf8()); + } + } + return name; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Http20Draft13.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Http20Draft13.java new file mode 100755 index 00000000..37cd4a1b --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Http20Draft13.java @@ -0,0 +1,745 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal.spdy; + +import com.contentstack.okhttp.Protocol; +import com.contentstack.okio.Buffer; +import com.contentstack.okio.BufferedSink; +import com.contentstack.okio.BufferedSource; +import com.contentstack.okio.ByteString; +import com.contentstack.okio.Source; +import com.contentstack.okio.Timeout; + +import java.io.IOException; +import java.util.List; +import java.util.logging.Logger; + +import static com.contentstack.okhttp.internal.spdy.Http20Draft13.FrameLogger.formatHeader; +import static com.contentstack.okio.ByteString.EMPTY; +import static java.lang.String.format; +import static java.util.logging.Level.FINE; + +/** + * Read and write HTTP/2 v13 frames. + *

http://tools.ietf.org/html/draft-ietf-httpbis-http2-13 + */ +public final class Http20Draft13 implements Variant { + private static final Logger logger = Logger.getLogger(Http20Draft13.class.getName()); + + @Override public Protocol getProtocol() { + return Protocol.HTTP_2; + } + + private static final ByteString CONNECTION_PREFACE + = ByteString.encodeUtf8("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"); + + static final int MAX_FRAME_SIZE = 0x3fff; // 16383 + + static final byte TYPE_DATA = 0x0; + static final byte TYPE_HEADERS = 0x1; + static final byte TYPE_PRIORITY = 0x2; + static final byte TYPE_RST_STREAM = 0x3; + static final byte TYPE_SETTINGS = 0x4; + static final byte TYPE_PUSH_PROMISE = 0x5; + static final byte TYPE_PING = 0x6; + static final byte TYPE_GOAWAY = 0x7; + static final byte TYPE_WINDOW_UPDATE = 0x8; + static final byte TYPE_CONTINUATION = 0x9; + + static final byte FLAG_NONE = 0x0; + static final byte FLAG_ACK = 0x1; // Used for settings and ping. + static final byte FLAG_END_STREAM = 0x1; // Used for headers and data. + static final byte FLAG_END_SEGMENT = 0x2; + static final byte FLAG_END_HEADERS = 0x4; // Used for headers and continuation. + static final byte FLAG_END_PUSH_PROMISE = 0x4; + static final byte FLAG_PADDED = 0x8; // Used for headers and data. + static final byte FLAG_PRIORITY = 0x20; // Used for headers. + static final byte FLAG_COMPRESSED = 0x20; // Used for data. + + /** + * Creates a frame reader with max header table size of 4096 and data frame + * compression disabled. + */ + @Override public FrameReader newReader(BufferedSource source, boolean client) { + return new Reader(source, 4096, client); + } + + @Override public FrameWriter newWriter(BufferedSink sink, boolean client) { + return new Writer(sink, client); + } + + @Override public int maxFrameSize() { + return MAX_FRAME_SIZE; + } + + static final class Reader implements FrameReader { + private final BufferedSource source; + private final ContinuationSource continuation; + private final boolean client; + + // Visible for testing. + final HpackDraft08.Reader hpackReader; + + Reader(BufferedSource source, int headerTableSize, boolean client) { + this.source = source; + this.client = client; + this.continuation = new ContinuationSource(this.source); + this.hpackReader = new HpackDraft08.Reader(headerTableSize, continuation); + } + + @Override public void readConnectionPreface() throws IOException { + if (client) return; // Nothing to read; servers doesn't send a connection preface! + ByteString connectionPreface = source.readByteString(CONNECTION_PREFACE.size()); + if (logger.isLoggable(FINE)) logger.fine(format("<< CONNECTION %s", connectionPreface.hex())); + if (!CONNECTION_PREFACE.equals(connectionPreface)) { + throw ioException("Expected a connection header but was %s", connectionPreface.utf8()); + } + } + + @Override public boolean nextFrame(Handler handler) throws IOException { + int w1; + int w2; + try { + w1 = source.readInt(); + w2 = source.readInt(); + } catch (IOException e) { + return false; // This might be a normal socket close. + } + + // boolean r = (w1 & 0xc0000000) != 0; // Reserved: Ignore first 2 bits. + short length = (short) ((w1 & 0x3fff0000) >> 16); // 14-bit unsigned == MAX_FRAME_SIZE + byte type = (byte) ((w1 & 0xff00) >> 8); + byte flags = (byte) (w1 & 0xff); + // boolean r = (w2 & 0x80000000) != 0; // Reserved: Ignore first bit. + int streamId = (w2 & 0x7fffffff); // 31-bit opaque identifier. + if (logger.isLoggable(FINE)) logger.fine(formatHeader(true, streamId, length, type, flags)); + + switch (type) { + case TYPE_DATA: + readData(handler, length, flags, streamId); + break; + + case TYPE_HEADERS: + readHeaders(handler, length, flags, streamId); + break; + + case TYPE_PRIORITY: + readPriority(handler, length, flags, streamId); + break; + + case TYPE_RST_STREAM: + readRstStream(handler, length, flags, streamId); + break; + + case TYPE_SETTINGS: + readSettings(handler, length, flags, streamId); + break; + + case TYPE_PUSH_PROMISE: + readPushPromise(handler, length, flags, streamId); + break; + + case TYPE_PING: + readPing(handler, length, flags, streamId); + break; + + case TYPE_GOAWAY: + readGoAway(handler, length, flags, streamId); + break; + + case TYPE_WINDOW_UPDATE: + readWindowUpdate(handler, length, flags, streamId); + break; + + default: + // Implementations MUST discard frames that have unknown or unsupported publishType. + source.skip(length); + } + return true; + } + + private void readHeaders(Handler handler, short length, byte flags, int streamId) + throws IOException { + if (streamId == 0) throw ioException("PROTOCOL_ERROR: TYPE_HEADERS streamId == 0"); + + boolean endStream = (flags & FLAG_END_STREAM) != 0; + + short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.readByte() & 0xff) : 0; + + if ((flags & FLAG_PRIORITY) != 0) { + readPriority(handler, streamId); + length -= 5; // account for above read. + } + + length = lengthWithoutPadding(length, flags, padding); + + List

headerBlock = readHeaderBlock(length, padding, flags, streamId); + + handler.headers(false, endStream, streamId, -1, headerBlock, HeadersMode.HTTP_20_HEADERS); + } + + private List
readHeaderBlock(short length, short padding, byte flags, int streamId) + throws IOException { + continuation.length = continuation.left = length; + continuation.padding = padding; + continuation.flags = flags; + continuation.streamId = streamId; + + hpackReader.readHeaders(); + hpackReader.emitReferenceSet(); + // http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-8.1.3 + return hpackReader.getAndReset(); + } + + private void readData(Handler handler, short length, byte flags, int streamId) + throws IOException { + + boolean inFinished = (flags & FLAG_END_STREAM) != 0; + boolean gzipped = (flags & FLAG_COMPRESSED) != 0; + if (gzipped) { + throw ioException("PROTOCOL_ERROR: FLAG_COMPRESSED without SETTINGS_COMPRESS_DATA"); + } + + short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.readByte() & 0xff) : 0; + length = lengthWithoutPadding(length, flags, padding); + + handler.data(inFinished, streamId, source, length); + source.skip(padding); + } + + private void readPriority(Handler handler, short length, byte flags, int streamId) + throws IOException { + if (length != 5) throw ioException("TYPE_PRIORITY length: %d != 5", length); + if (streamId == 0) throw ioException("TYPE_PRIORITY streamId == 0"); + readPriority(handler, streamId); + } + + private void readPriority(Handler handler, int streamId) throws IOException { + int w1 = source.readInt(); + boolean exclusive = (w1 & 0x80000000) != 0; + int streamDependency = (w1 & 0x7fffffff); + int weight = (source.readByte() & 0xff) + 1; + handler.priority(streamId, streamDependency, weight, exclusive); + } + + private void readRstStream(Handler handler, short length, byte flags, int streamId) + throws IOException { + if (length != 4) throw ioException("TYPE_RST_STREAM length: %d != 4", length); + if (streamId == 0) throw ioException("TYPE_RST_STREAM streamId == 0"); + int errorCodeInt = source.readInt(); + ErrorCode errorCode = ErrorCode.fromHttp2(errorCodeInt); + if (errorCode == null) { + throw ioException("TYPE_RST_STREAM unexpected error code: %d", errorCodeInt); + } + handler.rstStream(streamId, errorCode); + } + + private void readSettings(Handler handler, short length, byte flags, int streamId) + throws IOException { + if (streamId != 0) throw ioException("TYPE_SETTINGS streamId != 0"); + if ((flags & FLAG_ACK) != 0) { + if (length != 0) throw ioException("FRAME_SIZE_ERROR ack frame should be empty!"); + handler.ackSettings(); + return; + } + + if (length % 6 != 0) throw ioException("TYPE_SETTINGS length %% 6 != 0: %s", length); + Settings settings = new Settings(); + for (int i = 0; i < length; i += 6) { + short id = source.readShort(); + int value = source.readInt(); + + switch (id) { + case 1: // SETTINGS_HEADER_TABLE_SIZE + break; + case 2: // SETTINGS_ENABLE_PUSH + if (value != 0 && value != 1) { + throw ioException("PROTOCOL_ERROR SETTINGS_ENABLE_PUSH != 0 or 1"); + } + break; + case 3: // SETTINGS_MAX_CONCURRENT_STREAMS + id = 4; // Renumbered in draft 10. + break; + case 4: // SETTINGS_INITIAL_WINDOW_SIZE + id = 7; // Renumbered in draft 10. + if (value < 0) { + throw ioException("PROTOCOL_ERROR SETTINGS_INITIAL_WINDOW_SIZE > 2^31 - 1"); + } + break; + case 5: // SETTINGS_COMPRESS_DATA + break; + default: + throw ioException("PROTOCOL_ERROR invalid settings id: %s", id); + } + settings.set(id, 0, value); + } + handler.settings(false, settings); + if (settings.getHeaderTableSize() >= 0) { + hpackReader.maxHeaderTableByteCountSetting(settings.getHeaderTableSize()); + } + } + + private void readPushPromise(Handler handler, short length, byte flags, int streamId) + throws IOException { + if (streamId == 0) { + throw ioException("PROTOCOL_ERROR: TYPE_PUSH_PROMISE streamId == 0"); + } + short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.readByte() & 0xff) : 0; + int promisedStreamId = source.readInt() & 0x7fffffff; + length -= 4; // account for above read. + length = lengthWithoutPadding(length, flags, padding); + List
headerBlock = readHeaderBlock(length, padding, flags, streamId); + handler.pushPromise(streamId, promisedStreamId, headerBlock); + } + + private void readPing(Handler handler, short length, byte flags, int streamId) + throws IOException { + if (length != 8) throw ioException("TYPE_PING length != 8: %s", length); + if (streamId != 0) throw ioException("TYPE_PING streamId != 0"); + int payload1 = source.readInt(); + int payload2 = source.readInt(); + boolean ack = (flags & FLAG_ACK) != 0; + handler.ping(ack, payload1, payload2); + } + + private void readGoAway(Handler handler, short length, byte flags, int streamId) + throws IOException { + if (length < 8) throw ioException("TYPE_GOAWAY length < 8: %s", length); + if (streamId != 0) throw ioException("TYPE_GOAWAY streamId != 0"); + int lastStreamId = source.readInt(); + int errorCodeInt = source.readInt(); + int opaqueDataLength = length - 8; + ErrorCode errorCode = ErrorCode.fromHttp2(errorCodeInt); + if (errorCode == null) { + throw ioException("TYPE_GOAWAY unexpected error code: %d", errorCodeInt); + } + ByteString debugData = EMPTY; + if (opaqueDataLength > 0) { // Must read debug data in order to not corrupt the connection. + debugData = source.readByteString(opaqueDataLength); + } + handler.goAway(lastStreamId, errorCode, debugData); + } + + private void readWindowUpdate(Handler handler, short length, byte flags, int streamId) + throws IOException { + if (length != 4) throw ioException("TYPE_WINDOW_UPDATE length !=4: %s", length); + long increment = (source.readInt() & 0x7fffffffL); + if (increment == 0) throw ioException("windowSizeIncrement was 0", increment); + handler.windowUpdate(streamId, increment); + } + + @Override public void close() throws IOException { + source.close(); + } + } + + static final class Writer implements FrameWriter { + private final BufferedSink sink; + private final boolean client; + private final Buffer hpackBuffer; + private final HpackDraft08.Writer hpackWriter; + private boolean closed; + + Writer(BufferedSink sink, boolean client) { + this.sink = sink; + this.client = client; + this.hpackBuffer = new Buffer(); + this.hpackWriter = new HpackDraft08.Writer(hpackBuffer); + } + + @Override public synchronized void flush() throws IOException { + if (closed) throw new IOException("closed"); + sink.flush(); + } + + @Override public synchronized void ackSettings() throws IOException { + if (closed) throw new IOException("closed"); + int length = 0; + byte type = TYPE_SETTINGS; + byte flags = FLAG_ACK; + int streamId = 0; + frameHeader(streamId, length, type, flags); + sink.flush(); + } + + @Override public synchronized void connectionPreface() throws IOException { + if (closed) throw new IOException("closed"); + if (!client) return; // Nothing to write; servers don't send connection headers! + if (logger.isLoggable(FINE)) { + logger.fine(format(">> CONNECTION %s", CONNECTION_PREFACE.hex())); + } + sink.write(CONNECTION_PREFACE.toByteArray()); + sink.flush(); + } + + @Override public synchronized void synStream(boolean outFinished, boolean inFinished, + int streamId, int associatedStreamId, List
headerBlock) + throws IOException { + if (inFinished) throw new UnsupportedOperationException(); + if (closed) throw new IOException("closed"); + headers(outFinished, streamId, headerBlock); + } + + @Override public synchronized void synReply(boolean outFinished, int streamId, + List
headerBlock) throws IOException { + if (closed) throw new IOException("closed"); + headers(outFinished, streamId, headerBlock); + } + + @Override public synchronized void headers(int streamId, List
headerBlock) + throws IOException { + if (closed) throw new IOException("closed"); + headers(false, streamId, headerBlock); + } + + @Override public synchronized void pushPromise(int streamId, int promisedStreamId, + List
requestHeaders) throws IOException { + if (closed) throw new IOException("closed"); + if (hpackBuffer.size() != 0) throw new IllegalStateException(); + hpackWriter.writeHeaders(requestHeaders); + + long byteCount = hpackBuffer.size(); + int length = (int) Math.min(MAX_FRAME_SIZE - 4, byteCount); + byte type = TYPE_PUSH_PROMISE; + byte flags = byteCount == length ? FLAG_END_HEADERS : 0; + frameHeader(streamId, length + 4, type, flags); + sink.writeInt(promisedStreamId & 0x7fffffff); + sink.write(hpackBuffer, length); + + if (byteCount > length) writeContinuationFrames(streamId, byteCount - length); + } + + void headers(boolean outFinished, int streamId, List
headerBlock) throws IOException { + if (closed) throw new IOException("closed"); + if (hpackBuffer.size() != 0) throw new IllegalStateException(); + hpackWriter.writeHeaders(headerBlock); + + long byteCount = hpackBuffer.size(); + int length = (int) Math.min(MAX_FRAME_SIZE, byteCount); + byte type = TYPE_HEADERS; + byte flags = byteCount == length ? FLAG_END_HEADERS : 0; + if (outFinished) flags |= FLAG_END_STREAM; + frameHeader(streamId, length, type, flags); + sink.write(hpackBuffer, length); + + if (byteCount > length) writeContinuationFrames(streamId, byteCount - length); + } + + private void writeContinuationFrames(int streamId, long byteCount) throws IOException { + while (byteCount > 0) { + int length = (int) Math.min(MAX_FRAME_SIZE, byteCount); + byteCount -= length; + frameHeader(streamId, length, TYPE_CONTINUATION, byteCount == 0 ? FLAG_END_HEADERS : 0); + sink.write(hpackBuffer, length); + } + } + + @Override public synchronized void rstStream(int streamId, ErrorCode errorCode) + throws IOException { + if (closed) throw new IOException("closed"); + if (errorCode.spdyRstCode == -1) throw new IllegalArgumentException(); + + int length = 4; + byte type = TYPE_RST_STREAM; + byte flags = FLAG_NONE; + frameHeader(streamId, length, type, flags); + sink.writeInt(errorCode.httpCode); + sink.flush(); + } + + @Override public synchronized void data(boolean outFinished, int streamId, Buffer source) + throws IOException { + data(outFinished, streamId, source, (int) source.size()); + } + + @Override public synchronized void data(boolean outFinished, int streamId, Buffer source, + int byteCount) throws IOException { + if (closed) throw new IOException("closed"); + byte flags = FLAG_NONE; + if (outFinished) flags |= FLAG_END_STREAM; + dataFrame(streamId, flags, source, byteCount); + } + + void dataFrame(int streamId, byte flags, Buffer buffer, int byteCount) throws IOException { + byte type = TYPE_DATA; + frameHeader(streamId, byteCount, type, flags); + if (byteCount > 0) { + sink.write(buffer, byteCount); + } + } + + @Override public synchronized void settings(Settings settings) throws IOException { + if (closed) throw new IOException("closed"); + int length = settings.size() * 6; + byte type = TYPE_SETTINGS; + byte flags = FLAG_NONE; + int streamId = 0; + frameHeader(streamId, length, type, flags); + for (int i = 0; i < Settings.COUNT; i++) { + if (!settings.isSet(i)) continue; + int id = i; + if (id == 4) id = 3; // SETTINGS_MAX_CONCURRENT_STREAMS renumbered. + else if (id == 7) id = 4; // SETTINGS_INITIAL_WINDOW_SIZE renumbered. + sink.writeShort(id); + sink.writeInt(settings.get(i)); + } + sink.flush(); + } + + @Override public synchronized void ping(boolean ack, int payload1, int payload2) + throws IOException { + if (closed) throw new IOException("closed"); + int length = 8; + byte type = TYPE_PING; + byte flags = ack ? FLAG_ACK : FLAG_NONE; + int streamId = 0; + frameHeader(streamId, length, type, flags); + sink.writeInt(payload1); + sink.writeInt(payload2); + sink.flush(); + } + + @Override public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode, + byte[] debugData) throws IOException { + if (closed) throw new IOException("closed"); + if (errorCode.httpCode == -1) throw illegalArgument("errorCode.httpCode == -1"); + int length = 8 + debugData.length; + byte type = TYPE_GOAWAY; + byte flags = FLAG_NONE; + int streamId = 0; + frameHeader(streamId, length, type, flags); + sink.writeInt(lastGoodStreamId); + sink.writeInt(errorCode.httpCode); + if (debugData.length > 0) { + sink.write(debugData); + } + sink.flush(); + } + + @Override public synchronized void windowUpdate(int streamId, long windowSizeIncrement) + throws IOException { + if (closed) throw new IOException("closed"); + if (windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL) { + throw illegalArgument("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: %s", + windowSizeIncrement); + } + int length = 4; + byte type = TYPE_WINDOW_UPDATE; + byte flags = FLAG_NONE; + frameHeader(streamId, length, type, flags); + sink.writeInt((int) windowSizeIncrement); + sink.flush(); + } + + @Override public synchronized void close() throws IOException { + closed = true; + sink.close(); + } + + void frameHeader(int streamId, int length, byte type, byte flags) throws IOException { + if (logger.isLoggable(FINE)) logger.fine(formatHeader(false, streamId, length, type, flags)); + if (length > MAX_FRAME_SIZE) { + throw illegalArgument("FRAME_SIZE_ERROR length > %d: %d", MAX_FRAME_SIZE, length); + } + if ((streamId & 0x80000000) != 0) throw illegalArgument("reserved bit set: %s", streamId); + sink.writeInt((length & 0x3fff) << 16 | (type & 0xff) << 8 | (flags & 0xff)); + sink.writeInt(streamId & 0x7fffffff); + } + } + + private static IllegalArgumentException illegalArgument(String message, Object... args) { + throw new IllegalArgumentException(format(message, args)); + } + + private static IOException ioException(String message, Object... args) throws IOException { + throw new IOException(format(message, args)); + } + + /** + * Decompression of the header block occurs above the framing layer. This + * class lazily reads continuation frames as they are needed by {@link + * HpackDraft08.Reader#readHeaders()}. + */ + static final class ContinuationSource implements Source { + private final BufferedSource source; + + short length; + byte flags; + int streamId; + + short left; + short padding; + + public ContinuationSource(BufferedSource source) { + this.source = source; + } + + @Override public long read(Buffer sink, long byteCount) throws IOException { + while (left == 0) { + source.skip(padding); + padding = 0; + if ((flags & FLAG_END_HEADERS) != 0) return -1; + readContinuationHeader(); + } + + long read = source.read(sink, Math.min(byteCount, left)); + if (read == -1) return -1; + left -= read; + return read; + } + + @Override public Timeout timeout() { + return source.timeout(); + } + + @Override public void close() throws IOException { + } + + private void readContinuationHeader() throws IOException { + int previousStreamId = streamId; + int w1 = source.readInt(); + int w2 = source.readInt(); + length = left = (short) ((w1 & 0x3fff0000) >> 16); + byte type = (byte) ((w1 & 0xff00) >> 8); + flags = (byte) (w1 & 0xff); + if (logger.isLoggable(FINE)) logger.fine(formatHeader(true, streamId, length, type, flags)); + streamId = (w2 & 0x7fffffff); + if (type != TYPE_CONTINUATION) throw ioException("%s != TYPE_CONTINUATION", type); + if (streamId != previousStreamId) throw ioException("TYPE_CONTINUATION streamId changed"); + } + } + + private static short lengthWithoutPadding(short length, byte flags, short padding) + throws IOException { + if ((flags & FLAG_PADDED) != 0) length--; // Account for reading the padding length. + if (padding > length) { + throw ioException("PROTOCOL_ERROR padding %s > remaining length %s", padding, length); + } + return (short) (length - padding); + } + + /** + * Logs a human-readable representation of HTTP/2 frame headers. + * + *

The format is: + * + *

+   *   direction streamID length type flags
+   * 
+ * Where direction is {@code <<} for inbound and {@code >>} for outbound. + * + *

For example, the following would indicate a HEAD request sent from + * the client. + *

+   * {@code
+   *   << 0x0000000f    12 HEADERS       END_HEADERS|END_STREAM
+   * }
+   * 
+ */ + static final class FrameLogger { + + static String formatHeader(boolean inbound, int streamId, int length, byte type, byte flags) { + String formattedType = type < TYPES.length ? TYPES[type] : format("0x%02x", type); + String formattedFlags = formatFlags(type, flags); + return format("%s 0x%08x %5d %-13s %s", inbound ? "<<" : ">>", streamId, length, + formattedType, formattedFlags); + } + + /** + * Looks up valid string representing flags from the table. Invalid + * combinations are represented in binary. + */ + // Visible for testing. + static String formatFlags(byte type, byte flags) { + if (flags == 0) return ""; + switch (type) { // Special case publishType that have 0 or 1 flag. + case TYPE_SETTINGS: + case TYPE_PING: + return flags == FLAG_ACK ? "ACK" : BINARY[flags]; + case TYPE_PRIORITY: + case TYPE_RST_STREAM: + case TYPE_GOAWAY: + case TYPE_WINDOW_UPDATE: + return BINARY[flags]; + } + String result = flags < FLAGS.length ? FLAGS[flags] : BINARY[flags]; + // Special case publishType that have overlap flag values. + if (type == TYPE_PUSH_PROMISE && (flags & FLAG_END_PUSH_PROMISE) != 0) { + return result.replace("HEADERS", "PUSH_PROMISE"); + } else if (type == TYPE_DATA && (flags & FLAG_COMPRESSED) != 0) { + return result.replace("PRIORITY", "COMPRESSED"); + } + return result; + } + + /** Lookup table for valid frame publishType. */ + private static final String[] TYPES = new String[] { + "DATA", + "HEADERS", + "PRIORITY", + "RST_STREAM", + "SETTINGS", + "PUSH_PROMISE", + "PING", + "GOAWAY", + "WINDOW_UPDATE", + "CONTINUATION" + }; + + /** + * Lookup table for valid flags for DATA, HEADERS, CONTINUATION. Invalid + * combinations are represented in binary. + */ + private static final String[] FLAGS = new String[0x40]; // Highest bit flag is 0x20. + private static final String[] BINARY = new String[256]; + + static { + for (int i = 0; i < BINARY.length; i++) { + BINARY[i] = format("%8s", Integer.toBinaryString(i)).replace(' ', '0'); + } + + FLAGS[FLAG_NONE] = ""; + FLAGS[FLAG_END_STREAM] = "END_STREAM"; + FLAGS[FLAG_END_SEGMENT] = "END_SEGMENT"; + FLAGS[FLAG_END_STREAM | FLAG_END_SEGMENT] = "END_STREAM|END_SEGMENT"; + int[] prefixFlags = + new int[] {FLAG_END_STREAM, FLAG_END_SEGMENT, FLAG_END_SEGMENT | FLAG_END_STREAM}; + + FLAGS[FLAG_PADDED] = "PADDED"; + for (int prefixFlag : prefixFlags) { + FLAGS[prefixFlag | FLAG_PADDED] = FLAGS[prefixFlag] + "|PADDED"; + } + + FLAGS[FLAG_END_HEADERS] = "END_HEADERS"; // Same as END_PUSH_PROMISE. + FLAGS[FLAG_PRIORITY] = "PRIORITY"; // Same as FLAG_COMPRESSED. + FLAGS[FLAG_END_HEADERS | FLAG_PRIORITY] = "END_HEADERS|PRIORITY"; // Only valid on HEADERS. + int[] frameFlags = + new int[] {FLAG_END_HEADERS, FLAG_PRIORITY, FLAG_END_HEADERS | FLAG_PRIORITY}; + + for (int frameFlag : frameFlags) { + for (int prefixFlag : prefixFlags) { + FLAGS[prefixFlag | frameFlag] = FLAGS[prefixFlag] + '|' + FLAGS[frameFlag]; + FLAGS[prefixFlag | frameFlag | FLAG_PADDED] = + FLAGS[prefixFlag] + '|' + FLAGS[frameFlag] + "|PADDED"; + } + } + + for (int i = 0; i < FLAGS.length; i++) { // Fill in holes with binary representation. + if (FLAGS[i] == null) FLAGS[i] = BINARY[i]; + } + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Huffman.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Huffman.java new file mode 100755 index 00000000..fed14930 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Huffman.java @@ -0,0 +1,225 @@ +/* + * Copyright 2013 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal.spdy; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * This class was originally composed from the following classes in + * Twitter Hpack. + *
    + *
  • {@code com.twitter.hpack.HuffmanEncoder}
  • + *
  • {@code com.twitter.hpack.HuffmanDecoder}
  • + *
  • {@code com.twitter.hpack.HpackUtil}
  • + *
+ */ +class Huffman { + + // Appendix C: Huffman Codes + // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-08#appendix-C + private static final int[] CODES = { + 0x1ff8, 0x7fffd8, 0xfffffe2, 0xfffffe3, 0xfffffe4, 0xfffffe5, 0xfffffe6, 0xfffffe7, 0xfffffe8, + 0xffffea, 0x3ffffffc, 0xfffffe9, 0xfffffea, 0x3ffffffd, 0xfffffeb, 0xfffffec, 0xfffffed, + 0xfffffee, 0xfffffef, 0xffffff0, 0xffffff1, 0xffffff2, 0x3ffffffe, 0xffffff3, 0xffffff4, + 0xffffff5, 0xffffff6, 0xffffff7, 0xffffff8, 0xffffff9, 0xffffffa, 0xffffffb, 0x14, 0x3f8, + 0x3f9, 0xffa, 0x1ff9, 0x15, 0xf8, 0x7fa, 0x3fa, 0x3fb, 0xf9, 0x7fb, 0xfa, 0x16, 0x17, 0x18, + 0x0, 0x1, 0x2, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x5c, 0xfb, 0x7ffc, 0x20, 0xffb, + 0x3fc, 0x1ffa, 0x21, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, + 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0xfc, 0x73, 0xfd, 0x1ffb, 0x7fff0, + 0x1ffc, 0x3ffc, 0x22, 0x7ffd, 0x3, 0x23, 0x4, 0x24, 0x5, 0x25, 0x26, 0x27, 0x6, 0x74, 0x75, + 0x28, 0x29, 0x2a, 0x7, 0x2b, 0x76, 0x2c, 0x8, 0x9, 0x2d, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7ffe, + 0x7fc, 0x3ffd, 0x1ffd, 0xffffffc, 0xfffe6, 0x3fffd2, 0xfffe7, 0xfffe8, 0x3fffd3, 0x3fffd4, + 0x3fffd5, 0x7fffd9, 0x3fffd6, 0x7fffda, 0x7fffdb, 0x7fffdc, 0x7fffdd, 0x7fffde, 0xffffeb, + 0x7fffdf, 0xffffec, 0xffffed, 0x3fffd7, 0x7fffe0, 0xffffee, 0x7fffe1, 0x7fffe2, 0x7fffe3, + 0x7fffe4, 0x1fffdc, 0x3fffd8, 0x7fffe5, 0x3fffd9, 0x7fffe6, 0x7fffe7, 0xffffef, 0x3fffda, + 0x1fffdd, 0xfffe9, 0x3fffdb, 0x3fffdc, 0x7fffe8, 0x7fffe9, 0x1fffde, 0x7fffea, 0x3fffdd, + 0x3fffde, 0xfffff0, 0x1fffdf, 0x3fffdf, 0x7fffeb, 0x7fffec, 0x1fffe0, 0x1fffe1, 0x3fffe0, + 0x1fffe2, 0x7fffed, 0x3fffe1, 0x7fffee, 0x7fffef, 0xfffea, 0x3fffe2, 0x3fffe3, 0x3fffe4, + 0x7ffff0, 0x3fffe5, 0x3fffe6, 0x7ffff1, 0x3ffffe0, 0x3ffffe1, 0xfffeb, 0x7fff1, 0x3fffe7, + 0x7ffff2, 0x3fffe8, 0x1ffffec, 0x3ffffe2, 0x3ffffe3, 0x3ffffe4, 0x7ffffde, 0x7ffffdf, + 0x3ffffe5, 0xfffff1, 0x1ffffed, 0x7fff2, 0x1fffe3, 0x3ffffe6, 0x7ffffe0, 0x7ffffe1, 0x3ffffe7, + 0x7ffffe2, 0xfffff2, 0x1fffe4, 0x1fffe5, 0x3ffffe8, 0x3ffffe9, 0xffffffd, 0x7ffffe3, + 0x7ffffe4, 0x7ffffe5, 0xfffec, 0xfffff3, 0xfffed, 0x1fffe6, 0x3fffe9, 0x1fffe7, 0x1fffe8, + 0x7ffff3, 0x3fffea, 0x3fffeb, 0x1ffffee, 0x1ffffef, 0xfffff4, 0xfffff5, 0x3ffffea, 0x7ffff4, + 0x3ffffeb, 0x7ffffe6, 0x3ffffec, 0x3ffffed, 0x7ffffe7, 0x7ffffe8, 0x7ffffe9, 0x7ffffea, + 0x7ffffeb, 0xffffffe, 0x7ffffec, 0x7ffffed, 0x7ffffee, 0x7ffffef, 0x7fffff0, 0x3ffffee + }; + + private static final byte[] CODE_LENGTHS = { + 13, 23, 28, 28, 28, 28, 28, 28, 28, 24, 30, 28, 28, 30, 28, 28, 28, 28, 28, 28, 28, 28, 30, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 6, 10, 10, 12, 13, 6, 8, 11, 10, 10, 8, 11, 8, 6, 6, 6, 5, + 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 8, 15, 6, 12, 10, 13, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 8, 13, 19, 13, 14, 6, 15, 5, 6, 5, 6, 5, 6, 6, 6, 5, 7, 7, 6, + 6, 6, 5, 6, 7, 6, 5, 5, 6, 7, 7, 7, 7, 7, 15, 11, 14, 13, 28, 20, 22, 20, 20, 22, 22, 22, 23, + 22, 23, 23, 23, 23, 23, 24, 23, 24, 24, 22, 23, 24, 23, 23, 23, 23, 21, 22, 23, 22, 23, 23, + 24, 22, 21, 20, 22, 22, 23, 23, 21, 23, 22, 22, 24, 21, 22, 23, 23, 21, 21, 22, 21, 23, 22, + 23, 23, 20, 22, 22, 22, 23, 22, 22, 23, 26, 26, 20, 19, 22, 23, 22, 25, 26, 26, 26, 27, 27, + 26, 24, 25, 19, 21, 26, 27, 27, 26, 27, 24, 21, 21, 26, 26, 28, 27, 27, 27, 20, 24, 20, 21, + 22, 21, 21, 23, 22, 22, 25, 25, 24, 24, 26, 23, 26, 27, 26, 26, 27, 27, 27, 27, 27, 28, 27, + 27, 27, 27, 27, 26 + }; + + private static final Huffman INSTANCE = new Huffman(); + + public static Huffman get() { + return INSTANCE; + } + + private final Node root = new Node(); + + private Huffman() { + buildTree(); + } + + void encode(byte[] data, OutputStream out) throws IOException { + long current = 0; + int n = 0; + + for (int i = 0; i < data.length; i++) { + int b = data[i] & 0xFF; + int code = CODES[b]; + int nbits = CODE_LENGTHS[b]; + + current <<= nbits; + current |= code; + n += nbits; + + while (n >= 8) { + n -= 8; + out.write(((int) (current >> n))); + } + } + + if (n > 0) { + current <<= (8 - n); + current |= (0xFF >>> n); + out.write((int) current); + } + } + + int encodedLength(byte[] bytes) { + long len = 0; + + for (int i = 0; i < bytes.length; i++) { + int b = bytes[i] & 0xFF; + len += CODE_LENGTHS[b]; + } + + return (int) ((len + 7) >> 3); + } + + byte[] decode(byte[] buf) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Node node = root; + int current = 0; + int nbits = 0; + for (int i = 0; i < buf.length; i++) { + int b = buf[i] & 0xFF; + current = (current << 8) | b; + nbits += 8; + while (nbits >= 8) { + int c = (current >>> (nbits - 8)) & 0xFF; + node = node.children[c]; + if (node.children == null) { + // terminal node + baos.write(node.symbol); + nbits -= node.terminalBits; + node = root; + } else { + // non-terminal node + nbits -= 8; + } + } + } + + while (nbits > 0) { + int c = (current << (8 - nbits)) & 0xFF; + node = node.children[c]; + if (node.children != null || node.terminalBits > nbits) { + break; + } + baos.write(node.symbol); + nbits -= node.terminalBits; + node = root; + } + + return baos.toByteArray(); + } + + private void buildTree() { + for (int i = 0; i < CODE_LENGTHS.length; i++) { + addCode(i, CODES[i], CODE_LENGTHS[i]); + } + } + + private void addCode(int sym, int code, byte len) { + Node terminal = new Node(sym, len); + + Node current = root; + while (len > 8) { + len -= 8; + int i = ((code >>> len) & 0xFF); + if (current.children == null) { + throw new IllegalStateException("invalid dictionary: prefix not unique"); + } + if (current.children[i] == null) { + current.children[i] = new Node(); + } + current = current.children[i]; + } + + int shift = 8 - len; + int start = (code << shift) & 0xFF; + int end = 1 << shift; + for (int i = start; i < start + end; i++) { + current.children[i] = terminal; + } + } + + private static final class Node { + + // Null if terminal. + private final Node[] children; + + // Terminal nodes have a symbol. + private final int symbol; + + // Number of bits represented in the terminal node. + private final int terminalBits; + + /** Construct an internal node. */ + Node() { + this.children = new Node[256]; + this.symbol = 0; // Not read. + this.terminalBits = 0; // Not read. + } + + /** + * Construct a terminal node. + * + * @param symbol symbol the node represents + * @param bits length of Huffman code in bits + */ + Node(int symbol, int bits) { + this.children = null; + this.symbol = symbol; + int b = bits & 0x07; + this.terminalBits = b == 0 ? 8 : b; + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/IncomingStreamHandler.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/IncomingStreamHandler.java new file mode 100755 index 00000000..275ed9d1 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/IncomingStreamHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contentstack.okhttp.internal.spdy; + +import java.io.IOException; + +/** Listener to be notified when a connected peer creates a new stream. */ +public interface IncomingStreamHandler { + IncomingStreamHandler REFUSE_INCOMING_STREAMS = new IncomingStreamHandler() { + @Override public void receive(SpdyStream stream) throws IOException { + stream.close(ErrorCode.REFUSED_STREAM); + } + }; + + /** + * Handle a new stream from this connection's peer. Implementations should + * respond by either {@link SpdyStream#reply replying to the stream} or + * {@link SpdyStream#close closing it}. This response does not need to be + * synchronous. + */ + void receive(SpdyStream stream) throws IOException; +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/NameValueBlockReader.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/NameValueBlockReader.java new file mode 100755 index 00000000..4ad63f36 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/NameValueBlockReader.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal.spdy; + +import com.contentstack.okio.Buffer; +import com.contentstack.okio.BufferedSource; +import com.contentstack.okio.ByteString; +import com.contentstack.okio.ForwardingSource; +import com.contentstack.okio.InflaterSource; +import com.contentstack.okio.Okio; +import com.contentstack.okio.Source; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +/** + * Reads a SPDY/3 Name/Value header block. This class is made complicated by the + * requirement that we're strict with which bytes we put in the compressed bytes + * buffer. We need to put all compressed bytes into that buffer -- but no other + * bytes. + */ +class NameValueBlockReader { + /** This source transforms compressed bytes into uncompressed bytes. */ + private final InflaterSource inflaterSource; + + /** + * How many compressed bytes must be read into inflaterSource before + * {@link #readNameValueBlock} returns. + */ + private int compressedLimit; + + /** This source holds inflated bytes. */ + private final BufferedSource source; + + public NameValueBlockReader(BufferedSource source) { + // Limit the inflater input stream to only those bytes in the Name/Value + // block. We cut the inflater off at its source because we can't predict the + // ratio of compressed bytes to uncompressed bytes. + Source throttleSource = new ForwardingSource(source) { + @Override public long read(Buffer sink, long byteCount) throws IOException { + if (compressedLimit == 0) return -1; // Out of data for the current block. + long read = super.read(sink, Math.min(byteCount, compressedLimit)); + if (read == -1) return -1; + compressedLimit -= read; + return read; + } + }; + + // Subclass inflater to install a dictionary when it's needed. + Inflater inflater = new Inflater() { + @Override public int inflate(byte[] buffer, int offset, int count) + throws DataFormatException { + int result = super.inflate(buffer, offset, count); + if (result == 0 && needsDictionary()) { + setDictionary(Spdy3.DICTIONARY); + result = super.inflate(buffer, offset, count); + } + return result; + } + }; + + this.inflaterSource = new InflaterSource(throttleSource, inflater); + this.source = Okio.buffer(inflaterSource); + } + + public List
readNameValueBlock(int length) throws IOException { + this.compressedLimit += length; + + int numberOfPairs = source.readInt(); + if (numberOfPairs < 0) throw new IOException("numberOfPairs < 0: " + numberOfPairs); + if (numberOfPairs > 1024) throw new IOException("numberOfPairs > 1024: " + numberOfPairs); + + List
entries = new ArrayList
(numberOfPairs); + for (int i = 0; i < numberOfPairs; i++) { + ByteString name = readByteString().toAsciiLowercase(); + ByteString values = readByteString(); + if (name.size() == 0) throw new IOException("name.size == 0"); + entries.add(new Header(name, values)); + } + + doneReading(); + return entries; + } + + private ByteString readByteString() throws IOException { + int length = source.readInt(); + return source.readByteString(length); + } + + private void doneReading() throws IOException { + // Move any outstanding unread bytes into the inflater. One side-effect of + // deflate compression is that sometimes there are bytes remaining in the + // stream after we've consumed all of the content. + if (compressedLimit > 0) { + inflaterSource.refill(); + if (compressedLimit != 0) throw new IOException("compressedLimit > 0: " + compressedLimit); + } + } + + public void close() throws IOException { + source.close(); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Ping.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Ping.java new file mode 100755 index 00000000..0c7fbd0e --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Ping.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2012 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal.spdy; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * A locally-originated ping. + */ +public final class Ping { + private final CountDownLatch latch = new CountDownLatch(1); + private long sent = -1; + private long received = -1; + + Ping() { + } + + void send() { + if (sent != -1) throw new IllegalStateException(); + sent = System.nanoTime(); + } + + void receive() { + if (received != -1 || sent == -1) throw new IllegalStateException(); + received = System.nanoTime(); + latch.countDown(); + } + + void cancel() { + if (received != -1 || sent == -1) throw new IllegalStateException(); + received = sent - 1; + latch.countDown(); + } + + /** + * Returns the round trip time for this ping in nanoseconds, waiting for the + * response to arrive if necessary. Returns -1 if the response was + * canceled. + */ + public long roundTripTime() throws InterruptedException { + latch.await(); + return received - sent; + } + + /** + * Returns the round trip time for this ping in nanoseconds, or -1 if the + * response was canceled, or -2 if the timeout elapsed before the round + * trip completed. + */ + public long roundTripTime(long timeout, TimeUnit unit) throws InterruptedException { + if (latch.await(timeout, unit)) { + return received - sent; + } else { + return -2; + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/PushObserver.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/PushObserver.java new file mode 100755 index 00000000..5b30392c --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/PushObserver.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal.spdy; + +import com.contentstack.okhttp.Protocol; +import com.contentstack.okio.BufferedSource; + +import java.io.IOException; +import java.util.List; + +/** + * {@link Protocol#HTTP_2 HTTP/2} only. + * Processes server-initiated HTTP requests on the client. Implementations must + * quickly dispatch callbacks to avoid creating a bottleneck. + * + *

While {@link #onReset} may occur at any time, the following callbacks are + * expected in order, correlated by stream ID. + *

    + *
  • {@link #onRequest}
  • + *
  • {@link #onHeaders} (unless canceled)
  • + *
  • {@link #onData} (optional sequence of data frames)
  • + *
+ * + *

As a stream ID is scoped to a single HTTP/2 connection, implementations + * which target multiple connections should expect repetition of stream IDs. + * + *

Return true to request cancellation of a pushed stream. Note that this + * does not guarantee future frames won't arrive on the stream ID. + */ +public interface PushObserver { + /** + * Describes the request that the server intends to push a response for. + * + * @param streamId server-initiated stream ID: an even number. + * @param requestHeaders minimally includes {@code :method}, {@code :scheme}, + * {@code :authority}, and (@code :path}. + */ + boolean onRequest(int streamId, List

requestHeaders); + + /** + * The response headers corresponding to a pushed request. When {@code last} + * is true, there are no data frames to follow. + * + * @param streamId server-initiated stream ID: an even number. + * @param responseHeaders minimally includes {@code :status}. + * @param last when true, there is no response data. + */ + boolean onHeaders(int streamId, List
responseHeaders, boolean last); + + /** + * A chunk of response data corresponding to a pushed request. This data + * must either be read or skipped. + * + * @param streamId server-initiated stream ID: an even number. + * @param source location of data corresponding with this stream ID. + * @param byteCount number of bytes to read or skip from the source. + * @param last when true, there are no data frames to follow. + */ + boolean onData(int streamId, BufferedSource source, int byteCount, boolean last) + throws IOException; + + /** Indicates the reason why this stream was canceled. */ + void onReset(int streamId, ErrorCode errorCode); + + PushObserver CANCEL = new PushObserver() { + + @Override public boolean onRequest(int streamId, List
requestHeaders) { + return true; + } + + @Override public boolean onHeaders(int streamId, List
responseHeaders, boolean last) { + return true; + } + + @Override public boolean onData(int streamId, BufferedSource source, int byteCount, + boolean last) throws IOException { + source.skip(byteCount); + return true; + } + + @Override public void onReset(int streamId, ErrorCode errorCode) { + } + }; +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Settings.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Settings.java new file mode 100755 index 00000000..a35a54c9 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Settings.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2012 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal.spdy; + +import java.util.Arrays; + +/** + * Settings describe characteristics of the sending peer, which are used by the receiving peer. + * Settings are {@link SpdyConnection connection} scoped. + */ +public final class Settings { + /** + * From the SPDY/3 and HTTP/2 specs, the default initial window size for all + * streams is 64 KiB. (Chrome 25 uses 10 MiB). + */ + static final int DEFAULT_INITIAL_WINDOW_SIZE = 64 * 1024; + + /** Peer request to clear durable settings. */ + static final int FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS = 0x1; + + /** Sent by servers only. The peer requests this setting persisted for future connections. */ + static final int PERSIST_VALUE = 0x1; + /** Sent by clients only. The client is reminding the server of a persisted value. */ + static final int PERSISTED = 0x2; + + /** spdy/3: Sender's estimate of max incoming kbps. */ + static final int UPLOAD_BANDWIDTH = 1; + /** HTTP/2: Size in bytes of the table used to decode the sender's header blocks. */ + static final int HEADER_TABLE_SIZE = 1; + /** spdy/3: Sender's estimate of max outgoing kbps. */ + static final int DOWNLOAD_BANDWIDTH = 2; + /** HTTP/2: The peer must not send a PUSH_PROMISE frame when this is 0. */ + static final int ENABLE_PUSH = 2; + /** spdy/3: Sender's estimate of millis between sending a request and receiving a response. */ + static final int ROUND_TRIP_TIME = 3; + /** Sender's maximum number of concurrent streams. */ + static final int MAX_CONCURRENT_STREAMS = 4; + /** spdy/3: Current CWND in Packets. */ + static final int CURRENT_CWND = 5; + /** spdy/3: Retransmission rate. Percentage */ + static final int DOWNLOAD_RETRANS_RATE = 6; + /** Window size in bytes. */ + static final int INITIAL_WINDOW_SIZE = 7; + /** spdy/3: Window size in bytes. */ + static final int CLIENT_CERTIFICATE_VECTOR_SIZE = 8; + /** Flow control options. */ + static final int FLOW_CONTROL_OPTIONS = 10; + + /** Total number of settings. */ + static final int COUNT = 10; + + /** If set, flow control is disabled for streams directed to the sender of these settings. */ + static final int FLOW_CONTROL_OPTIONS_DISABLED = 0x1; + + /** Bitfield of which flags that values. */ + private int set; + + /** Bitfield of flags that have {@link #PERSIST_VALUE}. */ + private int persistValue; + + /** Bitfield of flags that have {@link #PERSISTED}. */ + private int persisted; + + /** Flag values. */ + private final int[] values = new int[COUNT]; + + void clear() { + set = persistValue = persisted = 0; + Arrays.fill(values, 0); + } + + Settings set(int id, int idFlags, int value) { + if (id >= values.length) { + return this; // Discard unknown settings. + } + + int bit = 1 << id; + set |= bit; + if ((idFlags & PERSIST_VALUE) != 0) { + persistValue |= bit; + } else { + persistValue &= ~bit; + } + if ((idFlags & PERSISTED) != 0) { + persisted |= bit; + } else { + persisted &= ~bit; + } + + values[id] = value; + return this; + } + + /** Returns true if a value has been assigned for the setting {@code id}. */ + boolean isSet(int id) { + int bit = 1 << id; + return (set & bit) != 0; + } + + /** Returns the value for the setting {@code id}, or 0 if unset. */ + int get(int id) { + return values[id]; + } + + /** Returns the flags for the setting {@code id}, or 0 if unset. */ + int flags(int id) { + int result = 0; + if (isPersisted(id)) result |= Settings.PERSISTED; + if (persistValue(id)) result |= Settings.PERSIST_VALUE; + return result; + } + + /** Returns the number of settings that have values assigned. */ + int size() { + return Integer.bitCount(set); + } + + /** spdy/3 only. */ + int getUploadBandwidth(int defaultValue) { + int bit = 1 << UPLOAD_BANDWIDTH; + return (bit & set) != 0 ? values[UPLOAD_BANDWIDTH] : defaultValue; + } + + /** HTTP/2 only. Returns -1 if unset. */ + int getHeaderTableSize() { + int bit = 1 << HEADER_TABLE_SIZE; + return (bit & set) != 0 ? values[HEADER_TABLE_SIZE] : -1; + } + + /** spdy/3 only. */ + int getDownloadBandwidth(int defaultValue) { + int bit = 1 << DOWNLOAD_BANDWIDTH; + return (bit & set) != 0 ? values[DOWNLOAD_BANDWIDTH] : defaultValue; + } + + /** HTTP/2 only. */ + + boolean getEnablePush(boolean defaultValue) { + int bit = 1 << ENABLE_PUSH; + return ((bit & set) != 0 ? values[ENABLE_PUSH] : defaultValue ? 1 : 0) == 1; + } + + /** spdy/3 only. */ + int getRoundTripTime(int defaultValue) { + int bit = 1 << ROUND_TRIP_TIME; + return (bit & set) != 0 ? values[ROUND_TRIP_TIME] : defaultValue; + } + + + int getMaxConcurrentStreams(int defaultValue) { + int bit = 1 << MAX_CONCURRENT_STREAMS; + return (bit & set) != 0 ? values[MAX_CONCURRENT_STREAMS] : defaultValue; + } + + /** spdy/3 only. */ + int getCurrentCwnd(int defaultValue) { + int bit = 1 << CURRENT_CWND; + return (bit & set) != 0 ? values[CURRENT_CWND] : defaultValue; + } + + /** spdy/3 only. */ + int getDownloadRetransRate(int defaultValue) { + int bit = 1 << DOWNLOAD_RETRANS_RATE; + return (bit & set) != 0 ? values[DOWNLOAD_RETRANS_RATE] : defaultValue; + } + + int getInitialWindowSize(int defaultValue) { + int bit = 1 << INITIAL_WINDOW_SIZE; + return (bit & set) != 0 ? values[INITIAL_WINDOW_SIZE] : defaultValue; + } + + /** spdy/3 only. */ + int getClientCertificateVectorSize(int defaultValue) { + int bit = 1 << CLIENT_CERTIFICATE_VECTOR_SIZE; + return (bit & set) != 0 ? values[CLIENT_CERTIFICATE_VECTOR_SIZE] : defaultValue; + } + + boolean isFlowControlDisabled() { + int bit = 1 << FLOW_CONTROL_OPTIONS; + int value = (bit & set) != 0 ? values[FLOW_CONTROL_OPTIONS] : 0; + return (value & FLOW_CONTROL_OPTIONS_DISABLED) != 0; + } + + /** + * Returns true if this user agent should use this setting in future spdy/3 + * connections to the same host. + */ + boolean persistValue(int id) { + int bit = 1 << id; + return (persistValue & bit) != 0; + } + + /** Returns true if this setting was persisted. */ + boolean isPersisted(int id) { + int bit = 1 << id; + return (persisted & bit) != 0; + } + + /** + * Writes {@code other} into this. If any setting is populated by this and + * {@code other}, the value and flags from {@code other} will be kept. + */ + void merge(Settings other) { + for (int i = 0; i < COUNT; i++) { + if (!other.isSet(i)) continue; + set(i, other.flags(i), other.get(i)); + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Spdy3.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Spdy3.java new file mode 100755 index 00000000..9730e20a --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Spdy3.java @@ -0,0 +1,498 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal.spdy; + +import com.contentstack.okhttp.Protocol; +import com.contentstack.okhttp.internal.Util; +import com.contentstack.okio.Buffer; +import com.contentstack.okio.BufferedSink; +import com.contentstack.okio.BufferedSource; +import com.contentstack.okio.ByteString; +import com.contentstack.okio.DeflaterSink; +import com.contentstack.okio.Okio; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.ProtocolException; +import java.util.List; +import java.util.zip.Deflater; + +/** + * Read and write spdy/3.1 frames. + * http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1 + */ +public final class Spdy3 implements Variant { + + @Override public Protocol getProtocol() { + return Protocol.SPDY_3; + } + + static final int TYPE_DATA = 0x0; + static final int TYPE_SYN_STREAM = 0x1; + static final int TYPE_SYN_REPLY = 0x2; + static final int TYPE_RST_STREAM = 0x3; + static final int TYPE_SETTINGS = 0x4; + static final int TYPE_PING = 0x6; + static final int TYPE_GOAWAY = 0x7; + static final int TYPE_HEADERS = 0x8; + static final int TYPE_WINDOW_UPDATE = 0x9; + + static final int FLAG_FIN = 0x1; + static final int FLAG_UNIDIRECTIONAL = 0x2; + + static final int VERSION = 3; + + static final byte[] DICTIONARY; + static { + try { + DICTIONARY = ("\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004hea" + + "d\u0000\u0000\u0000\u0004post\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006dele" + + "te\u0000\u0000\u0000\u0005trace\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000" + + "\u000Eaccept-charset\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Fa" + + "ccept-language\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000" + + "\u0000\u0000\u0005allow\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-co" + + "ntrol\u0000\u0000\u0000\nconnection\u0000\u0000\u0000\fcontent-base\u0000\u0000" + + "\u0000\u0010content-encoding\u0000\u0000\u0000\u0010content-language\u0000\u0000" + + "\u0000\u000Econtent-length\u0000\u0000\u0000\u0010content-location\u0000\u0000\u0000" + + "\u000Bcontent-md5\u0000\u0000\u0000\rcontent-range\u0000\u0000\u0000\fcontent-type" + + "\u0000\u0000\u0000\u0004date\u0000\u0000\u0000\u0004etag\u0000\u0000\u0000\u0006expe" + + "ct\u0000\u0000\u0000\u0007expires\u0000\u0000\u0000\u0004from\u0000\u0000\u0000" + + "\u0004host\u0000\u0000\u0000\bif-match\u0000\u0000\u0000\u0011if-modified-since" + + "\u0000\u0000\u0000\rif-none-match\u0000\u0000\u0000\bif-range\u0000\u0000\u0000" + + "\u0013if-unmodified-since\u0000\u0000\u0000\rlast-modified\u0000\u0000\u0000\blocati" + + "on\u0000\u0000\u0000\fmax-forwards\u0000\u0000\u0000\u0006pragma\u0000\u0000\u0000" + + "\u0012proxy-authenticate\u0000\u0000\u0000\u0013proxy-authorization\u0000\u0000" + + "\u0000\u0005range\u0000\u0000\u0000\u0007referer\u0000\u0000\u0000\u000Bretry-after" + + "\u0000\u0000\u0000\u0006server\u0000\u0000\u0000\u0002te\u0000\u0000\u0000\u0007trai" + + "ler\u0000\u0000\u0000\u0011transfer-encoding\u0000\u0000\u0000\u0007upgrade\u0000" + + "\u0000\u0000\nuser-agent\u0000\u0000\u0000\u0004vary\u0000\u0000\u0000\u0003via" + + "\u0000\u0000\u0000\u0007warning\u0000\u0000\u0000\u0010www-authenticate\u0000\u0000" + + "\u0000\u0006method\u0000\u0000\u0000\u0003get\u0000\u0000\u0000\u0006status\u0000" + + "\u0000\u0000\u0006200 OK\u0000\u0000\u0000\u0007version\u0000\u0000\u0000\bHTTP/1.1" + + "\u0000\u0000\u0000\u0003url\u0000\u0000\u0000\u0006public\u0000\u0000\u0000\nset-coo" + + "kie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin100101201202205206300" + + ***REMOVED*** + + "tative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized" + + "403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Un" + + "availableJan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec 00:00:00 Mon, Tue, Wed, Th" + + "u, Fri, Sat, Sun, GMTchunked,text/html,image/png,image/jpg,image/gif,application/xml" + + ",application/xhtml+xml,text/plain,text/javascript,publicprivatemax-age=gzip,deflate," + + "sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0.").getBytes(Util.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(); + } + } + + @Override public FrameReader newReader(BufferedSource source, boolean client) { + return new Reader(source, client); + } + + @Override public FrameWriter newWriter(BufferedSink sink, boolean client) { + return new Writer(sink, client); + } + + @Override public int maxFrameSize() { + return 16383; + } + + /** Read spdy/3 frames. */ + static final class Reader implements FrameReader { + private final BufferedSource source; + private final boolean client; + private final NameValueBlockReader headerBlockReader; + + Reader(BufferedSource source, boolean client) { + this.source = source; + this.headerBlockReader = new NameValueBlockReader(this.source); + this.client = client; + } + + @Override public void readConnectionPreface() { + } + + /** + * Send the next frame to {@code handler}. Returns true unless there are no + * more frames on the stream. + */ + @Override public boolean nextFrame(Handler handler) throws IOException { + int w1; + int w2; + try { + w1 = source.readInt(); + w2 = source.readInt(); + } catch (IOException e) { + return false; // This might be a normal socket close. + } + + boolean control = (w1 & 0x80000000) != 0; + int flags = (w2 & 0xff000000) >>> 24; + int length = (w2 & 0xffffff); + + if (control) { + int version = (w1 & 0x7fff0000) >>> 16; + int type = (w1 & 0xffff); + + if (version != 3) { + throw new ProtocolException("version != 3: " + version); + } + + switch (type) { + case TYPE_SYN_STREAM: + readSynStream(handler, flags, length); + return true; + + case TYPE_SYN_REPLY: + readSynReply(handler, flags, length); + return true; + + case TYPE_RST_STREAM: + readRstStream(handler, flags, length); + return true; + + case TYPE_SETTINGS: + readSettings(handler, flags, length); + return true; + + case TYPE_PING: + readPing(handler, flags, length); + return true; + + case TYPE_GOAWAY: + readGoAway(handler, flags, length); + return true; + + case TYPE_HEADERS: + readHeaders(handler, flags, length); + return true; + + case TYPE_WINDOW_UPDATE: + readWindowUpdate(handler, flags, length); + return true; + + default: + source.skip(length); + return true; + } + } else { + int streamId = w1 & 0x7fffffff; + boolean inFinished = (flags & FLAG_FIN) != 0; + handler.data(inFinished, streamId, source, length); + return true; + } + } + + private void readSynStream(Handler handler, int flags, int length) throws IOException { + int w1 = source.readInt(); + int w2 = source.readInt(); + int streamId = w1 & 0x7fffffff; + int associatedStreamId = w2 & 0x7fffffff; + source.readShort(); // int priority = (s3 & 0xe000) >>> 13; int slot = s3 & 0xff; + List
headerBlock = headerBlockReader.readNameValueBlock(length - 10); + + boolean inFinished = (flags & FLAG_FIN) != 0; + boolean outFinished = (flags & FLAG_UNIDIRECTIONAL) != 0; + handler.headers(outFinished, inFinished, streamId, associatedStreamId, headerBlock, + HeadersMode.SPDY_SYN_STREAM); + } + + private void readSynReply(Handler handler, int flags, int length) throws IOException { + int w1 = source.readInt(); + int streamId = w1 & 0x7fffffff; + List
headerBlock = headerBlockReader.readNameValueBlock(length - 4); + boolean inFinished = (flags & FLAG_FIN) != 0; + handler.headers(false, inFinished, streamId, -1, headerBlock, HeadersMode.SPDY_REPLY); + } + + private void readRstStream(Handler handler, int flags, int length) throws IOException { + if (length != 8) throw ioException("TYPE_RST_STREAM length: %d != 8", length); + int streamId = source.readInt() & 0x7fffffff; + int errorCodeInt = source.readInt(); + ErrorCode errorCode = ErrorCode.fromSpdy3Rst(errorCodeInt); + if (errorCode == null) { + throw ioException("TYPE_RST_STREAM unexpected error code: %d", errorCodeInt); + } + handler.rstStream(streamId, errorCode); + } + + private void readHeaders(Handler handler, int flags, int length) throws IOException { + int w1 = source.readInt(); + int streamId = w1 & 0x7fffffff; + List
headerBlock = headerBlockReader.readNameValueBlock(length - 4); + handler.headers(false, false, streamId, -1, headerBlock, HeadersMode.SPDY_HEADERS); + } + + private void readWindowUpdate(Handler handler, int flags, int length) throws IOException { + if (length != 8) throw ioException("TYPE_WINDOW_UPDATE length: %d != 8", length); + int w1 = source.readInt(); + int w2 = source.readInt(); + int streamId = w1 & 0x7fffffff; + long increment = w2 & 0x7fffffff; + if (increment == 0) throw ioException("windowSizeIncrement was 0", increment); + handler.windowUpdate(streamId, increment); + } + + private void readPing(Handler handler, int flags, int length) throws IOException { + if (length != 4) throw ioException("TYPE_PING length: %d != 4", length); + int id = source.readInt(); + boolean ack = client == ((id & 1) == 1); + handler.ping(ack, id, 0); + } + + private void readGoAway(Handler handler, int flags, int length) throws IOException { + if (length != 8) throw ioException("TYPE_GOAWAY length: %d != 8", length); + int lastGoodStreamId = source.readInt() & 0x7fffffff; + int errorCodeInt = source.readInt(); + ErrorCode errorCode = ErrorCode.fromSpdyGoAway(errorCodeInt); + if (errorCode == null) { + throw ioException("TYPE_GOAWAY unexpected error code: %d", errorCodeInt); + } + handler.goAway(lastGoodStreamId, errorCode, ByteString.EMPTY); + } + + private void readSettings(Handler handler, int flags, int length) throws IOException { + int numberOfEntries = source.readInt(); + if (length != 4 + 8 * numberOfEntries) { + throw ioException("TYPE_SETTINGS length: %d != 4 + 8 * %d", length, numberOfEntries); + } + Settings settings = new Settings(); + for (int i = 0; i < numberOfEntries; i++) { + int w1 = source.readInt(); + int value = source.readInt(); + int idFlags = (w1 & 0xff000000) >>> 24; + int id = w1 & 0xffffff; + settings.set(id, idFlags, value); + } + boolean clearPrevious = (flags & Settings.FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS) != 0; + handler.settings(clearPrevious, settings); + } + + private static IOException ioException(String message, Object... args) throws IOException { + throw new IOException(String.format(message, args)); + } + + @Override public void close() throws IOException { + headerBlockReader.close(); + } + } + + /** Write spdy/3 frames. */ + static final class Writer implements FrameWriter { + private final BufferedSink sink; + private final Buffer headerBlockBuffer; + private final BufferedSink headerBlockOut; + private final boolean client; + private boolean closed; + + Writer(BufferedSink sink, boolean client) { + this.sink = sink; + this.client = client; + + Deflater deflater = new Deflater(); + deflater.setDictionary(DICTIONARY); + headerBlockBuffer = new Buffer(); + headerBlockOut = Okio.buffer(new DeflaterSink(headerBlockBuffer, deflater)); + } + + @Override public void ackSettings() { + // Do nothing: no ACK for SPDY/3 settings. + } + + @Override + public void pushPromise(int streamId, int promisedStreamId, List
requestHeaders) + throws IOException { + // Do nothing: no push promise for SPDY/3. + } + + @Override public synchronized void connectionPreface() { + // Do nothing: no connection preface for SPDY/3. + } + + @Override public synchronized void flush() throws IOException { + if (closed) throw new IOException("closed"); + sink.flush(); + } + + @Override public synchronized void synStream(boolean outFinished, boolean inFinished, + int streamId, int associatedStreamId, List
headerBlock) + throws IOException { + if (closed) throw new IOException("closed"); + writeNameValueBlockToBuffer(headerBlock); + int length = (int) (10 + headerBlockBuffer.size()); + int type = TYPE_SYN_STREAM; + int flags = (outFinished ? FLAG_FIN : 0) | (inFinished ? FLAG_UNIDIRECTIONAL : 0); + + int unused = 0; + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(streamId & 0x7fffffff); + sink.writeInt(associatedStreamId & 0x7fffffff); + sink.writeShort((unused & 0x7) << 13 | (unused & 0x1f) << 8 | (unused & 0xff)); + sink.writeAll(headerBlockBuffer); + sink.flush(); + } + + @Override public synchronized void synReply(boolean outFinished, int streamId, + List
headerBlock) throws IOException { + if (closed) throw new IOException("closed"); + writeNameValueBlockToBuffer(headerBlock); + int type = TYPE_SYN_REPLY; + int flags = (outFinished ? FLAG_FIN : 0); + int length = (int) (headerBlockBuffer.size() + 4); + + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(streamId & 0x7fffffff); + sink.writeAll(headerBlockBuffer); + sink.flush(); + } + + @Override public synchronized void headers(int streamId, List
headerBlock) + throws IOException { + if (closed) throw new IOException("closed"); + writeNameValueBlockToBuffer(headerBlock); + int flags = 0; + int type = TYPE_HEADERS; + int length = (int) (headerBlockBuffer.size() + 4); + + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(streamId & 0x7fffffff); + sink.writeAll(headerBlockBuffer); + } + + @Override public synchronized void rstStream(int streamId, ErrorCode errorCode) + throws IOException { + if (closed) throw new IOException("closed"); + if (errorCode.spdyRstCode == -1) throw new IllegalArgumentException(); + int flags = 0; + int type = TYPE_RST_STREAM; + int length = 8; + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(streamId & 0x7fffffff); + sink.writeInt(errorCode.spdyRstCode); + sink.flush(); + } + + @Override public synchronized void data(boolean outFinished, int streamId, Buffer source) + throws IOException { + data(outFinished, streamId, source, (int) source.size()); + } + + @Override public synchronized void data(boolean outFinished, int streamId, Buffer source, + int byteCount) throws IOException { + int flags = (outFinished ? FLAG_FIN : 0); + sendDataFrame(streamId, flags, source, byteCount); + } + + void sendDataFrame(int streamId, int flags, Buffer buffer, int byteCount) + throws IOException { + if (closed) throw new IOException("closed"); + if (byteCount > 0xffffffL) { + throw new IllegalArgumentException("FRAME_TOO_LARGE max size is 16Mib: " + byteCount); + } + sink.writeInt(streamId & 0x7fffffff); + sink.writeInt((flags & 0xff) << 24 | byteCount & 0xffffff); + if (byteCount > 0) { + sink.write(buffer, byteCount); + } + } + + private void writeNameValueBlockToBuffer(List
headerBlock) throws IOException { + if (headerBlockBuffer.size() != 0) throw new IllegalStateException(); + headerBlockOut.writeInt(headerBlock.size()); + for (int i = 0, size = headerBlock.size(); i < size; i++) { + ByteString name = headerBlock.get(i).name; + headerBlockOut.writeInt(name.size()); + headerBlockOut.write(name); + ByteString value = headerBlock.get(i).value; + headerBlockOut.writeInt(value.size()); + headerBlockOut.write(value); + } + headerBlockOut.flush(); + } + + @Override public synchronized void settings(Settings settings) throws IOException { + if (closed) throw new IOException("closed"); + int type = TYPE_SETTINGS; + int flags = 0; + int size = settings.size(); + int length = 4 + size * 8; + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(size); + for (int i = 0; i <= Settings.COUNT; i++) { + if (!settings.isSet(i)) continue; + int settingsFlags = settings.flags(i); + sink.writeInt((settingsFlags & 0xff) << 24 | (i & 0xffffff)); + sink.writeInt(settings.get(i)); + } + sink.flush(); + } + + @Override public synchronized void ping(boolean reply, int payload1, int payload2) + throws IOException { + if (closed) throw new IOException("closed"); + boolean payloadIsReply = client != ((payload1 & 1) == 1); + if (reply != payloadIsReply) throw new IllegalArgumentException("payload != reply"); + int type = TYPE_PING; + int flags = 0; + int length = 4; + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(payload1); + sink.flush(); + } + + @Override public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode, + byte[] ignored) throws IOException { + if (closed) throw new IOException("closed"); + if (errorCode.spdyGoAwayCode == -1) { + throw new IllegalArgumentException("errorCode.spdyGoAwayCode == -1"); + } + int type = TYPE_GOAWAY; + int flags = 0; + int length = 8; + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(lastGoodStreamId); + sink.writeInt(errorCode.spdyGoAwayCode); + sink.flush(); + } + + @Override public synchronized void windowUpdate(int streamId, long increment) + throws IOException { + if (closed) throw new IOException("closed"); + if (increment == 0 || increment > 0x7fffffffL) { + throw new IllegalArgumentException( + "windowSizeIncrement must be between 1 and 0x7fffffff: " + increment); + } + int type = TYPE_WINDOW_UPDATE; + int flags = 0; + int length = 8; + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(streamId); + sink.writeInt((int) increment); + sink.flush(); + } + + @Override public synchronized void close() throws IOException { + closed = true; + Util.closeAll(sink, headerBlockOut); + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/SpdyConnection.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/SpdyConnection.java new file mode 100755 index 00000000..d88fb735 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/SpdyConnection.java @@ -0,0 +1,865 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal.spdy; + +import com.contentstack.okhttp.Protocol; +import com.contentstack.okhttp.internal.NamedRunnable; +import com.contentstack.okhttp.internal.Util; +import com.contentstack.okio.Buffer; +import com.contentstack.okio.BufferedSource; +import com.contentstack.okio.ByteString; +import com.contentstack.okio.Okio; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * A socket connection to a remote peer. A connection hosts streams which can + * send and receive data. + * + *

Many methods in this API are synchronous: the call is + * completed before the method returns. This is typical for Java but atypical + * for SPDY. This is motivated by exception transparency: an IOException that + * was triggered by a certain caller can be caught and handled by that caller. + */ +public final class SpdyConnection implements Closeable { + + // Internal state of this connection is guarded by 'this'. No blocking + // operations may be performed while holding this lock! + // + // Socket writes are guarded by frameWriter. + // + // Socket reads are unguarded but are only made by the reader thread. + // + // Certain operations (like SYN_STREAM) need to synchronize on both the + // frameWriter (to do blocking I/O) and this (to create streams). Such + // operations must synchronize on 'this' last. This ensures that we never + // wait for a blocking operation while holding 'this'. + + private static final ExecutorService executor = new ThreadPoolExecutor(0, + Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue(), + Util.threadFactory("OkHttp SpdyConnection", true)); + + /** The protocol variant, like {@link Spdy3}. */ + final Protocol protocol; + + /** True if this peer initiated the connection. */ + final boolean client; + + /** + * User code to run in response to an incoming stream. Callbacks must not be + * run on the callback executor. + */ + private final IncomingStreamHandler handler; + private final Map streams = new HashMap(); + private final String hostName; + private int lastGoodStreamId; + private int nextStreamId; + private boolean shutdown; + private long idleStartTimeNs = System.nanoTime(); + + /** Ensures push promise callbacks events are sent in order per stream. */ + private final ExecutorService pushExecutor; + + /** Lazily-created map of in-flight pings awaiting a response. Guarded by this. */ + private Map pings; + /** User code to run in response to push promise events. */ + private final PushObserver pushObserver; + private int nextPingId; + + /** + * The total number of bytes consumed by the application, but not yet + * acknowledged by sending a {@code WINDOW_UPDATE} frame on this connection. + */ + // Visible for testing + long unacknowledgedBytesRead = 0; + + /** + * Count of bytes that can be written on the connection before receiving a + * window update. + */ + // Visible for testing + long bytesLeftInWriteWindow; + + /** Settings we communicate to the peer. */ + final Settings okHttpSettings = new Settings(); + // okHttpSettings.set(Settings.MAX_CONCURRENT_STREAMS, 0, max); + private static final int OKHTTP_CLIENT_WINDOW_SIZE = 16 * 1024 * 1024; + + /** Settings we receive from the peer. */ + final Settings peerSettings = new Settings(); + + private boolean receivedInitialPeerSettings = false; + final Variant variant; + final Socket socket; + final FrameWriter frameWriter; + final long maxFrameSize; + + // Visible for testing + final Reader readerRunnable; + + private SpdyConnection(Builder builder) throws IOException { + protocol = builder.protocol; + pushObserver = builder.pushObserver; + client = builder.client; + handler = builder.handler; + // http://tools.ietf.org/html/draft-ietf-httpbis-http2-13#section-5.1.1 + nextStreamId = builder.client ? 1 : 2; + if (builder.client && protocol == Protocol.HTTP_2) { + nextStreamId += 2; // In HTTP/2, 1 on client is reserved for Upgrade. + } + + nextPingId = builder.client ? 1 : 2; + + // Flow control was designed more for servers, or proxies than edge clients. + // If we are a client, set the flow control window to 16MiB. This avoids + // thrashing window updates every 64KiB, yet small enough to avoid blowing + // up the heap. + if (builder.client) { + okHttpSettings.set(Settings.INITIAL_WINDOW_SIZE, 0, OKHTTP_CLIENT_WINDOW_SIZE); + } + + hostName = builder.hostName; + + if (protocol == Protocol.HTTP_2) { + variant = new Http20Draft13(); + // Like newSingleThreadExecutor, except lazy creates the thread. + pushExecutor = new ThreadPoolExecutor(0, 1, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(), + Util.threadFactory(String.format("OkHttp %s Push Observer", hostName), true)); + // 1 less than SPDY http://tools.ietf.org/html/draft-ietf-httpbis-http2-13#section-6.9.2 + peerSettings.set(Settings.INITIAL_WINDOW_SIZE, 0, 65535); + } else if (protocol == Protocol.SPDY_3) { + variant = new Spdy3(); + pushExecutor = null; + } else { + throw new AssertionError(protocol); + } + bytesLeftInWriteWindow = peerSettings.getInitialWindowSize(Settings.DEFAULT_INITIAL_WINDOW_SIZE); + socket = builder.socket; + frameWriter = variant.newWriter(Okio.buffer(Okio.sink(builder.socket)), client); + maxFrameSize = variant.maxFrameSize(); + + readerRunnable = new Reader(); + new Thread(readerRunnable).start(); // Not a daemon thread. + } + + /** The protocol as selected using NPN or ALPN. */ + public Protocol getProtocol() { + return protocol; + } + + /** + * Returns the number of {@link SpdyStream#isOpen() open streams} on this + * connection. + */ + public synchronized int openStreamCount() { + return streams.size(); + } + + synchronized SpdyStream getStream(int id) { + return streams.get(id); + } + + synchronized SpdyStream removeStream(int streamId) { + SpdyStream stream = streams.remove(streamId); + if (stream != null && streams.isEmpty()) { + setIdle(true); + } + return stream; + } + + private synchronized void setIdle(boolean value) { + idleStartTimeNs = value ? System.nanoTime() : Long.MAX_VALUE; + } + + /** Returns true if this connection is idle. */ + public synchronized boolean isIdle() { + return idleStartTimeNs != Long.MAX_VALUE; + } + + /** + * Returns the time in ns when this connection became idle or Long.MAX_VALUE + * if connection is not idle. + */ + public synchronized long getIdleStartTimeNs() { + return idleStartTimeNs; + } + + /** + * Returns a new server-initiated stream. + * + * @param associatedStreamId the stream that triggered the sender to create + * this stream. + * @param out true to create an output stream that we can use to send data + * to the remote peer. Corresponds to {@code FLAG_FIN}. + */ + public SpdyStream pushStream(int associatedStreamId, List

requestHeaders, boolean out) + throws IOException { + if (client) throw new IllegalStateException("Client cannot push requests."); + if (protocol != Protocol.HTTP_2) throw new IllegalStateException("protocol != HTTP_2"); + return newStream(associatedStreamId, requestHeaders, out, false); + } + + /** + * Returns a new locally-initiated stream. + * + * @param out true to create an output stream that we can use to send data to the remote peer. + * Corresponds to {@code FLAG_FIN}. + * @param in true to create an input stream that the remote peer can use to send data to us. + * Corresponds to {@code FLAG_UNIDIRECTIONAL}. + */ + public SpdyStream newStream(List
requestHeaders, boolean out, boolean in) + throws IOException { + return newStream(0, requestHeaders, out, in); + } + + private SpdyStream newStream(int associatedStreamId, List
requestHeaders, boolean out, + boolean in) throws IOException { + boolean outFinished = !out; + boolean inFinished = !in; + SpdyStream stream; + int streamId; + + synchronized (frameWriter) { + synchronized (this) { + if (shutdown) { + throw new IOException("shutdown"); + } + streamId = nextStreamId; + nextStreamId += 2; + stream = new SpdyStream(streamId, this, outFinished, inFinished, requestHeaders); + if (stream.isOpen()) { + streams.put(streamId, stream); + setIdle(false); + } + } + if (associatedStreamId == 0) { + frameWriter.synStream(outFinished, inFinished, streamId, associatedStreamId, + requestHeaders); + } else if (client) { + throw new IllegalArgumentException("client streams shouldn't have associated stream IDs"); + } else { // HTTP/2 has a PUSH_PROMISE frame. + frameWriter.pushPromise(associatedStreamId, streamId, requestHeaders); + } + } + + if (!out) { + frameWriter.flush(); + } + + return stream; + } + + void writeSynReply(int streamId, boolean outFinished, List
alternating) + throws IOException { + frameWriter.synReply(outFinished, streamId, alternating); + } + + /** + * Callers of this method are not thread safe, and sometimes on application + * threads. Most often, this method will be called to send a buffer worth of + * data to the peer. + *

+ * Writes are subject to the write window of the stream and the connection. + * Until there is a window sufficient to send {@code byteCount}, the caller + * will block. For example, a user of {@code HttpURLConnection} who flushes + * more bytes to the output stream than the connection's write window will + * block. + *

+ * Zero {@code byteCount} writes are not subject to flow control and + * will not block. The only use case for zero {@code byteCount} is closing + * a flushed output stream. + */ + public void writeData(int streamId, boolean outFinished, Buffer buffer, long byteCount) + throws IOException { + if (byteCount == 0) { // Empty data frames are not flow-controlled. + frameWriter.data(outFinished, streamId, buffer, 0); + return; + } + + while (byteCount > 0) { + int toWrite; + synchronized (SpdyConnection.this) { + try { + while (bytesLeftInWriteWindow <= 0) { + SpdyConnection.this.wait(); // Wait until we receive a WINDOW_UPDATE. + } + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } + + toWrite = (int) Math.min(Math.min(byteCount, bytesLeftInWriteWindow), maxFrameSize); + bytesLeftInWriteWindow -= toWrite; + } + + byteCount -= toWrite; + frameWriter.data(outFinished && byteCount == 0, streamId, buffer, toWrite); + } + } + + /** + * {@code delta} will be negative if a settings frame initial window is + * smaller than the last. + */ + void addBytesToWriteWindow(long delta) { + bytesLeftInWriteWindow += delta; + if (delta > 0) SpdyConnection.this.notifyAll(); + } + + void writeSynResetLater(final int streamId, final ErrorCode errorCode) { + executor.submit(new NamedRunnable("OkHttp %s stream %d", hostName, streamId) { + @Override public void execute() { + try { + writeSynReset(streamId, errorCode); + } catch (IOException ignored) { + } + } + }); + } + + void writeSynReset(int streamId, ErrorCode statusCode) throws IOException { + frameWriter.rstStream(streamId, statusCode); + } + + void writeWindowUpdateLater(final int streamId, final long unacknowledgedBytesRead) { + executor.submit(new NamedRunnable("OkHttp Window Update %s stream %d", hostName, streamId) { + @Override public void execute() { + try { + frameWriter.windowUpdate(streamId, unacknowledgedBytesRead); + } catch (IOException ignored) { + } + } + }); + } + + /** + * Sends a ping frame to the peer. Use the returned object to await the + * ping's response and observe its round trip time. + */ + public Ping ping() throws IOException { + Ping ping = new Ping(); + int pingId; + synchronized (this) { + if (shutdown) { + throw new IOException("shutdown"); + } + pingId = nextPingId; + nextPingId += 2; + if (pings == null) pings = new HashMap(); + pings.put(pingId, ping); + } + writePing(false, pingId, 0x4f4b6f6b /* ASCII "OKok" */, ping); + return ping; + } + + private void writePingLater( + final boolean reply, final int payload1, final int payload2, final Ping ping) { + executor.submit(new NamedRunnable("OkHttp %s ping %08x%08x", + hostName, payload1, payload2) { + @Override public void execute() { + try { + writePing(reply, payload1, payload2, ping); + } catch (IOException ignored) { + } + } + }); + } + + private void writePing(boolean reply, int payload1, int payload2, Ping ping) throws IOException { + synchronized (frameWriter) { + // Observe the sent time immediately before performing I/O. + if (ping != null) ping.send(); + frameWriter.ping(reply, payload1, payload2); + } + } + + private synchronized Ping removePing(int id) { + return pings != null ? pings.remove(id) : null; + } + + public void flush() throws IOException { + frameWriter.flush(); + } + + /** + * Degrades this connection such that new streams can neither be created + * locally, nor accepted from the remote peer. Existing streams are not + * impacted. This is intended to permit an endpoint to gracefully stop + * accepting new requests without harming previously established streams. + */ + public void shutdown(ErrorCode statusCode) throws IOException { + synchronized (frameWriter) { + int lastGoodStreamId; + synchronized (this) { + if (shutdown) { + return; + } + shutdown = true; + lastGoodStreamId = this.lastGoodStreamId; + } + frameWriter.goAway(lastGoodStreamId, statusCode, Util.EMPTY_BYTE_ARRAY); + } + } + + /** + * Closes this connection. This cancels all open streams and unanswered + * pings. It closes the underlying input and output streams and shuts down + * internal executor services. + */ + @Override public void close() throws IOException { + close(ErrorCode.NO_ERROR, ErrorCode.CANCEL); + } + + private void close(ErrorCode connectionCode, ErrorCode streamCode) throws IOException { + assert (!Thread.holdsLock(this)); + IOException thrown = null; + try { + shutdown(connectionCode); + } catch (IOException e) { + thrown = e; + } + + SpdyStream[] streamsToClose = null; + Ping[] pingsToCancel = null; + synchronized (this) { + if (!streams.isEmpty()) { + streamsToClose = streams.values().toArray(new SpdyStream[streams.size()]); + streams.clear(); + setIdle(false); + } + if (pings != null) { + pingsToCancel = pings.values().toArray(new Ping[pings.size()]); + pings = null; + } + } + + if (streamsToClose != null) { + for (SpdyStream stream : streamsToClose) { + try { + stream.close(streamCode); + } catch (IOException e) { + if (thrown != null) thrown = e; + } + } + } + + if (pingsToCancel != null) { + for (Ping ping : pingsToCancel) { + ping.cancel(); + } + } + + // Close the writer to release its resources (such as deflaters). + try { + frameWriter.close(); + } catch (IOException e) { + if (thrown == null) thrown = e; + } + + // Close the socket to break out the reader thread, which will clean up after itself. + try { + socket.close(); + } catch (IOException e) { + thrown = e; + } + + if (thrown != null) throw thrown; + } + + /** + * Sends a connection header if the current variant requires it. This should + * be called after {@link Builder#build} for all new connections. + */ + public void sendConnectionPreface() throws IOException { + frameWriter.connectionPreface(); + frameWriter.settings(okHttpSettings); + int windowSize = okHttpSettings.getInitialWindowSize(Settings.DEFAULT_INITIAL_WINDOW_SIZE); + if (windowSize != Settings.DEFAULT_INITIAL_WINDOW_SIZE) { + frameWriter.windowUpdate(0, windowSize - Settings.DEFAULT_INITIAL_WINDOW_SIZE); + } + } + + public static class Builder { + private String hostName; + private Socket socket; + private IncomingStreamHandler handler = IncomingStreamHandler.REFUSE_INCOMING_STREAMS; + private Protocol protocol = Protocol.SPDY_3; + private PushObserver pushObserver = PushObserver.CANCEL; + private boolean client; + + public Builder(boolean client, Socket socket) throws IOException { + this(((InetSocketAddress) socket.getRemoteSocketAddress()).getHostName(), client, socket); + } + + /** + * @param client true if this peer initiated the connection; false if this + * peer accepted the connection. + */ + public Builder(String hostName, boolean client, Socket socket) throws IOException { + this.hostName = hostName; + this.client = client; + this.socket = socket; + } + + public Builder handler(IncomingStreamHandler handler) { + this.handler = handler; + return this; + } + + public Builder protocol(Protocol protocol) { + this.protocol = protocol; + return this; + } + + public Builder pushObserver(PushObserver pushObserver) { + this.pushObserver = pushObserver; + return this; + } + + public SpdyConnection build() throws IOException { + return new SpdyConnection(this); + } + } + + /** + * Methods in this class must not lock FrameWriter. If a method needs to + * write a frame, create an async task to do so. + */ + class Reader extends NamedRunnable implements FrameReader.Handler { + FrameReader frameReader; + + private Reader() { + super("OkHttp %s", hostName); + } + + @Override protected void execute() { + ErrorCode connectionErrorCode = ErrorCode.INTERNAL_ERROR; + ErrorCode streamErrorCode = ErrorCode.INTERNAL_ERROR; + try { + frameReader = variant.newReader(Okio.buffer(Okio.source(socket)), client); + if (!client) { + frameReader.readConnectionPreface(); + } + while (frameReader.nextFrame(this)) { + } + connectionErrorCode = ErrorCode.NO_ERROR; + streamErrorCode = ErrorCode.CANCEL; + } catch (IOException e) { + connectionErrorCode = ErrorCode.PROTOCOL_ERROR; + streamErrorCode = ErrorCode.PROTOCOL_ERROR; + } finally { + try { + close(connectionErrorCode, streamErrorCode); + } catch (IOException ignored) { + } + Util.closeQuietly(frameReader); + } + } + + @Override public void data(boolean inFinished, int streamId, BufferedSource source, int length) + throws IOException { + if (pushedStream(streamId)) { + pushDataLater(streamId, source, length, inFinished); + return; + } + SpdyStream dataStream = getStream(streamId); + if (dataStream == null) { + writeSynResetLater(streamId, ErrorCode.INVALID_STREAM); + source.skip(length); + return; + } + dataStream.receiveData(source, length); + if (inFinished) { + dataStream.receiveFin(); + } + } + + @Override public void headers(boolean outFinished, boolean inFinished, int streamId, + int associatedStreamId, List

headerBlock, HeadersMode headersMode) { + if (pushedStream(streamId)) { + pushHeadersLater(streamId, headerBlock, inFinished); + return; + } + SpdyStream stream; + synchronized (SpdyConnection.this) { + // If we're shutdown, don't bother with this stream. + if (shutdown) return; + + stream = getStream(streamId); + + if (stream == null) { + // The headers claim to be for an existing stream, but we don't have one. + if (headersMode.failIfStreamAbsent()) { + writeSynResetLater(streamId, ErrorCode.INVALID_STREAM); + return; + } + + // If the stream ID is less than the last created ID, assume it's already closed. + if (streamId <= lastGoodStreamId) return; + + // If the stream ID is in the client's namespace, assume it's already closed. + if (streamId % 2 == nextStreamId % 2) return; + + // Create a stream. + final SpdyStream newStream = new SpdyStream(streamId, SpdyConnection.this, outFinished, + inFinished, headerBlock); + lastGoodStreamId = streamId; + streams.put(streamId, newStream); + executor.submit(new NamedRunnable("OkHttp %s stream %d", hostName, streamId) { + @Override public void execute() { + try { + handler.receive(newStream); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }); + return; + } + } + + // The headers claim to be for a new stream, but we already have one. + if (headersMode.failIfStreamPresent()) { + stream.closeLater(ErrorCode.PROTOCOL_ERROR); + removeStream(streamId); + return; + } + + // Update an existing stream. + stream.receiveHeaders(headerBlock, headersMode); + if (inFinished) stream.receiveFin(); + } + + @Override public void rstStream(int streamId, ErrorCode errorCode) { + if (pushedStream(streamId)) { + pushResetLater(streamId, errorCode); + return; + } + SpdyStream rstStream = removeStream(streamId); + if (rstStream != null) { + rstStream.receiveRstStream(errorCode); + } + } + + @Override public void settings(boolean clearPrevious, Settings newSettings) { + long delta = 0; + SpdyStream[] streamsToNotify = null; + synchronized (SpdyConnection.this) { + int priorWriteWindowSize = peerSettings.getInitialWindowSize(Settings.DEFAULT_INITIAL_WINDOW_SIZE); + if (clearPrevious) peerSettings.clear(); + peerSettings.merge(newSettings); + if (getProtocol() == Protocol.HTTP_2) { + ackSettingsLater(); + } + int peerInitialWindowSize = peerSettings.getInitialWindowSize(Settings.DEFAULT_INITIAL_WINDOW_SIZE); + if (peerInitialWindowSize != -1 && peerInitialWindowSize != priorWriteWindowSize) { + delta = peerInitialWindowSize - priorWriteWindowSize; + if (!receivedInitialPeerSettings) { + addBytesToWriteWindow(delta); + receivedInitialPeerSettings = true; + } + if (!streams.isEmpty()) { + streamsToNotify = streams.values().toArray(new SpdyStream[streams.size()]); + } + } + } + if (streamsToNotify != null && delta != 0) { + for (SpdyStream stream : streams.values()) { + synchronized (stream) { + stream.addBytesToWriteWindow(delta); + } + } + } + } + + private void ackSettingsLater() { + executor.submit(new NamedRunnable("OkHttp %s ACK Settings", hostName) { + @Override public void execute() { + try { + frameWriter.ackSettings(); + } catch (IOException ignored) { + } + } + }); + } + + @Override public void ackSettings() { + } + + @Override public void ping(boolean reply, int payload1, int payload2) { + if (reply) { + Ping ping = removePing(payload1); + if (ping != null) { + ping.receive(); + } + } else { + // Send a reply to a client ping if this is a server and vice versa. + writePingLater(true, payload1, payload2, null); + } + } + + @Override public void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) { + if (debugData.size() > 0) { + } + synchronized (SpdyConnection.this) { + shutdown = true; + + // Fail all streams created after the last good stream ID. + for (Iterator> i = streams.entrySet().iterator(); + i.hasNext(); ) { + Map.Entry entry = i.next(); + int streamId = entry.getKey(); + if (streamId > lastGoodStreamId && entry.getValue().isLocallyInitiated()) { + entry.getValue().receiveRstStream(ErrorCode.REFUSED_STREAM); + i.remove(); + } + } + } + } + + @Override public void windowUpdate(int streamId, long windowSizeIncrement) { + if (streamId == 0) { + synchronized (SpdyConnection.this) { + bytesLeftInWriteWindow += windowSizeIncrement; + SpdyConnection.this.notifyAll(); + } + } else { + SpdyStream stream = getStream(streamId); + if (stream != null) { + synchronized (stream) { + stream.addBytesToWriteWindow(windowSizeIncrement); + } + } + } + } + + @Override public void priority(int streamId, int streamDependency, int weight, + boolean exclusive) { + } + + @Override + public void pushPromise(int streamId, int promisedStreamId, List
requestHeaders) { + pushRequestLater(promisedStreamId, requestHeaders); + } + + @Override public void alternateService(int streamId, String origin, ByteString protocol, + String host, int port, long maxAge) { + } + } + + /** Even, positive numbered streams are pushed streams in HTTP/2. */ + private boolean pushedStream(int streamId) { + return protocol == Protocol.HTTP_2 && streamId != 0 && (streamId & 1) == 0; + } + + // Guarded by this. + private final Set currentPushRequests = new LinkedHashSet(); + + private void pushRequestLater(final int streamId, final List
requestHeaders) { + synchronized (this) { + if (currentPushRequests.contains(streamId)) { + writeSynResetLater(streamId, ErrorCode.PROTOCOL_ERROR); + return; + } + currentPushRequests.add(streamId); + } + pushExecutor.submit(new NamedRunnable("OkHttp %s Push Request[%s]", hostName, streamId) { + @Override public void execute() { + boolean cancel = pushObserver.onRequest(streamId, requestHeaders); + try { + if (cancel) { + frameWriter.rstStream(streamId, ErrorCode.CANCEL); + synchronized (SpdyConnection.this) { + currentPushRequests.remove(streamId); + } + } + } catch (IOException ignored) { + } + } + }); + } + + private void pushHeadersLater(final int streamId, final List
requestHeaders, + final boolean inFinished) { + pushExecutor.submit(new NamedRunnable("OkHttp %s Push Headers[%s]", hostName, streamId) { + @Override public void execute() { + boolean cancel = pushObserver.onHeaders(streamId, requestHeaders, inFinished); + try { + if (cancel) frameWriter.rstStream(streamId, ErrorCode.CANCEL); + if (cancel || inFinished) { + synchronized (SpdyConnection.this) { + currentPushRequests.remove(streamId); + } + } + } catch (IOException ignored) { + } + } + }); + } + + /** + * Eagerly reads {@code byteCount} bytes from the source before launching a background task to + * process the data. This avoids corrupting the stream. + */ + private void pushDataLater(final int streamId, final BufferedSource source, final int byteCount, + final boolean inFinished) throws IOException { + final Buffer buffer = new Buffer(); + source.require(byteCount); // Eagerly read the frame before firing client thread. + source.read(buffer, byteCount); + if (buffer.size() != byteCount) throw new IOException(buffer.size() + " != " + byteCount); + pushExecutor.submit(new NamedRunnable("OkHttp %s Push Data[%s]", hostName, streamId) { + @Override public void execute() { + try { + boolean cancel = pushObserver.onData(streamId, buffer, byteCount, inFinished); + if (cancel) frameWriter.rstStream(streamId, ErrorCode.CANCEL); + if (cancel || inFinished) { + synchronized (SpdyConnection.this) { + currentPushRequests.remove(streamId); + } + } + } catch (IOException ignored) { + } + } + }); + } + + private void pushResetLater(final int streamId, final ErrorCode errorCode) { + pushExecutor.submit(new NamedRunnable("OkHttp %s Push Reset[%s]", hostName, streamId) { + @Override public void execute() { + pushObserver.onReset(streamId, errorCode); + synchronized (SpdyConnection.this) { + currentPushRequests.remove(streamId); + } + } + }); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/SpdyStream.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/SpdyStream.java new file mode 100755 index 00000000..4b87db0c --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/SpdyStream.java @@ -0,0 +1,575 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contentstack.okhttp.internal.spdy; + +import com.contentstack.okio.AsyncTimeout; +import com.contentstack.okio.Buffer; +import com.contentstack.okio.BufferedSource; +import com.contentstack.okio.Sink; +import com.contentstack.okio.Source; +import com.contentstack.okio.Timeout; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.util.ArrayList; +import java.util.List; + +/** A logical bidirectional stream. */ +public final class SpdyStream { + // Internal state is guarded by this. No long-running or potentially + // blocking operations are performed while the lock is held. + + /** + * The total number of bytes consumed by the application (with {@link + * SpdyDataSource#read}), but not yet acknowledged by sending a {@code + * WINDOW_UPDATE} frame on this stream. + */ + // Visible for testing + long unacknowledgedBytesRead = 0; + + /** + * Count of bytes that can be written on the stream before receiving a + * window update. Even if this is positive, writes will block until there + * available bytes in {@code connection.bytesLeftInWriteWindow}. + */ + // guarded by this + long bytesLeftInWriteWindow; + + private final int id; + private final SpdyConnection connection; + private long readTimeoutMillis = 0; + + /** Headers sent by the stream initiator. Immutable and non null. */ + private final List
requestHeaders; + + /** Headers sent in the stream reply. Null if reply is either not sent or not sent yet. */ + private List
responseHeaders; + + private final SpdyDataSource source; + final SpdyDataSink sink; + private final SpdyTimeout readTimeout = new SpdyTimeout(); + private final SpdyTimeout writeTimeout = new SpdyTimeout(); + + /** + * The reason why this stream was abnormally closed. If there are multiple + * reasons to abnormally close this stream (such as both peers closing it + * near-simultaneously) then this is the first reason known to this peer. + */ + private ErrorCode errorCode = null; + + SpdyStream(int id, SpdyConnection connection, boolean outFinished, boolean inFinished, + List
requestHeaders) { + if (connection == null) throw new NullPointerException("connection == null"); + if (requestHeaders == null) throw new NullPointerException("requestHeaders == null"); + this.id = id; + this.connection = connection; + this.bytesLeftInWriteWindow = + connection.peerSettings.getInitialWindowSize(Settings.DEFAULT_INITIAL_WINDOW_SIZE); + this.source = new SpdyDataSource( + connection.okHttpSettings.getInitialWindowSize(Settings.DEFAULT_INITIAL_WINDOW_SIZE)); + this.sink = new SpdyDataSink(); + this.source.finished = inFinished; + this.sink.finished = outFinished; + this.requestHeaders = requestHeaders; + } + + public int getId() { + return id; + } + + /** + * Returns true if this stream is open. A stream is open until either: + *
    + *
  • A {@code SYN_RESET} frame abnormally terminates the stream. + *
  • Both input and output streams have transmitted all data and + * headers. + *
+ * Note that the input stream may continue to yield data even after a stream + * reports itself as not open. This is because input data is buffered. + */ + public synchronized boolean isOpen() { + if (errorCode != null) { + return false; + } + if ((source.finished || source.closed) + && (sink.finished || sink.closed) + && responseHeaders != null) { + return false; + } + return true; + } + + /** Returns true if this stream was created by this peer. */ + public boolean isLocallyInitiated() { + boolean streamIsClient = ((id & 1) == 1); + return connection.client == streamIsClient; + } + + public SpdyConnection getConnection() { + return connection; + } + + public List
getRequestHeaders() { + return requestHeaders; + } + + /** + * Returns the stream's response headers, blocking if necessary if they + * have not been received yet. + */ + public synchronized List
getResponseHeaders() throws IOException { + readTimeout.enter(); + try { + while (responseHeaders == null && errorCode == null) { + waitForIo(); + } + } finally { + readTimeout.exitAndThrowIfTimedOut(); + } + if (responseHeaders != null) return responseHeaders; + throw new IOException("stream was reset: " + errorCode); + } + + /** + * Returns the reason why this stream was closed, or null if it closed + * normally or has not yet been closed. + */ + public synchronized ErrorCode getErrorCode() { + return errorCode; + } + + /** + * Sends a reply to an incoming stream. + * + * @param out true to create an output stream that we can use to send data + * to the remote peer. Corresponds to {@code FLAG_FIN}. + */ + public void reply(List
responseHeaders, boolean out) throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + boolean outFinished = false; + synchronized (this) { + if (responseHeaders == null) { + throw new NullPointerException("responseHeaders == null"); + } + if (this.responseHeaders != null) { + throw new IllegalStateException("reply already sent"); + } + this.responseHeaders = responseHeaders; + if (!out) { + this.sink.finished = true; + outFinished = true; + } + } + connection.writeSynReply(id, outFinished, responseHeaders); + + if (outFinished) { + connection.flush(); + } + } + + public Timeout readTimeout() { + return readTimeout; + } + + public Timeout writeTimeout() { + return writeTimeout; + } + + /** Returns a source that reads data from the peer. */ + public Source getSource() { + return source; + } + + /** + * Returns a sink that can be used to write data to the peer. + * + * @throws IllegalStateException if this stream was initiated by the peer + * and a {@link #reply} has not yet been sent. + */ + public Sink getSink() { + synchronized (this) { + if (responseHeaders == null && !isLocallyInitiated()) { + throw new IllegalStateException("reply before requesting the sink"); + } + } + return sink; + } + + /** + * Abnormally terminate this stream. This blocks until the {@code RST_STREAM} + * frame has been transmitted. + */ + public void close(ErrorCode rstStatusCode) throws IOException { + if (!closeInternal(rstStatusCode)) { + return; // Already closed. + } + connection.writeSynReset(id, rstStatusCode); + } + + /** + * Abnormally terminate this stream. This enqueues a {@code RST_STREAM} + * frame and returns immediately. + */ + public void closeLater(ErrorCode errorCode) { + if (!closeInternal(errorCode)) { + return; // Already closed. + } + connection.writeSynResetLater(id, errorCode); + } + + /** Returns true if this stream was closed. */ + private boolean closeInternal(ErrorCode errorCode) { + assert (!Thread.holdsLock(this)); + synchronized (this) { + if (this.errorCode != null) { + return false; + } + if (source.finished && sink.finished) { + return false; + } + this.errorCode = errorCode; + notifyAll(); + } + connection.removeStream(id); + return true; + } + + void receiveHeaders(List
headers, HeadersMode headersMode) { + assert (!Thread.holdsLock(SpdyStream.this)); + ErrorCode errorCode = null; + boolean open = true; + synchronized (this) { + if (responseHeaders == null) { + if (headersMode.failIfHeadersAbsent()) { + errorCode = ErrorCode.PROTOCOL_ERROR; + } else { + responseHeaders = headers; + open = isOpen(); + notifyAll(); + } + } else { + if (headersMode.failIfHeadersPresent()) { + errorCode = ErrorCode.STREAM_IN_USE; + } else { + List
newHeaders = new ArrayList
(); + newHeaders.addAll(responseHeaders); + newHeaders.addAll(headers); + this.responseHeaders = newHeaders; + } + } + } + if (errorCode != null) { + closeLater(errorCode); + } else if (!open) { + connection.removeStream(id); + } + } + + void receiveData(BufferedSource in, int length) throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + this.source.receive(in, length); + } + + void receiveFin() { + assert (!Thread.holdsLock(SpdyStream.this)); + boolean open; + synchronized (this) { + this.source.finished = true; + open = isOpen(); + notifyAll(); + } + if (!open) { + connection.removeStream(id); + } + } + + synchronized void receiveRstStream(ErrorCode errorCode) { + if (this.errorCode == null) { + this.errorCode = errorCode; + notifyAll(); + } + } + + /** + * A source that reads the incoming data frames of a stream. Although this + * class uses synchronization to safely receive incoming data frames, it is + * not intended for use by multiple readers. + */ + private final class SpdyDataSource implements Source { + /** Buffer to receive data from the network into. Only accessed by the reader thread. */ + private final Buffer receiveBuffer = new Buffer(); + + /** Buffer with readable data. Guarded by SpdyStream.this. */ + private final Buffer readBuffer = new Buffer(); + + /** Maximum number of bytes to buffer before reporting a flow control error. */ + private final long maxByteCount; + + /** True if the caller has closed this stream. */ + private boolean closed; + + /** + * True if either side has cleanly shut down this stream. We will + * receive no more bytes beyond those already in the buffer. + */ + private boolean finished; + + private SpdyDataSource(long maxByteCount) { + this.maxByteCount = maxByteCount; + } + + @Override public long read(Buffer sink, long byteCount) + throws IOException { + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + + long read; + synchronized (SpdyStream.this) { + waitUntilReadable(); + checkNotClosed(); + if (readBuffer.size() == 0) return -1; // This source is exhausted. + + // Move bytes from the read buffer into the caller's buffer. + read = readBuffer.read(sink, Math.min(byteCount, readBuffer.size())); + + // Flow control: notify the peer that we're ready for more data! + unacknowledgedBytesRead += read; + if (unacknowledgedBytesRead + >= connection.okHttpSettings.getInitialWindowSize(Settings.DEFAULT_INITIAL_WINDOW_SIZE) / 2) { + connection.writeWindowUpdateLater(id, unacknowledgedBytesRead); + unacknowledgedBytesRead = 0; + } + } + + // Update connection.unacknowledgedBytesRead outside the stream lock. + synchronized (connection) { // Multiple application threads may hit this section. + connection.unacknowledgedBytesRead += read; + if (connection.unacknowledgedBytesRead + >= connection.okHttpSettings.getInitialWindowSize(Settings.DEFAULT_INITIAL_WINDOW_SIZE) / 2) { + connection.writeWindowUpdateLater(0, connection.unacknowledgedBytesRead); + connection.unacknowledgedBytesRead = 0; + } + } + + return read; + } + + /** Returns once the source is either readable or finished. */ + private void waitUntilReadable() throws IOException { + readTimeout.enter(); + try { + while (readBuffer.size() == 0 && !finished && !closed && errorCode == null) { + waitForIo(); + } + } finally { + readTimeout.exitAndThrowIfTimedOut(); + } + } + + void receive(BufferedSource in, long byteCount) throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + + while (byteCount > 0) { + boolean finished; + boolean flowControlError; + synchronized (SpdyStream.this) { + finished = this.finished; + flowControlError = byteCount + readBuffer.size() > maxByteCount; + } + + // If the peer sends more data than we can handle, discard it and close the connection. + if (flowControlError) { + in.skip(byteCount); + closeLater(ErrorCode.FLOW_CONTROL_ERROR); + return; + } + + // Discard data received after the stream is finished. It's probably a benign race. + if (finished) { + in.skip(byteCount); + return; + } + + // Fill the receive buffer without holding any locks. + long read = in.read(receiveBuffer, byteCount); + if (read == -1) throw new EOFException(); + byteCount -= read; + + // Move the received data to the read buffer to the reader can read it. + synchronized (SpdyStream.this) { + boolean wasEmpty = readBuffer.size() == 0; + readBuffer.writeAll(receiveBuffer); + if (wasEmpty) { + SpdyStream.this.notifyAll(); + } + } + } + } + + @Override public Timeout timeout() { + return readTimeout; + } + + @Override public void close() throws IOException { + synchronized (SpdyStream.this) { + closed = true; + readBuffer.clear(); + SpdyStream.this.notifyAll(); + } + cancelStreamIfNecessary(); + } + + private void checkNotClosed() throws IOException { + if (closed) { + throw new IOException("stream closed"); + } + if (errorCode != null) { + throw new IOException("stream was reset: " + errorCode); + } + } + } + + private void cancelStreamIfNecessary() throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + boolean open; + boolean cancel; + synchronized (this) { + cancel = !source.finished && source.closed && (sink.finished || sink.closed); + open = isOpen(); + } + if (cancel) { + // RST this stream to prevent additional data from being sent. This + // is safe because the input stream is closed (we won't use any + // further bytes) and the output stream is either finished or closed + // (so RSTing both streams doesn't cause harm). + SpdyStream.this.close(ErrorCode.CANCEL); + } else if (!open) { + connection.removeStream(id); + } + } + + /** + * A sink that writes outgoing data frames of a stream. This class is not + * thread safe. + */ + final class SpdyDataSink implements Sink { + private boolean closed; + + /** + * True if either side has cleanly shut down this stream. We shall send + * no more bytes. + */ + private boolean finished; + + @Override public void write(Buffer source, long byteCount) throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + while (byteCount > 0) { + long toWrite; + synchronized (SpdyStream.this) { + writeTimeout.enter(); + try { + while (bytesLeftInWriteWindow <= 0 && !finished && !closed && errorCode == null) { + waitForIo(); // Wait until we receive a WINDOW_UPDATE. + } + } finally { + writeTimeout.exitAndThrowIfTimedOut(); + } + + checkOutNotClosed(); // Kick out if the stream was reset or closed while waiting. + toWrite = Math.min(bytesLeftInWriteWindow, byteCount); + bytesLeftInWriteWindow -= toWrite; + } + + byteCount -= toWrite; + connection.writeData(id, false, source, toWrite); + } + } + + @Override public void flush() throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + synchronized (SpdyStream.this) { + checkOutNotClosed(); + } + connection.flush(); + } + + @Override public Timeout timeout() { + return writeTimeout; + } + + @Override public void close() throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + synchronized (SpdyStream.this) { + if (closed) return; + } + if (!sink.finished) { + connection.writeData(id, true, null, 0); + } + synchronized (SpdyStream.this) { + closed = true; + } + connection.flush(); + cancelStreamIfNecessary(); + } + } + + /** + * {@code delta} will be negative if a settings frame initial window is + * smaller than the last. + */ + void addBytesToWriteWindow(long delta) { + bytesLeftInWriteWindow += delta; + if (delta > 0) SpdyStream.this.notifyAll(); + } + + private void checkOutNotClosed() throws IOException { + if (sink.closed) { + throw new IOException("stream closed"); + } else if (sink.finished) { + throw new IOException("stream finished"); + } else if (errorCode != null) { + throw new IOException("stream was reset: " + errorCode); + } + } + + /** + * Like {@link #wait}, but throws an {@code InterruptedIOException} when + * interrupted instead of the more awkward {@link InterruptedException}. + */ + private void waitForIo() throws InterruptedIOException { + try { + wait(); + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } + } + + /** + * The Okio timeout watchdog will call {@link #timedOut} if the timeout is + * reached. In that case we close the stream (asynchronously) which will + * notify the waiting thread. + */ + class SpdyTimeout extends AsyncTimeout { + @Override protected void timedOut() { + closeLater(ErrorCode.CANCEL); + } + + public void exitAndThrowIfTimedOut() throws InterruptedIOException { + if (exit()) throw new InterruptedIOException("timeout"); + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Variant.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Variant.java new file mode 100755 index 00000000..1cc443c0 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/spdy/Variant.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okhttp.internal.spdy; + +import com.contentstack.okhttp.Protocol; +import com.contentstack.okio.BufferedSink; +import com.contentstack.okio.BufferedSource; + +/** A version and dialect of the framed socket protocol. */ +public interface Variant { + + /** The protocol as selected using NPN or ALPN. */ + Protocol getProtocol(); + + /** + * @param client true if this is the HTTP client's reader, reading frames from a server. + */ + FrameReader newReader(BufferedSource source, boolean client); + + /** + * @param client true if this is the HTTP client's writer, writing frames to a server. + */ + FrameWriter newWriter(BufferedSink sink, boolean client); + + int maxFrameSize(); +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/tls/DistinguishedNameParser.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/tls/DistinguishedNameParser.java new file mode 100755 index 00000000..f11103e3 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/tls/DistinguishedNameParser.java @@ -0,0 +1,406 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contentstack.okhttp.internal.tls; + +import javax.security.auth.x500.X500Principal; + +/** + * A distinguished name (DN) parser. This parser only supports extracting a + * string value from a DN. It doesn't support values in the hex-string style. + */ +final class DistinguishedNameParser { + private final String dn; + private final int length; + private int pos; + private int beg; + private int end; + + /** Temporary variable to store positions of the currently parsed item. */ + private int cur; + + /** Distinguished name characters. */ + private char[] chars; + + public DistinguishedNameParser(X500Principal principal) { + // RFC2253 is used to ensure we get attributes in the reverse + // order of the underlying ASN.1 encoding, so that the most + // significant values of repeated attributes occur first. + this.dn = principal.getName(X500Principal.RFC2253); + this.length = this.dn.length(); + } + + // gets next attribute type: (ALPHA 1*keychar) / oid + private String nextAT() { + // skip preceding space chars, they can present after + // comma or semicolon (compatibility with RFC 1779) + for (; pos < length && chars[pos] == ' '; pos++) { + } + if (pos == length) { + return null; // reached the end of DN + } + + // mark the beginning of attribute type + beg = pos; + + // attribute type chars + pos++; + for (; pos < length && chars[pos] != '=' && chars[pos] != ' '; pos++) { + // we don't follow exact BNF syntax here: + // accept any char except space and '=' + } + if (pos >= length) { + throw new IllegalStateException("Unexpected end of DN: " + dn); + } + + // mark the end of attribute type + end = pos; + + // skip trailing space chars between attribute type and '=' + // (compatibility with RFC 1779) + if (chars[pos] == ' ') { + for (; pos < length && chars[pos] != '=' && chars[pos] == ' '; pos++) { + } + + if (chars[pos] != '=' || pos == length) { + throw new IllegalStateException("Unexpected end of DN: " + dn); + } + } + + pos++; //skip '=' char + + // skip space chars between '=' and attribute value + // (compatibility with RFC 1779) + for (; pos < length && chars[pos] == ' '; pos++) { + } + + // in case of oid attribute type skip its prefix: "oid." or "OID." + // (compatibility with RFC 1779) + if ((end - beg > 4) && (chars[beg + 3] == '.') + && (chars[beg] == 'O' || chars[beg] == 'o') + && (chars[beg + 1] == 'I' || chars[beg + 1] == 'i') + && (chars[beg + 2] == 'D' || chars[beg + 2] == 'd')) { + beg += 4; + } + + return new String(chars, beg, end - beg); + } + + // gets quoted attribute value: QUOTATION *( quotechar / pair ) QUOTATION + private String quotedAV() { + pos++; + beg = pos; + end = beg; + while (true) { + + if (pos == length) { + throw new IllegalStateException("Unexpected end of DN: " + dn); + } + + if (chars[pos] == '"') { + // enclosing quotation was found + pos++; + break; + } else if (chars[pos] == '\\') { + chars[end] = getEscaped(); + } else { + // shift char: required for string with escaped chars + chars[end] = chars[pos]; + } + pos++; + end++; + } + + // skip trailing space chars before comma or semicolon. + // (compatibility with RFC 1779) + for (; pos < length && chars[pos] == ' '; pos++) { + } + + return new String(chars, beg, end - beg); + } + + // gets hex string attribute value: "#" hexstring + private String hexAV() { + if (pos + 4 >= length) { + // encoded byte array must be not less then 4 c + throw new IllegalStateException("Unexpected end of DN: " + dn); + } + + beg = pos; // store '#' position + pos++; + while (true) { + + // check for end of attribute value + // looks for space and component separators + if (pos == length || chars[pos] == '+' || chars[pos] == ',' + || chars[pos] == ';') { + end = pos; + break; + } + + if (chars[pos] == ' ') { + end = pos; + pos++; + // skip trailing space chars before comma or semicolon. + // (compatibility with RFC 1779) + for (; pos < length && chars[pos] == ' '; pos++) { + } + break; + } else if (chars[pos] >= 'A' && chars[pos] <= 'F') { + chars[pos] += 32; //to low case + } + + pos++; + } + + // verify length of hex string + // encoded byte array must be not less then 4 and must be even number + int hexLen = end - beg; // skip first '#' char + if (hexLen < 5 || (hexLen & 1) == 0) { + throw new IllegalStateException("Unexpected end of DN: " + dn); + } + + // get byte encoding from string representation + byte[] encoded = new byte[hexLen / 2]; + for (int i = 0, p = beg + 1; i < encoded.length; p += 2, i++) { + encoded[i] = (byte) getByte(p); + } + + return new String(chars, beg, hexLen); + } + + // gets string attribute value: *( stringchar / pair ) + private String escapedAV() { + beg = pos; + end = pos; + while (true) { + if (pos >= length) { + // the end of DN has been found + return new String(chars, beg, end - beg); + } + + switch (chars[pos]) { + case '+': + case ',': + case ';': + // separator char has been found + return new String(chars, beg, end - beg); + case '\\': + // escaped char + chars[end++] = getEscaped(); + pos++; + break; + case ' ': + // need to figure out whether space defines + // the end of attribute value or not + cur = end; + + pos++; + chars[end++] = ' '; + + for (; pos < length && chars[pos] == ' '; pos++) { + chars[end++] = ' '; + } + if (pos == length || chars[pos] == ',' || chars[pos] == '+' + || chars[pos] == ';') { + // separator char or the end of DN has been found + return new String(chars, beg, cur - beg); + } + break; + default: + chars[end++] = chars[pos]; + pos++; + } + } + } + + // returns escaped char + private char getEscaped() { + pos++; + if (pos == length) { + throw new IllegalStateException("Unexpected end of DN: " + dn); + } + + switch (chars[pos]) { + case '"': + case '\\': + case ',': + case '=': + case '+': + case '<': + case '>': + case '#': + case ';': + case ' ': + case '*': + case '%': + case '_': + return chars[pos]; + default: + // RFC doesn't explicitly say that escaped hex pair is + // interpreted as UTF-8 char. It only contains an example of such DN. + return getUTF8(); + } + } + + // decodes UTF-8 char + // see http://www.unicode.org for UTF-8 bit distribution table + private char getUTF8() { + int res = getByte(pos); + pos++; + + if (res < 128) { // one byte: 0-7F + return (char) res; + } else if (res >= 192 && res <= 247) { + + int count; + if (res <= 223) { // two bytes: C0-DF + count = 1; + res = res & 0x1F; + } else if (res <= 239) { // three bytes: E0-EF + count = 2; + res = res & 0x0F; + } else { // four bytes: F0-F7 + count = 3; + res = res & 0x07; + } + + int b; + for (int i = 0; i < count; i++) { + pos++; + if (pos == length || chars[pos] != '\\') { + return 0x3F; + } + pos++; + + b = getByte(pos); + pos++; + if ((b & 0xC0) != 0x80) { + return 0x3F; + } + + res = (res << 6) + (b & 0x3F); + } + return (char) res; + } else { + return 0x3F; + } + } + + // Returns byte representation of a char pair + // The char pair is composed of DN char in + // specified 'position' and the next char + // According to BNF syntax: + // hexchar = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" + // / "a" / "b" / "c" / "d" / "e" / "f" + private int getByte(int position) { + if (position + 1 >= length) { + throw new IllegalStateException("Malformed DN: " + dn); + } + + int b1, b2; + + b1 = chars[position]; + if (b1 >= '0' && b1 <= '9') { + b1 = b1 - '0'; + } else if (b1 >= 'a' && b1 <= 'f') { + b1 = b1 - 87; // 87 = 'a' - 10 + } else if (b1 >= 'A' && b1 <= 'F') { + b1 = b1 - 55; // 55 = 'A' - 10 + } else { + throw new IllegalStateException("Malformed DN: " + dn); + } + + b2 = chars[position + 1]; + if (b2 >= '0' && b2 <= '9') { + b2 = b2 - '0'; + } else if (b2 >= 'a' && b2 <= 'f') { + b2 = b2 - 87; // 87 = 'a' - 10 + } else if (b2 >= 'A' && b2 <= 'F') { + b2 = b2 - 55; // 55 = 'A' - 10 + } else { + throw new IllegalStateException("Malformed DN: " + dn); + } + + return (b1 << 4) + b2; + } + + /** + * Parses the DN and returns the most significant attribute value + * for an attribute type, or null if none found. + * + * @param attributeType attribute type to look for (e.g. "ca") + */ + public String findMostSpecific(String attributeType) { + // Initialize internal state. + pos = 0; + beg = 0; + end = 0; + cur = 0; + chars = dn.toCharArray(); + + String attType = nextAT(); + if (attType == null) { + return null; + } + while (true) { + String attValue = ""; + + if (pos == length) { + return null; + } + + switch (chars[pos]) { + case '"': + attValue = quotedAV(); + break; + case '#': + attValue = hexAV(); + break; + case '+': + case ',': + case ';': // compatibility with RFC 1779: semicolon can separate RDNs + //empty attribute value + break; + default: + attValue = escapedAV(); + } + + // Values are ordered from most specific to least specific + // due to the RFC2253 formatting. So take the first match + // we see. + if (attributeType.equalsIgnoreCase(attType)) { + return attValue; + } + + if (pos >= length) { + return null; + } + + if (chars[pos] == ',' || chars[pos] == ';') { + } else if (chars[pos] != '+') { + throw new IllegalStateException("Malformed DN: " + dn); + } + + pos++; + attType = nextAT(); + if (attType == null) { + throw new IllegalStateException("Malformed DN: " + dn); + } + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okhttp/internal/tls/OkHostnameVerifier.java b/contentstack/src/main/java/com/contentstack/okhttp/internal/tls/OkHostnameVerifier.java new file mode 100755 index 00000000..2839aa0e --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okhttp/internal/tls/OkHostnameVerifier.java @@ -0,0 +1,192 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contentstack.okhttp.internal.tls; + +import java.security.cert.Certificate; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; +import javax.security.auth.x500.X500Principal; + +/** + * A HostnameVerifier consistent with RFC 2818. + */ +public final class OkHostnameVerifier implements HostnameVerifier { + public static final OkHostnameVerifier INSTANCE = new OkHostnameVerifier(); + + /** + * Quick and dirty pattern to differentiate IP addresses from hostnames. This + * is an approximation of Android's private InetAddress#isNumeric API. + * + *

This matches IPv6 addresses as a hex string containing at least one + * colon, and possibly including dots after the first colon. It matches IPv4 + * addresses as strings containing only decimal digits and dots. This pattern + * matches strings like "a:.23" and "54" that are neither IP addresses nor + * hostnames; they will be verified as IP addresses (which is a more strict + * verification). + */ + private static final Pattern VERIFY_AS_IP_ADDRESS = Pattern.compile( + "([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)"); + + private static final int ALT_DNS_NAME = 2; + private static final int ALT_IPA_NAME = 7; + + private OkHostnameVerifier() { + } + + public boolean verify(String host, SSLSession session) { + try { + Certificate[] certificates = session.getPeerCertificates(); + return verify(host, (X509Certificate) certificates[0]); + } catch (SSLException e) { + return false; + } + } + + public boolean verify(String host, X509Certificate certificate) { + return verifyAsIpAddress(host) + ? verifyIpAddress(host, certificate) + : verifyHostName(host, certificate); + } + + static boolean verifyAsIpAddress(String host) { + return VERIFY_AS_IP_ADDRESS.matcher(host).matches(); + } + + /** + * Returns true if {@code certificate} matches {@code ipAddress}. + */ + private boolean verifyIpAddress(String ipAddress, X509Certificate certificate) { + for (String altName : getSubjectAltNames(certificate, ALT_IPA_NAME)) { + if (ipAddress.equalsIgnoreCase(altName)) { + return true; + } + } + return false; + } + + /** + * Returns true if {@code certificate} matches {@code hostName}. + */ + private boolean verifyHostName(String hostName, X509Certificate certificate) { + hostName = hostName.toLowerCase(Locale.US); + boolean hasDns = false; + for (String altName : getSubjectAltNames(certificate, ALT_DNS_NAME)) { + hasDns = true; + if (verifyHostName(hostName, altName)) { + return true; + } + } + + if (!hasDns) { + X500Principal principal = certificate.getSubjectX500Principal(); + // RFC 2818 advises using the most specific name for matching. + String cn = new DistinguishedNameParser(principal).findMostSpecific("cn"); + if (cn != null) { + return verifyHostName(hostName, cn); + } + } + + return false; + } + + private List getSubjectAltNames(X509Certificate certificate, int type) { + List result = new ArrayList(); + try { + Collection subjectAltNames = certificate.getSubjectAlternativeNames(); + if (subjectAltNames == null) { + return Collections.emptyList(); + } + for (Object subjectAltName : subjectAltNames) { + List entry = (List) subjectAltName; + if (entry == null || entry.size() < 2) { + continue; + } + Integer altNameType = (Integer) entry.get(0); + if (altNameType == null) { + continue; + } + if (altNameType == type) { + String altName = (String) entry.get(1); + if (altName != null) { + result.add(altName); + } + } + } + return result; + } catch (CertificateParsingException e) { + return Collections.emptyList(); + } + } + + /** + * Returns true if {@code hostName} matches the name or pattern {@code cn}. + * + * @param hostName lowercase host name. + * @param cn certificate host name. May include wildcards like + * {@code *.android.com}. + */ + public boolean verifyHostName(String hostName, String cn) { + // Check length == 0 instead of .isEmpty() to support Java 5. + if (hostName == null || hostName.length() == 0 || cn == null || cn.length() == 0) { + return false; + } + + cn = cn.toLowerCase(Locale.US); + + if (!cn.contains("*")) { + return hostName.equals(cn); + } + + if (cn.startsWith("*.") && hostName.regionMatches(0, cn, 2, cn.length() - 2)) { + return true; // "*.foo.com" matches "foo.com" + } + + int asterisk = cn.indexOf('*'); + int dot = cn.indexOf('.'); + if (asterisk > dot) { + return false; // malformed; wildcard must be in the first part of the cn + } + + if (!hostName.regionMatches(0, cn, 0, asterisk)) { + return false; // prefix before '*' doesn't match + } + + int suffixLength = cn.length() - (asterisk + 1); + int suffixStart = hostName.length() - suffixLength; + if (hostName.indexOf('.', asterisk) < suffixStart) { + return false; // wildcard '*' can't match a '.' + } + + if (!hostName.regionMatches(suffixStart, cn, asterisk + 1, suffixLength)) { + return false; // suffix after '*' doesn't match + } + + return true; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okio/AsyncTimeout.java b/contentstack/src/main/java/com/contentstack/okio/AsyncTimeout.java new file mode 100755 index 00000000..87f53938 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/AsyncTimeout.java @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okio; + +import java.io.IOException; +import java.io.InterruptedIOException; + +/** + * This timeout uses a background thread to take action exactly when the timeout + * occurs. Use this to implement timeouts where they aren't supported natively, + * such as to sockets that are blocked on writing. + * + *

Subclasses should override {@link #timedOut} to take action when a timeout + * occurs. This method will be invoked by the shared watchdog thread so it + * should not do any long-running operations. Otherwise we risk starving other + * timeouts from being triggered. + * + *

Use {@link #sink} and {@link #source} to apply this timeout to a stream. + * The returned value will apply the timeout to each operation on the wrapped + * stream. + * + *

Callers should call {@link #enter} before doing work that is subject to + * timeouts, and {@link #exit} afterwards. The return value of {@link #exit} + * indicates whether a timeout was triggered. Note that the call to {@link + * #timedOut} is asynchronous, and may be called after {@link #exit}. + */ +public class AsyncTimeout extends Timeout { + /** + * The watchdog thread processes a linked list of pending timeouts, sorted in + * the order to be triggered. This class synchronizes on AsyncTimeout.class. + * This lock guards the queue. + * + *

Head's 'next' points to the first element of the linked list. The first + * element is the next node to time out, or null if the queue is empty. The + * head is null until the watchdog thread is started. + */ + private static AsyncTimeout head; + + /** True if this node is currently in the queue. */ + private boolean inQueue; + + /** The next node in the linked list. */ + private AsyncTimeout next; + + /** If scheduled, this is the time that the watchdog should time this out. */ + private long timeoutAt; + + public final void enter() { + if (inQueue) throw new IllegalStateException("Unbalanced enter/exit"); + long timeoutNanos = timeoutNanos(); + boolean hasDeadline = hasDeadline(); + if (timeoutNanos == 0 && !hasDeadline) { + return; // No timeout and no deadline? Don't bother with the queue. + } + inQueue = true; + scheduleTimeout(this, timeoutNanos, hasDeadline); + } + + private static synchronized void scheduleTimeout( + AsyncTimeout node, long timeoutNanos, boolean hasDeadline) { + // Start the watchdog thread and create the head node when the first timeout is scheduled. + if (head == null) { + head = new AsyncTimeout(); + new Watchdog().start(); + } + + long now = System.nanoTime(); + if (timeoutNanos != 0 && hasDeadline) { + // Compute the earliest event; either timeout or deadline. Because nanoTime can wrap around, + // Math.min() is undefined for absolute values, but meaningful for relative ones. + node.timeoutAt = now + Math.min(timeoutNanos, node.deadlineNanoTime() - now); + } else if (timeoutNanos != 0) { + node.timeoutAt = now + timeoutNanos; + } else if (hasDeadline) { + node.timeoutAt = node.deadlineNanoTime(); + } else { + throw new AssertionError(); + } + + // Insert the node in sorted order. + long remainingNanos = node.remainingNanos(now); + for (AsyncTimeout prev = head; true; prev = prev.next) { + if (prev.next == null || remainingNanos < prev.next.remainingNanos(now)) { + node.next = prev.next; + prev.next = node; + if (prev == head) { + AsyncTimeout.class.notify(); // Wake up the watchdog when inserting at the front. + } + break; + } + } + } + + /** Returns true if the timeout occurred. */ + public final boolean exit() { + if (!inQueue) return false; + inQueue = false; + return cancelScheduledTimeout(this); + } + + /** Returns true if the timeout occurred. */ + private static synchronized boolean cancelScheduledTimeout(AsyncTimeout node) { + // Remove the node from the linked list. + for (AsyncTimeout prev = head; prev != null; prev = prev.next) { + if (prev.next == node) { + prev.next = node.next; + node.next = null; + return false; + } + } + + // The node wasn't found in the linked list: it must have timed out! + return true; + } + + /** + * Returns the amount of time left until the time out. This will be negative + * if the timeout has elapsed and the timeout should occur immediately. + */ + private long remainingNanos(long now) { + return timeoutAt - now; + } + + /** + * Invoked by the watchdog thread when the time between calls to {@link + * #enter()} and {@link #exit()} has exceeded the timeout. + */ + protected void timedOut() { + } + + /** + * Returns a new sink that delegates to {@code sink}, using this to implement + * timeouts. This works best if {@link #timedOut} is overridden to interrupt + * {@code sink}'s current operation. + */ + public final Sink sink(final Sink sink) { + return new Sink() { + @Override public void write(Buffer source, long byteCount) throws IOException { + boolean throwOnTimeout = false; + enter(); + try { + sink.write(source, byteCount); + throwOnTimeout = true; + } catch (IOException e) { + throw exit(e); + } finally { + exit(throwOnTimeout); + } + } + + @Override public void flush() throws IOException { + boolean throwOnTimeout = false; + enter(); + try { + sink.flush(); + throwOnTimeout = true; + } catch (IOException e) { + throw exit(e); + } finally { + exit(throwOnTimeout); + } + } + + @Override public void close() throws IOException { + boolean throwOnTimeout = false; + enter(); + try { + sink.close(); + throwOnTimeout = true; + } catch (IOException e) { + throw exit(e); + } finally { + exit(throwOnTimeout); + } + } + + @Override public Timeout timeout() { + return AsyncTimeout.this; + } + + @Override public String toString() { + return "AsyncTimeout.sink(" + sink + ")"; + } + }; + } + + /** + * Returns a new source that delegates to {@code source}, using this to + * implement timeouts. This works best if {@link #timedOut} is overridden to + * interrupt {@code sink}'s current operation. + */ + public final Source source(final Source source) { + return new Source() { + @Override public long read(Buffer sink, long byteCount) throws IOException { + boolean throwOnTimeout = false; + enter(); + try { + long result = source.read(sink, byteCount); + throwOnTimeout = true; + return result; + } catch (IOException e) { + throw exit(e); + } finally { + exit(throwOnTimeout); + } + } + + @Override public void close() throws IOException { + boolean throwOnTimeout = false; + try { + source.close(); + throwOnTimeout = true; + } catch (IOException e) { + throw exit(e); + } finally { + exit(throwOnTimeout); + } + } + + @Override public Timeout timeout() { + return AsyncTimeout.this; + } + + @Override public String toString() { + return "AsyncTimeout.source(" + source + ")"; + } + }; + } + + /** + * Throws an InterruptedIOException if {@code throwOnTimeout} is true and a + * timeout occurred. + */ + final void exit(boolean throwOnTimeout) throws IOException { + boolean timedOut = exit(); + if (timedOut && throwOnTimeout) throw new InterruptedIOException("timeout"); + } + + /** + * Returns either {@code cause} or an InterruptedIOException that's caused by + * {@code cause} if a timeout occurred. + */ + final IOException exit(IOException cause) throws IOException { + if (!exit()) return cause; + InterruptedIOException e = new InterruptedIOException("timeout"); + e.initCause(cause); + return e; + } + + private static final class Watchdog extends Thread { + public Watchdog() { + super("Okio Watchdog"); + setDaemon(true); + } + + public void run() { + while (true) { + try { + AsyncTimeout timedOut = awaitTimeout(); + + // Didn't find a node to interrupt. Try again. + if (timedOut == null) continue; + + // Close the timed out node. + timedOut.timedOut(); + } catch (InterruptedException ignored) { + } + } + } + } + + /** + * Removes and returns the node at the head of the list, waiting for it to + * time out if necessary. Returns null if the situation changes while waiting: + * either a newer node is inserted at the head, or the node being waited on + * has been removed. + */ + private static synchronized AsyncTimeout awaitTimeout() throws InterruptedException { + // Get the next eligible node. + AsyncTimeout node = head.next; + + // The queue is empty. Wait for something to be enqueued. + if (node == null) { + AsyncTimeout.class.wait(); + return null; + } + + long waitNanos = node.remainingNanos(System.nanoTime()); + + // The head of the queue hasn't timed out yet. Await that. + if (waitNanos > 0) { + // Waiting is made complicated by the fact that we work in nanoseconds, + // but the API wants (millis, nanos) in two arguments. + long waitMillis = waitNanos / 1000000L; + waitNanos -= (waitMillis * 1000000L); + AsyncTimeout.class.wait(waitMillis, (int) waitNanos); + return null; + } + + // The head of the queue has timed out. Remove it. + head.next = node.next; + node.next = null; + return node; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okio/Base64.java b/contentstack/src/main/java/com/contentstack/okio/Base64.java new file mode 100755 index 00000000..e42391c6 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/Base64.java @@ -0,0 +1,147 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @author Alexander Y. Kleymenov + */ +package com.contentstack.okio; + +import java.io.UnsupportedEncodingException; + +final class Base64 { + private Base64() { + } + + public static byte[] decode(String in) { + // Ignore trailing '=' padding and whitespace from the input. + int limit = in.length(); + for (; limit > 0; limit--) { + char c = in.charAt(limit - 1); + if (c != '=' && c != '\n' && c != '\r' && c != ' ' && c != '\t') { + break; + } + } + + // If the input includes whitespace, this output array will be longer than necessary. + byte[] out = new byte[(int) (limit * 6L / 8L)]; + int outCount = 0; + int inCount = 0; + + int word = 0; + for (int pos = 0; pos < limit; pos++) { + char c = in.charAt(pos); + + int bits; + if (c >= 'A' && c <= 'Z') { + // char ASCII value + // A 65 0 + // Z 90 25 (ASCII - 65) + bits = c - 65; + } else if (c >= 'a' && c <= 'z') { + // char ASCII value + // a 97 26 + // z 122 51 (ASCII - 71) + bits = c - 71; + } else if (c >= '0' && c <= '9') { + // char ASCII value + // 0 48 52 + // 9 57 61 (ASCII + 4) + bits = c + 4; + } else if (c == '+') { + bits = 62; + } else if (c == '/') { + bits = 63; + } else if (c == '\n' || c == '\r' || c == ' ' || c == '\t') { + continue; + } else { + return null; + } + + // Append this char's 6 bits to the word. + word = (word << 6) | (byte) bits; + + // For every 4 chars of input, we accumulate 24 bits of output. Emit 3 bytes. + inCount++; + if (inCount % 4 == 0) { + out[outCount++] = (byte) (word >> 16); + out[outCount++] = (byte) (word >> 8); + out[outCount++] = (byte) word; + } + } + + int lastWordChars = inCount % 4; + if (lastWordChars == 1) { + // We read 1 char followed by "===". But 6 bits is a truncated byte! Fail. + return null; + } else if (lastWordChars == 2) { + // We read 2 chars followed by "==". Emit 1 byte with 8 of those 12 bits. + word = word << 12; + out[outCount++] = (byte) (word >> 16); + } else if (lastWordChars == 3) { + // We read 3 chars, followed by "=". Emit 2 bytes for 16 of those 18 bits. + word = word << 6; + out[outCount++] = (byte) (word >> 16); + out[outCount++] = (byte) (word >> 8); + } + + // If we sized our out array perfectly, we're done. + if (outCount == out.length) return out; + + // Copy the decoded bytes to a new, right-sized array. + byte[] prefix = new byte[outCount]; + System.arraycopy(out, 0, prefix, 0, outCount); + return prefix; + } + + private static final byte[] MAP = new byte[] { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', + 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', + 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', + '5', '6', '7', '8', '9', '+', '/' + }; + + public static String encode(byte[] in) { + int length = (in.length + 2) * 4 / 3; + byte[] out = new byte[length]; + int index = 0, end = in.length - in.length % 3; + for (int i = 0; i < end; i += 3) { + out[index++] = MAP[(in[i] & 0xff) >> 2]; + out[index++] = MAP[((in[i] & 0x03) << 4) | ((in[i + 1] & 0xff) >> 4)]; + out[index++] = MAP[((in[i + 1] & 0x0f) << 2) | ((in[i + 2] & 0xff) >> 6)]; + out[index++] = MAP[(in[i + 2] & 0x3f)]; + } + switch (in.length % 3) { + case 1: + out[index++] = MAP[(in[end] & 0xff) >> 2]; + out[index++] = MAP[(in[end] & 0x03) << 4]; + out[index++] = '='; + out[index++] = '='; + break; + case 2: + out[index++] = MAP[(in[end] & 0xff) >> 2]; + out[index++] = MAP[((in[end] & 0x03) << 4) | ((in[end + 1] & 0xff) >> 4)]; + out[index++] = MAP[((in[end + 1] & 0x0f) << 2)]; + out[index++] = '='; + break; + } + try { + return new String(out, 0, index, "US-ASCII"); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okio/Buffer.java b/contentstack/src/main/java/com/contentstack/okio/Buffer.java new file mode 100755 index 00000000..cade1837 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/Buffer.java @@ -0,0 +1,945 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okio; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static com.contentstack.okio.Util.checkOffsetAndCount; +import static com.contentstack.okio.Util.reverseBytesLong; + +/** + * A collection of bytes in memory. + * + *

Moving data from one buffer to another is fast. Instead + * of copying bytes from one place in memory to another, this class just changes + * ownership of the underlying byte arrays. + * + *

This buffer grows with your data. Just like ArrayList, + * each buffer starts small. It consumes only the memory it needs to. + * + *

This buffer pools its byte arrays. When you allocate a + * byte array in Java, the runtime must zero-fill the requested array before + * returning it to you. Even if you're going to write over that space anyway. + * This class avoids zero-fill and GC churn by pooling byte arrays. + */ +public final class Buffer implements BufferedSource, BufferedSink, Cloneable { + Segment head; + long size; + + public Buffer() { + } + + /** Returns the number of bytes currently in this buffer. */ + public long size() { + return size; + } + + @Override public Buffer buffer() { + return this; + } + + @Override public OutputStream outputStream() { + return new OutputStream() { + @Override public void write(int b) { + writeByte((byte) b); + } + + @Override public void write(byte[] data, int offset, int byteCount) { + Buffer.this.write(data, offset, byteCount); + } + + @Override public void flush() { + } + + @Override public void close() { + } + + @Override public String toString() { + return this + ".outputStream()"; + } + }; + } + + @Override public Buffer emitCompleteSegments() { + return this; // Nowhere to emit to! + } + + @Override public boolean exhausted() { + return size == 0; + } + + @Override public void require(long byteCount) throws EOFException { + if (this.size < byteCount) throw new EOFException(); + } + + @Override public boolean request(long byteCount) throws IOException { + return size >= byteCount; + } + + @Override public InputStream inputStream() { + return new InputStream() { + @Override public int read() { + if (size > 0) return readByte() & 0xff; + return -1; + } + + @Override public int read(byte[] sink, int offset, int byteCount) { + return Buffer.this.read(sink, offset, byteCount); + } + + @Override public int available() { + return (int) Math.min(size, Integer.MAX_VALUE); + } + + @Override public void close() { + } + + @Override public String toString() { + return Buffer.this + ".inputStream()"; + } + }; + } + + /** Copy the contents of this to {@code out}. */ + public Buffer copyTo(OutputStream out) throws IOException { + return copyTo(out, 0, size); + } + + /** + * Copy {@code byteCount} bytes from this, starting at {@code offset}, to + * {@code out}. + */ + public Buffer copyTo(OutputStream out, long offset, long byteCount) throws IOException { + if (out == null) throw new IllegalArgumentException("out == null"); + checkOffsetAndCount(size, offset, byteCount); + if (byteCount == 0) return this; + + // Skip segments that we aren't copying from. + Segment s = head; + for (; offset >= (s.limit - s.pos); s = s.next) { + offset -= (s.limit - s.pos); + } + + // Copy from one segment at a time. + for (; byteCount > 0; s = s.next) { + int pos = (int) (s.pos + offset); + int toWrite = (int) Math.min(s.limit - pos, byteCount); + out.write(s.data, pos, toWrite); + byteCount -= toWrite; + offset = 0; + } + + return this; + } + + /** Write the contents of this to {@code out}. */ + public Buffer writeTo(OutputStream out) throws IOException { + return writeTo(out, size); + } + + /** Write {@code byteCount} bytes from this to {@code out}. */ + public Buffer writeTo(OutputStream out, long byteCount) throws IOException { + if (out == null) throw new IllegalArgumentException("out == null"); + checkOffsetAndCount(size, 0, byteCount); + + Segment s = head; + while (byteCount > 0) { + int toCopy = (int) Math.min(byteCount, s.limit - s.pos); + out.write(s.data, s.pos, toCopy); + + s.pos += toCopy; + size -= toCopy; + byteCount -= toCopy; + + if (s.pos == s.limit) { + Segment toRecycle = s; + head = s = toRecycle.pop(); + SegmentPool.INSTANCE.recycle(toRecycle); + } + } + + return this; + } + + /** Read and exhaust bytes from {@code in} to this. */ + public Buffer readFrom(InputStream in) throws IOException { + readFrom(in, Long.MAX_VALUE, true); + return this; + } + + /** Read {@code byteCount} bytes from {@code in} to this. */ + public Buffer readFrom(InputStream in, long byteCount) throws IOException { + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + readFrom(in, byteCount, false); + return this; + } + + private void readFrom(InputStream in, long byteCount, boolean forever) throws IOException { + if (in == null) throw new IllegalArgumentException("in == null"); + while (byteCount > 0 || forever) { + Segment tail = writableSegment(1); + int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit); + int bytesRead = in.read(tail.data, tail.limit, maxToCopy); + if (bytesRead == -1) { + if (forever) return; + throw new EOFException(); + } + tail.limit += bytesRead; + size += bytesRead; + byteCount -= bytesRead; + } + } + + /** + * Returns the number of bytes in segments that are not writable. This is the + * number of bytes that can be flushed immediately to an underlying sink + * without harming throughput. + */ + public long completeSegmentByteCount() { + long result = size; + if (result == 0) return 0; + + // Omit the tail if it's still writable. + Segment tail = head.prev; + if (tail.limit < Segment.SIZE) { + result -= tail.limit - tail.pos; + } + + return result; + } + + @Override public byte readByte() { + if (size == 0) throw new IllegalStateException("size == 0"); + + Segment segment = head; + int pos = segment.pos; + int limit = segment.limit; + + byte[] data = segment.data; + byte b = data[pos++]; + size -= 1; + + if (pos == limit) { + head = segment.pop(); + SegmentPool.INSTANCE.recycle(segment); + } else { + segment.pos = pos; + } + + return b; + } + + /** Returns the byte at {@code pos}. */ + public byte getByte(long pos) { + checkOffsetAndCount(size, pos, 1); + for (Segment s = head; true; s = s.next) { + int segmentByteCount = s.limit - s.pos; + if (pos < segmentByteCount) return s.data[s.pos + (int) pos]; + pos -= segmentByteCount; + } + } + + @Override public short readShort() { + if (size < 2) throw new IllegalStateException("size < 2: " + size); + + Segment segment = head; + int pos = segment.pos; + int limit = segment.limit; + + // If the short is split across multiple segments, delegate to readByte(). + if (limit - pos < 2) { + int s = (readByte() & 0xff) << 8 + | (readByte() & 0xff); + return (short) s; + } + + byte[] data = segment.data; + int s = (data[pos++] & 0xff) << 8 + | (data[pos++] & 0xff); + size -= 2; + + if (pos == limit) { + head = segment.pop(); + SegmentPool.INSTANCE.recycle(segment); + } else { + segment.pos = pos; + } + + return (short) s; + } + + @Override public int readInt() { + if (size < 4) throw new IllegalStateException("size < 4: " + size); + + Segment segment = head; + int pos = segment.pos; + int limit = segment.limit; + + // If the int is split across multiple segments, delegate to readByte(). + if (limit - pos < 4) { + return (readByte() & 0xff) << 24 + | (readByte() & 0xff) << 16 + | (readByte() & 0xff) << 8 + | (readByte() & 0xff); + } + + byte[] data = segment.data; + int i = (data[pos++] & 0xff) << 24 + | (data[pos++] & 0xff) << 16 + | (data[pos++] & 0xff) << 8 + | (data[pos++] & 0xff); + size -= 4; + + if (pos == limit) { + head = segment.pop(); + SegmentPool.INSTANCE.recycle(segment); + } else { + segment.pos = pos; + } + + return i; + } + + @Override public long readLong() { + if (size < 8) throw new IllegalStateException("size < 8: " + size); + + Segment segment = head; + int pos = segment.pos; + int limit = segment.limit; + + // If the long is split across multiple segments, delegate to readInt(). + if (limit - pos < 8) { + return (readInt() & 0xffffffffL) << 32 + | (readInt() & 0xffffffffL); + } + + byte[] data = segment.data; + long v = (data[pos++] & 0xffL) << 56 + | (data[pos++] & 0xffL) << 48 + | (data[pos++] & 0xffL) << 40 + | (data[pos++] & 0xffL) << 32 + | (data[pos++] & 0xffL) << 24 + | (data[pos++] & 0xffL) << 16 + | (data[pos++] & 0xffL) << 8 + | (data[pos++] & 0xffL); + size -= 8; + + if (pos == limit) { + head = segment.pop(); + SegmentPool.INSTANCE.recycle(segment); + } else { + segment.pos = pos; + } + + return v; + } + + @Override public short readShortLe() { + return Util.reverseBytesShort(readShort()); + } + + @Override public int readIntLe() { + return Util.reverseBytesInt(readInt()); + } + + @Override public long readLongLe() { + return Util.reverseBytesLong(readLong()); + } + + @Override public ByteString readByteString() { + return new ByteString(readByteArray()); + } + + @Override public ByteString readByteString(long byteCount) throws EOFException { + return new ByteString(readByteArray(byteCount)); + } + + @Override public void readFully(Buffer sink, long byteCount) throws EOFException { + if (size < byteCount) { + sink.write(this, size); // Exhaust ourselves. + throw new EOFException(); + } + sink.write(this, byteCount); + } + + @Override public long readAll(Sink sink) throws IOException { + long byteCount = size; + if (byteCount > 0) { + sink.write(this, byteCount); + } + return byteCount; + } + + @Override public String readUtf8() { + try { + return readString(size, Util.UTF_8); + } catch (EOFException e) { + throw new AssertionError(e); + } + } + + @Override public String readUtf8(long byteCount) throws EOFException { + return readString(byteCount, Util.UTF_8); + } + + @Override public String readString(Charset charset) { + try { + return readString(size, charset); + } catch (EOFException e) { + throw new AssertionError(e); + } + } + + @Override public String readString(long byteCount, Charset charset) throws EOFException { + checkOffsetAndCount(size, 0, byteCount); + if (charset == null) throw new IllegalArgumentException("charset == null"); + if (byteCount > Integer.MAX_VALUE) { + throw new IllegalArgumentException("byteCount > Integer.MAX_VALUE: " + byteCount); + } + if (byteCount == 0) return ""; + + Segment head = this.head; + if (head.pos + byteCount > head.limit) { + // If the string spans multiple segments, delegate to readBytes(). + return new String(readByteArray(byteCount), charset); + } + + String result = new String(head.data, head.pos, (int) byteCount, charset); + head.pos += byteCount; + size -= byteCount; + + if (head.pos == head.limit) { + this.head = head.pop(); + SegmentPool.INSTANCE.recycle(head); + } + + return result; + } + + @Override public String readUtf8Line() throws EOFException { + long newline = indexOf((byte) '\n'); + + if (newline == -1) { + return size != 0 ? readUtf8(size) : null; + } + + return readUtf8Line(newline); + } + + @Override public String readUtf8LineStrict() throws EOFException { + long newline = indexOf((byte) '\n'); + if (newline == -1) throw new EOFException(); + return readUtf8Line(newline); + } + + String readUtf8Line(long newline) throws EOFException { + if (newline > 0 && getByte(newline - 1) == '\r') { + // Read everything until '\r\n', then skip the '\r\n'. + String result = readUtf8((newline - 1)); + skip(2); + return result; + + } else { + // Read everything until '\n', then skip the '\n'. + String result = readUtf8(newline); + skip(1); + return result; + } + } + + @Override public byte[] readByteArray() { + try { + return readByteArray(size); + } catch (EOFException e) { + throw new AssertionError(e); + } + } + + @Override public byte[] readByteArray(long byteCount) throws EOFException { + checkOffsetAndCount(this.size, 0, byteCount); + if (byteCount > Integer.MAX_VALUE) { + throw new IllegalArgumentException("byteCount > Integer.MAX_VALUE: " + byteCount); + } + + byte[] result = new byte[(int) byteCount]; + readFully(result); + return result; + } + + @Override public int read(byte[] sink) { + return read(sink, 0, sink.length); + } + + @Override public void readFully(byte[] sink) throws EOFException { + int offset = 0; + while (offset < sink.length) { + int read = read(sink, offset, sink.length - offset); + if (read == -1) throw new EOFException(); + offset += read; + } + } + + @Override public int read(byte[] sink, int offset, int byteCount) { + checkOffsetAndCount(sink.length, offset, byteCount); + + Segment s = this.head; + if (s == null) return -1; + int toCopy = Math.min(byteCount, s.limit - s.pos); + System.arraycopy(s.data, s.pos, sink, offset, toCopy); + + s.pos += toCopy; + this.size -= toCopy; + + if (s.pos == s.limit) { + this.head = s.pop(); + SegmentPool.INSTANCE.recycle(s); + } + + return toCopy; + } + + /** + * Discards all bytes in this buffer. Calling this method when you're done + * with a buffer will return its segments to the pool. + */ + public void clear() { + try { + skip(size); + } catch (EOFException e) { + throw new AssertionError(e); + } + } + + /** Discards {@code byteCount} bytes from the head of this buffer. */ + @Override public void skip(long byteCount) throws EOFException { + while (byteCount > 0) { + if (head == null) throw new EOFException(); + + int toSkip = (int) Math.min(byteCount, head.limit - head.pos); + size -= toSkip; + byteCount -= toSkip; + head.pos += toSkip; + + if (head.pos == head.limit) { + Segment toRecycle = head; + head = toRecycle.pop(); + SegmentPool.INSTANCE.recycle(toRecycle); + } + } + } + + @Override public Buffer write(ByteString byteString) { + if (byteString == null) throw new IllegalArgumentException("byteString == null"); + return write(byteString.data, 0, byteString.data.length); + } + + @Override public Buffer writeUtf8(String string) { + if (string == null) throw new IllegalArgumentException("string == null"); + return writeString(string, Util.UTF_8); + } + + @Override public Buffer writeString(String string, Charset charset) { + if (string == null) throw new IllegalArgumentException("string == null"); + if (charset == null) throw new IllegalArgumentException("charset == null"); + byte[] data = string.getBytes(charset); + return write(data, 0, data.length); + } + + @Override public Buffer write(byte[] source) { + if (source == null) throw new IllegalArgumentException("source == null"); + return write(source, 0, source.length); + } + + @Override public Buffer write(byte[] source, int offset, int byteCount) { + if (source == null) throw new IllegalArgumentException("source == null"); + checkOffsetAndCount(source.length, offset, byteCount); + + int limit = offset + byteCount; + while (offset < limit) { + Segment tail = writableSegment(1); + + int toCopy = Math.min(limit - offset, Segment.SIZE - tail.limit); + System.arraycopy(source, offset, tail.data, tail.limit, toCopy); + + offset += toCopy; + tail.limit += toCopy; + } + + this.size += byteCount; + return this; + } + + @Override public long writeAll(Source source) throws IOException { + if (source == null) throw new IllegalArgumentException("source == null"); + long totalBytesRead = 0; + for (long readCount; (readCount = source.read(this, Segment.SIZE)) != -1; ) { + totalBytesRead += readCount; + } + return totalBytesRead; + } + + @Override public Buffer writeByte(int b) { + Segment tail = writableSegment(1); + tail.data[tail.limit++] = (byte) b; + size += 1; + return this; + } + + @Override public Buffer writeShort(int s) { + Segment tail = writableSegment(2); + byte[] data = tail.data; + int limit = tail.limit; + data[limit++] = (byte) ((s >>> 8) & 0xff); + data[limit++] = (byte) (s & 0xff); + tail.limit = limit; + size += 2; + return this; + } + + @Override public Buffer writeShortLe(int s) { + return writeShort(Util.reverseBytesShort((short) s)); + } + + @Override public Buffer writeInt(int i) { + Segment tail = writableSegment(4); + byte[] data = tail.data; + int limit = tail.limit; + data[limit++] = (byte) ((i >>> 24) & 0xff); + data[limit++] = (byte) ((i >>> 16) & 0xff); + data[limit++] = (byte) ((i >>> 8) & 0xff); + data[limit++] = (byte) (i & 0xff); + tail.limit = limit; + size += 4; + return this; + } + + @Override public Buffer writeIntLe(int i) { + return writeInt(Util.reverseBytesInt(i)); + } + + @Override public Buffer writeLong(long v) { + Segment tail = writableSegment(8); + byte[] data = tail.data; + int limit = tail.limit; + data[limit++] = (byte) ((v >>> 56L) & 0xff); + data[limit++] = (byte) ((v >>> 48L) & 0xff); + data[limit++] = (byte) ((v >>> 40L) & 0xff); + data[limit++] = (byte) ((v >>> 32L) & 0xff); + data[limit++] = (byte) ((v >>> 24L) & 0xff); + data[limit++] = (byte) ((v >>> 16L) & 0xff); + data[limit++] = (byte) ((v >>> 8L) & 0xff); + data[limit++] = (byte) (v & 0xff); + tail.limit = limit; + size += 8; + return this; + } + + @Override public Buffer writeLongLe(long v) { + return writeLong(reverseBytesLong(v)); + } + + /** + * Returns a tail segment that we can write at least {@code minimumCapacity} + * bytes to, creating it if necessary. + */ + Segment writableSegment(int minimumCapacity) { + if (minimumCapacity < 1 || minimumCapacity > Segment.SIZE) throw new IllegalArgumentException(); + + if (head == null) { + head = SegmentPool.INSTANCE.take(); // Acquire a first segment. + return head.next = head.prev = head; + } + + Segment tail = head.prev; + if (tail.limit + minimumCapacity > Segment.SIZE) { + tail = tail.push(SegmentPool.INSTANCE.take()); // Append a new empty segment to fill up. + } + return tail; + } + + @Override public void write(Buffer source, long byteCount) { + // Move bytes from the head of the source buffer to the tail of this buffer + // while balancing two conflicting goals: don't waste CPU and don't waste + // memory. + // + // + // Don't waste CPU (ie. don't copy data around). + // + // Copying large amounts of data is expensive. Instead, we prefer to + // reassign entire segments from one buffer to the other. + // + // + // Don't waste memory. + // + // As an invariant, adjacent pairs of segments in a buffer should be at + // least 50% full, except for the head segment and the tail segment. + // + // The head segment cannot maintain the invariant because the application is + // consuming bytes from this segment, decreasing its level. + // + // The tail segment cannot maintain the invariant because the application is + // producing bytes, which may require new nearly-empty tail segments to be + // appended. + // + // + // Moving segments between buffers + // + // When writing one buffer to another, we prefer to reassign entire segments + // over copying bytes into their most compact form. Suppose we have a buffer + // with these segment levels [91%, 61%]. If we append a buffer with a + // single [72%] segment, that yields [91%, 61%, 72%]. No bytes are copied. + // + // Or suppose we have a buffer with these segment levels: [100%, 2%], and we + // want to append it to a buffer with these segment levels [99%, 3%]. This + // operation will yield the following segments: [100%, 2%, 99%, 3%]. That + // is, we do not spend time copying bytes around to achieve more efficient + // memory use like [100%, 100%, 4%]. + // + // When combining buffers, we will compact adjacent buffers when their + // combined level doesn't exceed 100%. For example, when we start with + // [100%, 40%] and append [30%, 80%], the result is [100%, 70%, 80%]. + // + // + // Splitting segments + // + // Occasionally we write only part of a source buffer to a sink buffer. For + // example, given a sink [51%, 91%], we may want to write the first 30% of + // a source [92%, 82%] to it. To simplify, we first transform the source to + // an equivalent buffer [30%, 62%, 82%] and then move the head segment, + // yielding sink [51%, 91%, 30%] and source [62%, 82%]. + + if (source == null) throw new IllegalArgumentException("source == null"); + if (source == this) throw new IllegalArgumentException("source == this"); + checkOffsetAndCount(source.size, 0, byteCount); + + while (byteCount > 0) { + // Is a prefix of the source's head segment all that we need to move? + if (byteCount < (source.head.limit - source.head.pos)) { + Segment tail = head != null ? head.prev : null; + if (tail == null || byteCount + (tail.limit - tail.pos) > Segment.SIZE) { + // We're going to need another segment. Split the source's head + // segment in two, then move the first of those two to this buffer. + source.head = source.head.split((int) byteCount); + } else { + // Our existing segments are sufficient. Move bytes from source's head to our tail. + source.head.writeTo(tail, (int) byteCount); + source.size -= byteCount; + this.size += byteCount; + return; + } + } + + // Remove the source's head segment and append it to our tail. + Segment segmentToMove = source.head; + long movedByteCount = segmentToMove.limit - segmentToMove.pos; + source.head = segmentToMove.pop(); + if (head == null) { + head = segmentToMove; + head.next = head.prev = head; + } else { + Segment tail = head.prev; + tail = tail.push(segmentToMove); + tail.compact(); + } + source.size -= movedByteCount; + this.size += movedByteCount; + byteCount -= movedByteCount; + } + } + + @Override public long read(Buffer sink, long byteCount) { + if (sink == null) throw new IllegalArgumentException("sink == null"); + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + if (this.size == 0) return -1L; + if (byteCount > this.size) byteCount = this.size; + sink.write(this, byteCount); + return byteCount; + } + + @Override public long indexOf(byte b) { + return indexOf(b, 0); + } + + /** + * Returns the index of {@code b} in this at or beyond {@code fromIndex}, or + * -1 if this buffer does not contain {@code b} in that range. + */ + @Override public long indexOf(byte b, long fromIndex) { + if (fromIndex < 0) throw new IllegalArgumentException("fromIndex < 0"); + + Segment s = head; + if (s == null) return -1L; + long offset = 0L; + do { + int segmentByteCount = s.limit - s.pos; + if (fromIndex >= segmentByteCount) { + fromIndex -= segmentByteCount; + } else { + byte[] data = s.data; + for (long pos = s.pos + fromIndex, limit = s.limit; pos < limit; pos++) { + if (data[(int) pos] == b) return offset + pos - s.pos; + } + fromIndex = 0; + } + offset += segmentByteCount; + s = s.next; + } while (s != head); + return -1L; + } + + @Override public long indexOfElement(ByteString targetBytes) { + return indexOfElement(targetBytes, 0); + } + + @Override public long indexOfElement(ByteString targetBytes, long fromIndex) { + if (fromIndex < 0) throw new IllegalArgumentException("fromIndex < 0"); + + Segment s = head; + if (s == null) return -1L; + long offset = 0L; + byte[] toFind = targetBytes.data; + do { + int segmentByteCount = s.limit - s.pos; + if (fromIndex >= segmentByteCount) { + fromIndex -= segmentByteCount; + } else { + byte[] data = s.data; + for (long pos = s.pos + fromIndex, limit = s.limit; pos < limit; pos++) { + byte b = data[(int) pos]; + for (byte targetByte : toFind) { + if (b == targetByte) return offset + pos - s.pos; + } + } + fromIndex = 0; + } + offset += segmentByteCount; + s = s.next; + } while (s != head); + return -1L; + } + + @Override public void flush() { + } + + @Override public void close() { + } + + @Override public Timeout timeout() { + return Timeout.NONE; + } + + /** For testing. This returns the sizes of the segments in this buffer. */ + List segmentSizes() { + if (head == null) return Collections.emptyList(); + List result = new ArrayList(); + result.add(head.limit - head.pos); + for (Segment s = head.next; s != head; s = s.next) { + result.add(s.limit - s.pos); + } + return result; + } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Buffer)) return false; + Buffer that = (Buffer) o; + if (size != that.size) return false; + if (size == 0) return true; // Both buffers are empty. + + Segment sa = this.head; + Segment sb = that.head; + int posA = sa.pos; + int posB = sb.pos; + + for (long pos = 0, count; pos < size; pos += count) { + count = Math.min(sa.limit - posA, sb.limit - posB); + + for (int i = 0; i < count; i++) { + if (sa.data[posA++] != sb.data[posB++]) return false; + } + + if (posA == sa.limit) { + sa = sa.next; + posA = sa.pos; + } + + if (posB == sb.limit) { + sb = sb.next; + posB = sb.pos; + } + } + + return true; + } + + @Override public int hashCode() { + Segment s = head; + if (s == null) return 0; + int result = 1; + do { + for (int pos = s.pos, limit = s.limit; pos < limit; pos++) { + result = 31 * result + s.data[pos]; + } + s = s.next; + } while (s != head); + return result; + } + + @Override public String toString() { + if (size == 0) { + return "Buffer[size=0]"; + } + + if (size <= 16) { + ByteString data = clone().readByteString(); + return String.format("Buffer[size=%s data=%s]", size, data.hex()); + } + + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + md5.update(head.data, head.pos, head.limit - head.pos); + for (Segment s = head.next; s != head; s = s.next) { + md5.update(s.data, s.pos, s.limit - s.pos); + } + return String.format("Buffer[size=%s md5=%s]", + size, ByteString.of(md5.digest()).hex()); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(); + } + } + + /** Returns a deep copy of this buffer. */ + @Override public Buffer clone() { + Buffer result = new Buffer(); + if (size == 0) return result; + + result.write(head.data, head.pos, head.limit - head.pos); + for (Segment s = head.next; s != head; s = s.next) { + result.write(s.data, s.pos, s.limit - s.pos); + } + + return result; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okio/BufferedSink.java b/contentstack/src/main/java/com/contentstack/okio/BufferedSink.java new file mode 100755 index 00000000..4acb953a --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/BufferedSink.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okio; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; + +/** + * A sink that keeps a buffer internally so that callers can do small writes + * without a performance penalty. + */ +public interface BufferedSink extends Sink { + /** Returns this sink's internal buffer. */ + Buffer buffer(); + + BufferedSink write(ByteString byteString) throws IOException; + + /** + * Like {@link OutputStream#write(byte[])}, this writes a complete byte array to + * this sink. + */ + BufferedSink write(byte[] source) throws IOException; + + /** + * Like {@link OutputStream#write(byte[], int, int)}, this writes {@code byteCount} + * bytes of {@code source}, starting at {@code offset}. + */ + BufferedSink write(byte[] source, int offset, int byteCount) throws IOException; + + /** + * Removes all bytes from {@code source} and appends them to this. Returns the + * number of bytes read which will be 0 if {@code source} is exhausted. + */ + long writeAll(Source source) throws IOException; + + /** Encodes {@code string} in UTF-8 and writes it to this sink. */ + BufferedSink writeUtf8(String string) throws IOException; + + /** Encodes {@code string} in {@code charset} and writes it to this sink. */ + BufferedSink writeString(String string, Charset charset) throws IOException; + + /** Writes a byte to this sink. */ + BufferedSink writeByte(int b) throws IOException; + + /** Writes a big-endian short to this sink using two bytes. */ + BufferedSink writeShort(int s) throws IOException; + + /** Writes a little-endian short to this sink using two bytes. */ + BufferedSink writeShortLe(int s) throws IOException; + + /** Writes a big-endian int to this sink using four bytes. */ + BufferedSink writeInt(int i) throws IOException; + + /** Writes a little-endian int to this sink using four bytes. */ + BufferedSink writeIntLe(int i) throws IOException; + + /** Writes a big-endian long to this sink using eight bytes. */ + BufferedSink writeLong(long v) throws IOException; + + /** Writes a little-endian long to this sink using eight bytes. */ + BufferedSink writeLongLe(long v) throws IOException; + + /** Writes complete segments to this sink. Like {@link #flush}, but weaker. */ + BufferedSink emitCompleteSegments() throws IOException; + + /** Returns an output stream that writes to this sink. */ + OutputStream outputStream(); +} diff --git a/contentstack/src/main/java/com/contentstack/okio/BufferedSource.java b/contentstack/src/main/java/com/contentstack/okio/BufferedSource.java new file mode 100755 index 00000000..0104e25d --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/BufferedSource.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okio; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; + +/** + * A source that keeps a buffer internally so that callers can do small reads + * without a performance penalty. It also allows clients to read ahead, + * buffering as much as necessary before consuming input. + */ +public interface BufferedSource extends Source { + /** Returns this source's internal buffer. */ + Buffer buffer(); + + /** + * Returns true if there are no more bytes in this source. This will block + * until there are bytes to read or the source is definitely exhausted. + */ + boolean exhausted() throws IOException; + + /** + * Returns when the buffer contains at least {@code byteCount} bytes. Throws + * an {@link java.io.EOFException} if the source is exhausted before the + * required bytes can be read. + */ + void require(long byteCount) throws IOException; + + /** + * Returns true when the buffer contains at least {@code byteCount} bytes, + * expanding it as necessary. Returns false if the source is exhausted before + * the requested bytes can be read. + */ + boolean request(long byteCount) throws IOException; + + /** Removes a byte from this source and returns it. */ + byte readByte() throws IOException; + + /** Removes two bytes from this source and returns a big-endian short. */ + short readShort() throws IOException; + + /** Removes two bytes from this source and returns a little-endian short. */ + short readShortLe() throws IOException; + + /** Removes four bytes from this source and returns a big-endian int. */ + int readInt() throws IOException; + + /** Removes four bytes from this source and returns a little-endian int. */ + int readIntLe() throws IOException; + + /** Removes eight bytes from this source and returns a big-endian long. */ + long readLong() throws IOException; + + /** Removes eight bytes from this source and returns a little-endian long. */ + long readLongLe() throws IOException; + + /** + * Reads and discards {@code byteCount} bytes from this source. Throws an + * {@link java.io.EOFException} if the source is exhausted before the + * requested bytes can be skipped. + */ + void skip(long byteCount) throws IOException; + + /** Removes all bytes bytes from this and returns them as a byte string. */ + ByteString readByteString() throws IOException; + + /** Removes {@code byteCount} bytes from this and returns them as a byte string. */ + ByteString readByteString(long byteCount) throws IOException; + + /** Removes all bytes from this and returns them as a byte array. */ + byte[] readByteArray() throws IOException; + + /** Removes {@code byteCount} bytes from this and returns them as a byte array. */ + byte[] readByteArray(long byteCount) throws IOException; + + /** + * Removes up to {@code sink.length} bytes from this and copies them into {@code sink}. + * Returns the number of bytes read, or -1 if this source is exhausted. + */ + int read(byte[] sink) throws IOException; + + /** + * Removes exactly {@code sink.length} bytes from this and copies them into {@code sink}. + * Throws an {@link java.io.EOFException} if the requested number of bytes cannot be read. + */ + void readFully(byte[] sink) throws IOException; + + /** + * Removes up to {@code byteCount} bytes from this and copies them into {@code sink} at + * {@code offset}. Returns the number of bytes read, or -1 if this source is exhausted. + */ + int read(byte[] sink, int offset, int byteCount) throws IOException; + + /** + * Removes exactly {@code byteCount} bytes from this and appends them to + * {@code sink}. Throws an {@link java.io.EOFException} if the requested + * number of bytes cannot be read. + */ + void readFully(Buffer sink, long byteCount) throws IOException; + + /** + * Removes all bytes from this and appends them to {@code sink}. Returns the + * total number of bytes written to {@code sink} which will be 0 if this is + * exhausted. + */ + long readAll(Sink sink) throws IOException; + + /** Removes all bytes from this, decodes them as UTF-8, and returns the string. */ + String readUtf8() throws IOException; + + /** + * Removes {@code byteCount} bytes from this, decodes them as UTF-8, and + * returns the string. + */ + String readUtf8(long byteCount) throws IOException; + + /** + * Removes and returns characters up to but not including the next line break. + * A line break is either {@code "\n"} or {@code "\r\n"}; these characters are + * not included in the result. + * + *

On the end of the stream this method returns null, just + * like {@link java.io.BufferedReader}. If the source doesn't end with a line + * break then an implicit line break is assumed. Null is returned once the + * source is exhausted. Use this for human-generated data, where a trailing + * line break is optional. + */ + String readUtf8Line() throws IOException; + + /** + * Removes and returns characters up to but not including the next line break. + * A line break is either {@code "\n"} or {@code "\r\n"}; these characters are + * not included in the result. + * + *

On the end of the stream this method throws. Every call + * must consume either '\r\n' or '\n'. If these characters are absent in the + * stream, an {@link java.io.EOFException} is thrown. Use this for + * machine-generated data where a missing line break implies truncated input. + */ + String readUtf8LineStrict() throws IOException; + + /** + * Removes all bytes from this, decodes them as {@code charset}, and returns + * the string. + */ + String readString(Charset charset) throws IOException; + + /** + * Removes {@code byteCount} bytes from this, decodes them as {@code charset}, + * and returns the string. + */ + String readString(long byteCount, Charset charset) throws IOException; + + /** + * Returns the index of the first {@code b} in the buffer. This expands the + * buffer as necessary until {@code b} is found. This reads an unbounded + * number of bytes into the buffer. Returns -1 if the stream is exhausted + * before the requested byte is found. + */ + long indexOf(byte b) throws IOException; + + /** + * Returns the index of the first {@code b} in the buffer at or after {@code + * fromIndex}. This expands the buffer as necessary until {@code b} is found. + * This reads an unbounded number of bytes into the buffer. Returns -1 if the + * stream is exhausted before the requested byte is found. + */ + long indexOf(byte b, long fromIndex) throws IOException; + + /** + * Returns the index of the first byte in {@code targetBytes} in the buffer. + * This expands the buffer as necessary until a target byte is found. This + * reads an unbounded number of bytes into the buffer. Returns -1 if the + * stream is exhausted before the requested byte is found. + */ + long indexOfElement(ByteString targetBytes) throws IOException; + + /** + * Returns the index of the first byte in {@code targetBytes} in the buffer + * at or after {@code fromIndex}. This expands the buffer as necessary until + * a target byte is found. This reads an unbounded number of bytes into the + * buffer. Returns -1 if the stream is exhausted before the requested byte is + * found. + */ + long indexOfElement(ByteString targetBytes, long fromIndex) throws IOException; + + /** Returns an input stream that reads from this source. */ + InputStream inputStream(); +} diff --git a/contentstack/src/main/java/com/contentstack/okio/ByteString.java b/contentstack/src/main/java/com/contentstack/okio/ByteString.java new file mode 100755 index 00000000..b799c4c3 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/ByteString.java @@ -0,0 +1,281 @@ +/* + * Copyright 2014 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okio; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +/** + * An immutable sequence of bytes. + * + *

Full disclosure: this class provides untrusted input and + * output streams with raw access to the underlying byte array. A hostile + * stream implementation could keep a reference to the mutable byte string, + * violating the immutable guarantee of this class. For this reason a byte + * string's immutability guarantee cannot be relied upon for security in applets + * and other environments that run both trusted and untrusted code in the same + * process. + */ +public final class ByteString implements Serializable { + private static final char[] HEX_DIGITS = + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + private static final long serialVersionUID = 1L; + + /** A singleton empty {@code ByteString}. */ + public static final ByteString EMPTY = ByteString.of(); + + final byte[] data; + private transient int hashCode; // Lazily computed; 0 if unknown. + private transient String utf8; // Lazily computed. + + ByteString(byte[] data) { + this.data = data; // Trusted internal constructor doesn't clone data. + } + + /** + * Returns a new byte string containing a clone of the bytes of {@code data}. + */ + public static ByteString of(byte... data) { + if (data == null) throw new IllegalArgumentException("data == null"); + return new ByteString(data.clone()); + } + + /** + * Returns a new byte string containing a copy of {@code byteCount} bytes of {@code data} starting + * at {@code offset}. + */ + public static ByteString of(byte[] data, int offset, int byteCount) { + if (data == null) throw new IllegalArgumentException("data == null"); + Util.checkOffsetAndCount(data.length, offset, byteCount); + + byte[] copy = new byte[byteCount]; + System.arraycopy(data, offset, copy, 0, byteCount); + return new ByteString(copy); + } + + /** Returns a new byte string containing the {@code UTF-8} bytes of {@code s}. */ + public static ByteString encodeUtf8(String s) { + if (s == null) throw new IllegalArgumentException("s == null"); + ByteString byteString = new ByteString(s.getBytes(Util.UTF_8)); + byteString.utf8 = s; + return byteString; + } + + /** Constructs a new {@code String} by decoding the bytes as {@code UTF-8}. */ + public String utf8() { + String result = utf8; + // We don't care if we double-allocate in racy code. + return result != null ? result : (utf8 = new String(data, Util.UTF_8)); + } + + /** + * Returns this byte string encoded as Base64. In violation of the + * RFC, the returned string does not wrap lines at 76 columns. + */ + public String base64() { + return Base64.encode(data); + } + + /** + * Decodes the Base64-encoded bytes and returns their value as a byte string. + * Returns null if {@code base64} is not a Base64-encoded sequence of bytes. + */ + public static ByteString decodeBase64(String base64) { + if (base64 == null) throw new IllegalArgumentException("base64 == null"); + byte[] decoded = Base64.decode(base64); + return decoded != null ? new ByteString(decoded) : null; + } + + /** Returns this byte string encoded in hexadecimal. */ + public String hex() { + char[] result = new char[data.length * 2]; + int c = 0; + for (byte b : data) { + result[c++] = HEX_DIGITS[(b >> 4) & 0xf]; + result[c++] = HEX_DIGITS[b & 0xf]; + } + return new String(result); + } + + /** Decodes the hex-encoded bytes and returns their value a byte string. */ + public static ByteString decodeHex(String hex) { + if (hex == null) throw new IllegalArgumentException("hex == null"); + if (hex.length() % 2 != 0) throw new IllegalArgumentException("Unexpected hex string: " + hex); + + byte[] result = new byte[hex.length() / 2]; + for (int i = 0; i < result.length; i++) { + int d1 = decodeHexDigit(hex.charAt(i * 2)) << 4; + int d2 = decodeHexDigit(hex.charAt(i * 2 + 1)); + result[i] = (byte) (d1 + d2); + } + return of(result); + } + + private static int decodeHexDigit(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + throw new IllegalArgumentException("Unexpected hex digit: " + c); + } + + /** + * Reads {@code count} bytes from {@code in} and returns the result. + * + * @throws java.io.EOFException if {@code in} has fewer than {@code count} + * bytes to read. + */ + public static ByteString read(InputStream in, int byteCount) throws IOException { + if (in == null) throw new IllegalArgumentException("in == null"); + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + + byte[] result = new byte[byteCount]; + for (int offset = 0, read; offset < byteCount; offset += read) { + read = in.read(result, offset, byteCount - offset); + if (read == -1) throw new EOFException(); + } + return new ByteString(result); + } + + /** + * Returns a byte string equal to this byte string, but with the bytes 'A' + * through 'Z' replaced with the corresponding byte in 'a' through 'z'. + * Returns this byte string if it contains no bytes in 'A' through 'Z'. + */ + public ByteString toAsciiLowercase() { + // Search for an uppercase character. If we don't find one, return this. + for (int i = 0; i < data.length; i++) { + byte c = data[i]; + if (c < 'A' || c > 'Z') continue; + + // If we reach this point, this string is not not lowercase. Create and + // return a new byte string. + byte[] lowercase = data.clone(); + lowercase[i++] = (byte) (c - ('A' - 'a')); + for (; i < lowercase.length; i++) { + c = lowercase[i]; + if (c < 'A' || c > 'Z') continue; + lowercase[i] = (byte) (c - ('A' - 'a')); + } + return new ByteString(lowercase); + } + return this; + } + + /** + * Returns a byte string equal to this byte string, but with the bytes 'a' + * through 'z' replaced with the corresponding byte in 'A' through 'Z'. + * Returns this byte string if it contains no bytes in 'a' through 'z'. + */ + public ByteString toAsciiUppercase() { + // Search for an lowercase character. If we don't find one, return this. + for (int i = 0; i < data.length; i++) { + byte c = data[i]; + if (c < 'a' || c > 'z') continue; + + // If we reach this point, this string is not not uppercase. Create and + // return a new byte string. + byte[] lowercase = data.clone(); + lowercase[i++] = (byte) (c - ('a' - 'A')); + for (; i < lowercase.length; i++) { + c = lowercase[i]; + if (c < 'a' || c > 'z') continue; + lowercase[i] = (byte) (c - ('a' - 'A')); + } + return new ByteString(lowercase); + } + return this; + } + + /** Returns the byte at {@code pos}. */ + public byte getByte(int pos) { + return data[pos]; + } + + /** + * Returns the number of bytes in this ByteString. + */ + public int size() { + return data.length; + } + + /** + * Returns a byte array containing a copy of the bytes in this {@code ByteString}. + */ + public byte[] toByteArray() { + return data.clone(); + } + + /** Writes the contents of this byte string to {@code out}. */ + public void write(OutputStream out) throws IOException { + if (out == null) throw new IllegalArgumentException("out == null"); + out.write(data); + } + + @Override public boolean equals(Object o) { + return o == this || o instanceof ByteString && Arrays.equals(((ByteString) o).data, data); + } + + @Override public int hashCode() { + int result = hashCode; + return result != 0 ? result : (hashCode = Arrays.hashCode(data)); + } + + @Override public String toString() { + if (data.length == 0) { + return "ByteString[size=0]"; + } + + if (data.length <= 16) { + return String.format("ByteString[size=%s data=%s]", data.length, hex()); + } + + try { + return String.format("ByteString[size=%s md5=%s]", data.length, + ByteString.of(MessageDigest.getInstance("MD5").digest(data)).hex()); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(); + } + } + + private void readObject(ObjectInputStream in) throws IOException { + int dataLength = in.readInt(); + ByteString byteString = ByteString.read(in, dataLength); + try { + Field field = ByteString.class.getDeclaredField("data"); + field.setAccessible(true); + field.set(this, byteString.data); + } catch (NoSuchFieldException e) { + throw new AssertionError(); + } catch (IllegalAccessException e) { + throw new AssertionError(); + } + } + + private void writeObject(ObjectOutputStream out) throws IOException { + out.writeInt(data.length); + out.write(data); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okio/DeflaterSink.java b/contentstack/src/main/java/com/contentstack/okio/DeflaterSink.java new file mode 100755 index 00000000..02f5135c --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/DeflaterSink.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okio; + +import android.annotation.SuppressLint; + +import java.io.IOException; +import java.util.zip.Deflater; + +/** + * A sink that uses DEFLATE to + * compress data written to another source. + * + *

Sync flush

+ * Aggressive flushing of this stream may result in reduced compression. Each + * call to {@link #flush} immediately compresses all currently-buffered data; + * this early compression may be less effective than compression performed + * without flushing. + * + *

This is equivalent to using {@link Deflater} with the sync flush option. + * This class does not offer any partial flush mechanism. For best performance, + * only call {@link #flush} when application behavior requires it. + */ +public final class DeflaterSink implements Sink { + private final BufferedSink sink; + private final Deflater deflater; + private boolean closed; + + public DeflaterSink(Sink sink, Deflater deflater) { + this(Okio.buffer(sink), deflater); + } + + /** + * This package-private constructor shares a buffer with its trusted caller. + * In general we can't share a BufferedSource because the deflater holds input + * bytes until they are inflated. + */ + DeflaterSink(BufferedSink sink, Deflater deflater) { + if (sink == null) throw new IllegalArgumentException("source == null"); + if (deflater == null) throw new IllegalArgumentException("inflater == null"); + this.sink = sink; + this.deflater = deflater; + } + + @Override public void write(Buffer source, long byteCount) + throws IOException { + Util.checkOffsetAndCount(source.size, 0, byteCount); + while (byteCount > 0) { + // Share bytes from the head segment of 'source' with the deflater. + Segment head = source.head; + int toDeflate = (int) Math.min(byteCount, head.limit - head.pos); + deflater.setInput(head.data, head.pos, toDeflate); + + // Deflate those bytes into sink. + deflate(false); + + // Mark those bytes as read. + source.size -= toDeflate; + head.pos += toDeflate; + if (head.pos == head.limit) { + source.head = head.pop(); + SegmentPool.INSTANCE.recycle(head); + } + + byteCount -= toDeflate; + } + } + + @SuppressLint("NewApi") +private void deflate(boolean syncFlush) throws IOException { + Buffer buffer = sink.buffer(); + while (true) { + Segment s = buffer.writableSegment(1); + + // The 4-parameter overload of deflate() doesn't exist in the RI until + // Java 1.7, and is public (although with @hide) on Android since 2.3. + // The @hide tag means that this code won't compile against the Android + // 2.3 SDK, but it will run fine there. + int deflated = syncFlush + ? deflater.deflate(s.data, s.limit, Segment.SIZE - s.limit/*, Deflater.SYNC_FLUSH*/) + : deflater.deflate(s.data, s.limit, Segment.SIZE - s.limit); + + if (deflated > 0) { + s.limit += deflated; + buffer.size += deflated; + sink.emitCompleteSegments(); + } else if (deflater.needsInput()) { + return; + } + } + } + + @Override public void flush() throws IOException { + deflate(true); + sink.flush(); + } + + void finishDeflate() throws IOException { + deflater.finish(); + deflate(false); + } + + @Override public void close() throws IOException { + if (closed) return; + + // Emit deflated data to the underlying sink. If this fails, we still need + // to close the deflater and the sink; otherwise we risk leaking resources. + Throwable thrown = null; + try { + finishDeflate(); + } catch (Throwable e) { + thrown = e; + } + + try { + deflater.end(); + } catch (Throwable e) { + if (thrown == null) thrown = e; + } + + try { + sink.close(); + } catch (Throwable e) { + if (thrown == null) thrown = e; + } + closed = true; + + if (thrown != null) Util.sneakyRethrow(thrown); + } + + @Override public Timeout timeout() { + return sink.timeout(); + } + + @Override public String toString() { + return "DeflaterSink(" + sink + ")"; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okio/ForwardingSink.java b/contentstack/src/main/java/com/contentstack/okio/ForwardingSink.java new file mode 100755 index 00000000..595c0d1d --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/ForwardingSink.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okio; + +import java.io.IOException; + +/** A {@link Sink} which forwards calls to another. Useful for subclassing. */ +public abstract class ForwardingSink implements Sink { + private final Sink delegate; + + public ForwardingSink(Sink delegate) { + if (delegate == null) throw new IllegalArgumentException("delegate == null"); + this.delegate = delegate; + } + + /** {@link Sink} to which this instance is delegating. */ + public final Sink delegate() { + return delegate; + } + + @Override public void write(Buffer source, long byteCount) throws IOException { + delegate.write(source, byteCount); + } + + @Override public void flush() throws IOException { + delegate.flush(); + } + + @Override public Timeout timeout() { + return delegate.timeout(); + } + + @Override public void close() throws IOException { + delegate.close(); + } + + @Override public String toString() { + return getClass().getSimpleName() + "(" + delegate.toString() + ")"; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okio/ForwardingSource.java b/contentstack/src/main/java/com/contentstack/okio/ForwardingSource.java new file mode 100755 index 00000000..f59fe8e3 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/ForwardingSource.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okio; + +import java.io.IOException; + +/** A {@link Source} which forwards calls to another. Useful for subclassing. */ +public abstract class ForwardingSource implements Source { + private final Source delegate; + + public ForwardingSource(Source delegate) { + if (delegate == null) throw new IllegalArgumentException("delegate == null"); + this.delegate = delegate; + } + + /** {@link Source} to which this instance is delegating. */ + public final Source delegate() { + return delegate; + } + + @Override public long read(Buffer sink, long byteCount) throws IOException { + return delegate.read(sink, byteCount); + } + + @Override public Timeout timeout() { + return delegate.timeout(); + } + + @Override public void close() throws IOException { + delegate.close(); + } + + @Override public String toString() { + return getClass().getSimpleName() + "(" + delegate.toString() + ")"; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okio/GzipSink.java b/contentstack/src/main/java/com/contentstack/okio/GzipSink.java new file mode 100755 index 00000000..992b1f4c --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/GzipSink.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okio; + +import java.io.IOException; +import java.util.zip.CRC32; +import java.util.zip.Deflater; + +import static java.util.zip.Deflater.DEFAULT_COMPRESSION; + +/** + * A sink that uses GZIP to + * compress written data to another sink. + * + *

Sync flush

+ * Aggressive flushing of this stream may result in reduced compression. Each + * call to {@link #flush} immediately compresses all currently-buffered data; + * this early compression may be less effective than compression performed + * without flushing. + * + *

This is equivalent to using {@link Deflater} with the sync flush option. + * This class does not offer any partial flush mechanism. For best performance, + * only call {@link #flush} when application behavior requires it. + */ +public final class GzipSink implements Sink { + /** Sink into which the GZIP format is written. */ + private final BufferedSink sink; + + /** The deflater used to compress the body. */ + private final Deflater deflater; + + /** + * The deflater sink takes care of moving data between decompressed source and + * compressed sink buffers. + */ + private final DeflaterSink deflaterSink; + + private boolean closed; + + /** Checksum calculated for the compressed body. */ + private final CRC32 crc = new CRC32(); + + public GzipSink(Sink sink) { + if (sink == null) throw new IllegalArgumentException("sink == null"); + this.deflater = new Deflater(DEFAULT_COMPRESSION, true /* No wrap */); + this.sink = Okio.buffer(sink); + this.deflaterSink = new DeflaterSink(this.sink, deflater); + + writeHeader(); + } + + @Override public void write(Buffer source, long byteCount) throws IOException { + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + if (byteCount == 0) return; + + updateCrc(source, byteCount); + deflaterSink.write(source, byteCount); + } + + @Override public void flush() throws IOException { + deflaterSink.flush(); + } + + @Override public Timeout timeout() { + return sink.timeout(); + } + + @Override public void close() throws IOException { + if (closed) return; + + // This method delegates to the DeflaterSink for finishing the deflate process + // but keeps responsibility for releasing the deflater's resources. This is + // necessary because writeFooter needs to query the proccessed byte count which + // only works when the defalter is still open. + + Throwable thrown = null; + try { + deflaterSink.finishDeflate(); + writeFooter(); + } catch (Throwable e) { + thrown = e; + } + + try { + deflater.end(); + } catch (Throwable e) { + if (thrown == null) thrown = e; + } + + try { + sink.close(); + } catch (Throwable e) { + if (thrown == null) thrown = e; + } + closed = true; + + if (thrown != null) Util.sneakyRethrow(thrown); + } + + private void writeHeader() { + // Write the Gzip header directly into the buffer for the sink to avoid handling IOException. + Buffer buffer = this.sink.buffer(); + buffer.writeShort(0x1f8b); // Two-byte Gzip ID. + buffer.writeByte(0x08); // 8 == Deflate compression method. + buffer.writeByte(0x00); // No flags. + buffer.writeInt(0x00); // No modification time. + buffer.writeByte(0x00); // No extra flags. + buffer.writeByte(0x00); // No OS. + } + + private void writeFooter() throws IOException { + sink.writeIntLe((int) crc.getValue()); // CRC of original data. + sink.writeIntLe(deflater.getTotalIn()); // Length of original data. + } + + /** Updates the CRC with the given bytes. */ + private void updateCrc(Buffer buffer, long byteCount) { + for (Segment head = buffer.head; byteCount > 0; head = head.next) { + int segmentLength = (int) Math.min(byteCount, head.limit - head.pos); + crc.update(head.data, head.pos, segmentLength); + byteCount -= segmentLength; + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okio/GzipSource.java b/contentstack/src/main/java/com/contentstack/okio/GzipSource.java new file mode 100755 index 00000000..0822fec9 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/GzipSource.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okio; + +import java.io.EOFException; +import java.io.IOException; +import java.util.zip.CRC32; +import java.util.zip.Inflater; + +/** + * A source that uses GZIP to + * decompress data read from another source. + */ +public final class GzipSource implements Source { + private static final byte FHCRC = 1; + private static final byte FEXTRA = 2; + private static final byte FNAME = 3; + private static final byte FCOMMENT = 4; + + private static final byte SECTION_HEADER = 0; + private static final byte SECTION_BODY = 1; + private static final byte SECTION_TRAILER = 2; + private static final byte SECTION_DONE = 3; + + /** The current section. Always progresses forward. */ + private int section = SECTION_HEADER; + + /** + * Our source should yield a GZIP header (which we consume directly), followed + * by deflated bytes (which we consume via an InflaterSource), followed by a + * GZIP trailer (which we also consume directly). + */ + private final BufferedSource source; + + /** The inflater used to decompress the deflated body. */ + private final Inflater inflater; + + /** + * The inflater source takes care of moving data between compressed source and + * decompressed sink buffers. + */ + private final InflaterSource inflaterSource; + + /** Checksum used to check both the GZIP header and decompressed body. */ + private final CRC32 crc = new CRC32(); + + public GzipSource(Source source) { + if (source == null) throw new IllegalArgumentException("source == null"); + this.inflater = new Inflater(true); + this.source = Okio.buffer(source); + this.inflaterSource = new InflaterSource(this.source, inflater); + } + + @Override public long read(Buffer sink, long byteCount) throws IOException { + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + if (byteCount == 0) return 0; + + // If we haven't consumed the header, we must consume it before anything else. + if (section == SECTION_HEADER) { + consumeHeader(); + section = SECTION_BODY; + } + + // Attempt to read at least a byte of the body. If we do, we're done. + if (section == SECTION_BODY) { + long offset = sink.size; + long result = inflaterSource.read(sink, byteCount); + if (result != -1) { + updateCrc(sink, offset, result); + return result; + } + section = SECTION_TRAILER; + } + + // The body is exhausted; time to read the trailer. We always consume the + // trailer before returning a -1 exhausted result; that way if you read to + // the end of a GzipSource you guarantee that the CRC has been checked. + if (section == SECTION_TRAILER) { + consumeTrailer(); + section = SECTION_DONE; + + // Gzip streams self-terminate: they return -1 before their underlying + // source returns -1. Here we attempt to force the underlying stream to + // return -1 which may trigger it to release its resources. If it doesn't + // return -1, then our Gzip data finished prematurely! + if (!source.exhausted()) { + throw new IOException("gzip finished without exhausting source"); + } + } + + return -1; + } + + private void consumeHeader() throws IOException { + // Read the 10-byte header. We peek at the flags byte first so we know if we + // need to CRC the entire header. Then we read the magic ID1ID2 sequence. + // We can skip everything else in the first 10 bytes. + // +---+---+---+---+---+---+---+---+---+---+ + // |ID1|ID2|CM |FLG| MTIME |XFL|OS | (more-->) + // +---+---+---+---+---+---+---+---+---+---+ + source.require(10); + byte flags = source.buffer().getByte(3); + boolean fhcrc = ((flags >> FHCRC) & 1) == 1; + if (fhcrc) updateCrc(source.buffer(), 0, 10); + + short id1id2 = source.readShort(); + checkEqual("ID1ID2", (short) 0x1f8b, id1id2); + source.skip(8); + + // Skip optional extra fields. + // +---+---+=================================+ + // | XLEN |...XLEN bytes of "extra field"...| (more-->) + // +---+---+=================================+ + if (((flags >> FEXTRA) & 1) == 1) { + source.require(2); + if (fhcrc) updateCrc(source.buffer(), 0, 2); + int xlen = source.buffer().readShortLe(); + source.require(xlen); + if (fhcrc) updateCrc(source.buffer(), 0, xlen); + source.skip(xlen); + } + + // Skip an optional 0-terminated name. + // +=========================================+ + // |...original file name, zero-terminated...| (more-->) + // +=========================================+ + if (((flags >> FNAME) & 1) == 1) { + long index = source.indexOf((byte) 0); + if (index == -1) throw new EOFException(); + if (fhcrc) updateCrc(source.buffer(), 0, index + 1); + source.skip(index + 1); + } + + // Skip an optional 0-terminated comment. + // +===================================+ + // |...file comment, zero-terminated...| (more-->) + // +===================================+ + if (((flags >> FCOMMENT) & 1) == 1) { + long index = source.indexOf((byte) 0); + if (index == -1) throw new EOFException(); + if (fhcrc) updateCrc(source.buffer(), 0, index + 1); + source.skip(index + 1); + } + + // Confirm the optional header CRC. + // +---+---+ + // | CRC16 | + // +---+---+ + if (fhcrc) { + checkEqual("FHCRC", source.readShortLe(), (short) crc.getValue()); + crc.reset(); + } + } + + private void consumeTrailer() throws IOException { + // Read the eight-byte trailer. Confirm the body's CRC and size. + // +---+---+---+---+---+---+---+---+ + // | CRC32 | ISIZE | + // +---+---+---+---+---+---+---+---+ + checkEqual("CRC", source.readIntLe(), (int) crc.getValue()); + checkEqual("ISIZE", source.readIntLe(), inflater.getTotalOut()); + } + + @Override public Timeout timeout() { + return source.timeout(); + } + + @Override public void close() throws IOException { + inflaterSource.close(); + } + + /** Updates the CRC with the given bytes. */ + private void updateCrc(Buffer buffer, long offset, long byteCount) { + // Skip segments that we aren't checksumming. + Segment s = buffer.head; + for (; offset >= (s.limit - s.pos); s = s.next) { + offset -= (s.limit - s.pos); + } + + // Checksum one segment at a time. + for (; byteCount > 0; s = s.next) { + int pos = (int) (s.pos + offset); + int toUpdate = (int) Math.min(s.limit - pos, byteCount); + crc.update(s.data, pos, toUpdate); + byteCount -= toUpdate; + offset = 0; + } + } + + private void checkEqual(String name, int expected, int actual) throws IOException { + if (actual != expected) { + throw new IOException(String.format( + "%s: actual 0x%08x != expected 0x%08x", name, actual, expected)); + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okio/InflaterSource.java b/contentstack/src/main/java/com/contentstack/okio/InflaterSource.java new file mode 100755 index 00000000..6466e0c7 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/InflaterSource.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okio; + +import java.io.EOFException; +import java.io.IOException; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +/** + * A source that uses DEFLATE + * to decompress data read from another source. + */ +public final class InflaterSource implements Source { + private final BufferedSource source; + private final Inflater inflater; + + /** + * When we call Inflater.setInput(), the inflater keeps our byte array until + * it needs input again. This tracks how many bytes the inflater is currently + * holding on to. + */ + private int bufferBytesHeldByInflater; + private boolean closed; + + public InflaterSource(Source source, Inflater inflater) { + this(Okio.buffer(source), inflater); + } + + /** + * This package-private constructor shares a buffer with its trusted caller. + * In general we can't share a BufferedSource because the inflater holds input + * bytes until they are inflated. + */ + InflaterSource(BufferedSource source, Inflater inflater) { + if (source == null) throw new IllegalArgumentException("source == null"); + if (inflater == null) throw new IllegalArgumentException("inflater == null"); + this.source = source; + this.inflater = inflater; + } + + @Override public long read( + Buffer sink, long byteCount) throws IOException { + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + if (closed) throw new IllegalStateException("closed"); + if (byteCount == 0) return 0; + + while (true) { + boolean sourceExhausted = refill(); + + // Decompress the inflater's compressed data into the sink. + try { + Segment tail = sink.writableSegment(1); + int bytesInflated = inflater.inflate(tail.data, tail.limit, Segment.SIZE - tail.limit); + if (bytesInflated > 0) { + tail.limit += bytesInflated; + sink.size += bytesInflated; + return bytesInflated; + } + if (inflater.finished() || inflater.needsDictionary()) { + releaseInflatedBytes(); + return -1; + } + if (sourceExhausted) throw new EOFException("source exhausted prematurely"); + } catch (DataFormatException e) { + throw new IOException(e); + } + } + } + + /** + * Refills the inflater with compressed data if it needs input. (And only if + * it needs input). Returns true if the inflater required input but the source + * was exhausted. + */ + public boolean refill() throws IOException { + if (!inflater.needsInput()) return false; + + releaseInflatedBytes(); + if (inflater.getRemaining() != 0) throw new IllegalStateException("?"); + + // If there are compressed bytes in the source, assign them to the inflater. + if (source.exhausted()) return true; + + // Assign buffer bytes to the inflater. + Segment head = source.buffer().head; + bufferBytesHeldByInflater = head.limit - head.pos; + inflater.setInput(head.data, head.pos, bufferBytesHeldByInflater); + return false; + } + + /** When the inflater has processed compressed data, remove it from the buffer. */ + private void releaseInflatedBytes() throws IOException { + if (bufferBytesHeldByInflater == 0) return; + int toRelease = bufferBytesHeldByInflater - inflater.getRemaining(); + bufferBytesHeldByInflater -= toRelease; + source.skip(toRelease); + } + + @Override public Timeout timeout() { + return source.timeout(); + } + + @Override public void close() throws IOException { + if (closed) return; + inflater.end(); + closed = true; + source.close(); + } +} diff --git a/contentstack/src/main/java/com/contentstack/okio/Okio.java b/contentstack/src/main/java/com/contentstack/okio/Okio.java new file mode 100755 index 00000000..fcc5f6d8 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/Okio.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okio; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.contentstack.okio.Util.checkOffsetAndCount; + +/** Essential APIs for working with Okio. */ +public final class Okio { + private static final Logger logger = Logger.getLogger(Okio.class.getName()); + + private Okio() { + } + + /** + * Returns a new source that buffers reads from {@code source}. The returned + * source will perform bulk reads into its in-memory buffer. Use this wherever + * you read a source to get an ergonomic and efficient access to data. + */ + public static BufferedSource buffer(Source source) { + if (source == null) throw new IllegalArgumentException("source == null"); + return new RealBufferedSource(source); + } + + /** + * Returns a new sink that buffers writes to {@code sink}. The returned sink + * will batch writes to {@code sink}. Use this wherever you write to a sink to + * get an ergonomic and efficient access to data. + */ + public static BufferedSink buffer(Sink sink) { + if (sink == null) throw new IllegalArgumentException("sink == null"); + return new RealBufferedSink(sink); + } + + /** Returns a sink that writes to {@code out}. */ + public static Sink sink(final OutputStream out) { + return sink(out, new Timeout()); + } + + private static Sink sink(final OutputStream out, final Timeout timeout) { + if (out == null) throw new IllegalArgumentException("out == null"); + if (timeout == null) throw new IllegalArgumentException("timeout == null"); + + return new Sink() { + @Override public void write(Buffer source, long byteCount) throws IOException { + checkOffsetAndCount(source.size, 0, byteCount); + while (byteCount > 0) { + timeout.throwIfReached(); + Segment head = source.head; + int toCopy = (int) Math.min(byteCount, head.limit - head.pos); + out.write(head.data, head.pos, toCopy); + + head.pos += toCopy; + byteCount -= toCopy; + source.size -= toCopy; + + if (head.pos == head.limit) { + source.head = head.pop(); + SegmentPool.INSTANCE.recycle(head); + } + } + } + + @Override public void flush() throws IOException { + out.flush(); + } + + @Override public void close() throws IOException { + out.close(); + } + + @Override public Timeout timeout() { + return timeout; + } + + @Override public String toString() { + return "sink(" + out + ")"; + } + }; + } + + /** + * Returns a sink that writes to {@code socket}. Prefer this over {@link + * #sink(OutputStream)} because this method honors timeouts. When the socket + * write times out, the socket is asynchronously closed by a watchdog thread. + */ + public static Sink sink(final Socket socket) throws IOException { + if (socket == null) throw new IllegalArgumentException("socket == null"); + AsyncTimeout timeout = timeout(socket); + Sink sink = sink(socket.getOutputStream(), timeout); + return timeout.sink(sink); + } + + /** Returns a source that reads from {@code in}. */ + public static Source source(final InputStream in) { + return source(in, new Timeout()); + } + + private static Source source(final InputStream in, final Timeout timeout) { + if (in == null) throw new IllegalArgumentException("in == null"); + if (timeout == null) throw new IllegalArgumentException("timeout == null"); + + return new Source() { + @Override public long read(Buffer sink, long byteCount) throws IOException { + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + timeout.throwIfReached(); + Segment tail = sink.writableSegment(1); + int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit); + int bytesRead = in.read(tail.data, tail.limit, maxToCopy); + if (bytesRead == -1) return -1; + tail.limit += bytesRead; + sink.size += bytesRead; + return bytesRead; + } + + @Override public void close() throws IOException { + in.close(); + } + + @Override public Timeout timeout() { + return timeout; + } + + @Override public String toString() { + return "source(" + in + ")"; + } + }; + } + + /** Returns a source that reads from {@code file}. */ + public static Source source(File file) throws FileNotFoundException { + if (file == null) throw new IllegalArgumentException("file == null"); + return source(new FileInputStream(file)); + } + + /** Returns a source that reads from {@code path}. */ + // Should only be invoked on Java 7+. + /*public static Source source(android.graphics.Path path, OpenOption... options) throws IOException { + if (path == null) throw new IllegalArgumentException("path == null"); + return source(android.provider.MediaStore.Files.newInputStream(path, options)); + }*/ + + /** Returns a sink that writes to {@code file}. */ + public static Sink sink(File file) throws FileNotFoundException { + if (file == null) throw new IllegalArgumentException("file == null"); + return sink(new FileOutputStream(file)); + } + + /** Returns a sink that appends to {@code file}. */ + public static Sink appendingSink(File file) throws FileNotFoundException { + if (file == null) throw new IllegalArgumentException("file == null"); + return sink(new FileOutputStream(file, true)); + } + + /** Returns a sink that writes to {@code path}. */// Should only be invoked on Java 7+. + /* public static Sink sink(android.graphics.Path path, OpenOption... options) throws IOException { + if (path == null) throw new IllegalArgumentException("path == null"); + return sink(android.provider.MediaStore.Files.newOutputStream(path, options)); + }*/ + + /** + * Returns a source that reads from {@code socket}. Prefer this over {@link + * #source(InputStream)} because this method honors timeouts. When the socket + * read times out, the socket is asynchronously closed by a watchdog thread. + */ + public static Source source(final Socket socket) throws IOException { + if (socket == null) throw new IllegalArgumentException("socket == null"); + AsyncTimeout timeout = timeout(socket); + Source source = source(socket.getInputStream(), timeout); + return timeout.source(source); + } + + private static AsyncTimeout timeout(final Socket socket) { + return new AsyncTimeout() { + @Override protected void timedOut() { + try { + socket.close(); + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to close timed out socket " + socket, e); + } + } + }; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okio/RealBufferedSink.java b/contentstack/src/main/java/com/contentstack/okio/RealBufferedSink.java new file mode 100755 index 00000000..1d39c561 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/RealBufferedSink.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okio; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; + +final class RealBufferedSink implements BufferedSink { + public final Buffer buffer; + public final Sink sink; + private boolean closed; + + public RealBufferedSink(Sink sink, Buffer buffer) { + if (sink == null) throw new IllegalArgumentException("sink == null"); + this.buffer = buffer; + this.sink = sink; + } + + public RealBufferedSink(Sink sink) { + this(sink, new Buffer()); + } + + @Override public Buffer buffer() { + return buffer; + } + + @Override public void write(Buffer source, long byteCount) + throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.write(source, byteCount); + emitCompleteSegments(); + } + + @Override public BufferedSink write(ByteString byteString) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.write(byteString); + return emitCompleteSegments(); + } + + @Override public BufferedSink writeUtf8(String string) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.writeUtf8(string); + return emitCompleteSegments(); + } + + @Override public BufferedSink writeString(String string, Charset charset) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.writeString(string, charset); + return emitCompleteSegments(); + } + + @Override public BufferedSink write(byte[] source) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.write(source); + return emitCompleteSegments(); + } + + @Override public BufferedSink write(byte[] source, int offset, int byteCount) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.write(source, offset, byteCount); + return emitCompleteSegments(); + } + + @Override public long writeAll(Source source) throws IOException { + if (source == null) throw new IllegalArgumentException("source == null"); + long totalBytesRead = 0; + for (long readCount; (readCount = source.read(buffer, Segment.SIZE)) != -1; ) { + totalBytesRead += readCount; + emitCompleteSegments(); + } + return totalBytesRead; + } + + @Override public BufferedSink writeByte(int b) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.writeByte(b); + return emitCompleteSegments(); + } + + @Override public BufferedSink writeShort(int s) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.writeShort(s); + return emitCompleteSegments(); + } + + @Override public BufferedSink writeShortLe(int s) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.writeShortLe(s); + return emitCompleteSegments(); + } + + @Override public BufferedSink writeInt(int i) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.writeInt(i); + return emitCompleteSegments(); + } + + @Override public BufferedSink writeIntLe(int i) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.writeIntLe(i); + return emitCompleteSegments(); + } + + @Override public BufferedSink writeLong(long v) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.writeLong(v); + return emitCompleteSegments(); + } + + @Override public BufferedSink writeLongLe(long v) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.writeLongLe(v); + return emitCompleteSegments(); + } + + @Override public BufferedSink emitCompleteSegments() throws IOException { + if (closed) throw new IllegalStateException("closed"); + long byteCount = buffer.completeSegmentByteCount(); + if (byteCount > 0) sink.write(buffer, byteCount); + return this; + } + + @Override public OutputStream outputStream() { + return new OutputStream() { + @Override public void write(int b) throws IOException { + if (closed) throw new IOException("closed"); + buffer.writeByte((byte) b); + emitCompleteSegments(); + } + + @Override public void write(byte[] data, int offset, int byteCount) throws IOException { + if (closed) throw new IOException("closed"); + buffer.write(data, offset, byteCount); + emitCompleteSegments(); + } + + @Override public void flush() throws IOException { + // For backwards compatibility, a flush() on a closed stream is a no-op. + if (!closed) { + RealBufferedSink.this.flush(); + } + } + + @Override public void close() throws IOException { + RealBufferedSink.this.close(); + } + + @Override public String toString() { + return RealBufferedSink.this + ".outputStream()"; + } + }; + } + + @Override public void flush() throws IOException { + if (closed) throw new IllegalStateException("closed"); + if (buffer.size > 0) { + sink.write(buffer, buffer.size); + } + sink.flush(); + } + + @Override public void close() throws IOException { + if (closed) return; + + // Emit buffered data to the underlying sink. If this fails, we still need + // to close the sink; otherwise we risk leaking resources. + Throwable thrown = null; + try { + if (buffer.size > 0) { + sink.write(buffer, buffer.size); + } + } catch (Throwable e) { + thrown = e; + } + + try { + sink.close(); + } catch (Throwable e) { + if (thrown == null) thrown = e; + } + closed = true; + + if (thrown != null) Util.sneakyRethrow(thrown); + } + + @Override public Timeout timeout() { + return sink.timeout(); + } + + @Override public String toString() { + return "buffer(" + sink + ")"; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okio/RealBufferedSource.java b/contentstack/src/main/java/com/contentstack/okio/RealBufferedSource.java new file mode 100755 index 00000000..879a632f --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/RealBufferedSource.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okio; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; + +final class RealBufferedSource implements BufferedSource { + public final Buffer buffer; + public final Source source; + private boolean closed; + + public RealBufferedSource(Source source, Buffer buffer) { + if (source == null) throw new IllegalArgumentException("source == null"); + this.buffer = buffer; + this.source = source; + } + + public RealBufferedSource(Source source) { + this(source, new Buffer()); + } + + @Override public Buffer buffer() { + return buffer; + } + + @Override public long read(Buffer sink, long byteCount) throws IOException { + if (sink == null) throw new IllegalArgumentException("sink == null"); + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + if (closed) throw new IllegalStateException("closed"); + + if (buffer.size == 0) { + long read = source.read(buffer, Segment.SIZE); + if (read == -1) return -1; + } + + long toRead = Math.min(byteCount, buffer.size); + return buffer.read(sink, toRead); + } + + @Override public boolean exhausted() throws IOException { + if (closed) throw new IllegalStateException("closed"); + return buffer.exhausted() && source.read(buffer, Segment.SIZE) == -1; + } + + @Override public void require(long byteCount) throws IOException { + if (!request(byteCount)) throw new EOFException(); + } + + @Override public boolean request(long byteCount) throws IOException { + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + if (closed) throw new IllegalStateException("closed"); + while (buffer.size < byteCount) { + if (source.read(buffer, Segment.SIZE) == -1) return false; + } + return true; + } + + @Override public byte readByte() throws IOException { + require(1); + return buffer.readByte(); + } + + @Override public ByteString readByteString() throws IOException { + buffer.writeAll(source); + return buffer.readByteString(); + } + + @Override public ByteString readByteString(long byteCount) throws IOException { + require(byteCount); + return buffer.readByteString(byteCount); + } + + @Override public byte[] readByteArray() throws IOException { + buffer.writeAll(source); + return buffer.readByteArray(); + } + + @Override public byte[] readByteArray(long byteCount) throws IOException { + require(byteCount); + return buffer.readByteArray(byteCount); + } + + @Override public int read(byte[] sink) throws IOException { + return read(sink, 0, sink.length); + } + + @Override public void readFully(byte[] sink) throws IOException { + try { + require(sink.length); + } catch (EOFException e) { + // The underlying source is exhausted. Copy the bytes we got before rethrowing. + int offset = 0; + while (buffer.size > 0) { + int read = buffer.read(sink, offset, (int) buffer.size - offset); + if (read == -1) throw new AssertionError(); + offset += read; + } + throw e; + } + buffer.readFully(sink); + } + + @Override public int read(byte[] sink, int offset, int byteCount) throws IOException { + Util.checkOffsetAndCount(sink.length, offset, byteCount); + + if (buffer.size == 0) { + long read = source.read(buffer, Segment.SIZE); + if (read == -1) return -1; + } + + int toRead = (int) Math.min(byteCount, buffer.size); + return buffer.read(sink, offset, toRead); + } + + @Override public void readFully(Buffer sink, long byteCount) throws IOException { + try { + require(byteCount); + } catch (EOFException e) { + // The underlying source is exhausted. Copy the bytes we got before rethrowing. + sink.writeAll(buffer); + throw e; + } + buffer.readFully(sink, byteCount); + } + + @Override public long readAll(Sink sink) throws IOException { + if (sink == null) throw new IllegalArgumentException("sink == null"); + + long totalBytesWritten = 0; + while (source.read(buffer, Segment.SIZE) != -1) { + long emitByteCount = buffer.completeSegmentByteCount(); + if (emitByteCount > 0) { + totalBytesWritten += emitByteCount; + sink.write(buffer, emitByteCount); + } + } + if (buffer.size() > 0) { + totalBytesWritten += buffer.size(); + sink.write(buffer, buffer.size()); + } + return totalBytesWritten; + } + + @Override public String readUtf8() throws IOException { + buffer.writeAll(source); + return buffer.readUtf8(); + } + + @Override public String readUtf8(long byteCount) throws IOException { + require(byteCount); + return buffer.readUtf8(byteCount); + } + + @Override public String readString(Charset charset) throws IOException { + if (charset == null) throw new IllegalArgumentException("charset == null"); + + buffer.writeAll(source); + return buffer.readString(charset); + } + + @Override public String readString(long byteCount, Charset charset) throws IOException { + require(byteCount); + if (charset == null) throw new IllegalArgumentException("charset == null"); + return buffer.readString(byteCount, charset); + } + + @Override public String readUtf8Line() throws IOException { + long newline = indexOf((byte) '\n'); + + if (newline == -1) { + return buffer.size != 0 ? readUtf8(buffer.size) : null; + } + + return buffer.readUtf8Line(newline); + } + + @Override public String readUtf8LineStrict() throws IOException { + long newline = indexOf((byte) '\n'); + if (newline == -1L) throw new EOFException(); + return buffer.readUtf8Line(newline); + } + + @Override public short readShort() throws IOException { + require(2); + return buffer.readShort(); + } + + @Override public short readShortLe() throws IOException { + require(2); + return buffer.readShortLe(); + } + + @Override public int readInt() throws IOException { + require(4); + return buffer.readInt(); + } + + @Override public int readIntLe() throws IOException { + require(4); + return buffer.readIntLe(); + } + + @Override public long readLong() throws IOException { + require(8); + return buffer.readLong(); + } + + @Override public long readLongLe() throws IOException { + require(8); + return buffer.readLongLe(); + } + + @Override public void skip(long byteCount) throws IOException { + if (closed) throw new IllegalStateException("closed"); + while (byteCount > 0) { + if (buffer.size == 0 && source.read(buffer, Segment.SIZE) == -1) { + throw new EOFException(); + } + long toSkip = Math.min(byteCount, buffer.size()); + buffer.skip(toSkip); + byteCount -= toSkip; + } + } + + @Override public long indexOf(byte b) throws IOException { + return indexOf(b, 0); + } + + @Override public long indexOf(byte b, long fromIndex) throws IOException { + if (closed) throw new IllegalStateException("closed"); + while (fromIndex >= buffer.size) { + if (source.read(buffer, Segment.SIZE) == -1) return -1L; + } + long index; + while ((index = buffer.indexOf(b, fromIndex)) == -1) { + fromIndex = buffer.size; + if (source.read(buffer, Segment.SIZE) == -1) return -1L; + } + return index; + } + + @Override public long indexOfElement(ByteString targetBytes) throws IOException { + return indexOfElement(targetBytes, 0); + } + + @Override public long indexOfElement(ByteString targetBytes, long fromIndex) throws IOException { + if (closed) throw new IllegalStateException("closed"); + while (fromIndex >= buffer.size) { + if (source.read(buffer, Segment.SIZE) == -1) return -1L; + } + long index; + while ((index = buffer.indexOfElement(targetBytes, fromIndex)) == -1) { + fromIndex = buffer.size; + if (source.read(buffer, Segment.SIZE) == -1) return -1L; + } + return index; + } + + @Override public InputStream inputStream() { + return new InputStream() { + @Override public int read() throws IOException { + if (closed) throw new IOException("closed"); + if (buffer.size == 0) { + long count = source.read(buffer, Segment.SIZE); + if (count == -1) return -1; + } + return buffer.readByte() & 0xff; + } + + @Override public int read(byte[] data, int offset, int byteCount) throws IOException { + if (closed) throw new IOException("closed"); + Util.checkOffsetAndCount(data.length, offset, byteCount); + + if (buffer.size == 0) { + long count = source.read(buffer, Segment.SIZE); + if (count == -1) return -1; + } + + return buffer.read(data, offset, byteCount); + } + + @Override public int available() throws IOException { + if (closed) throw new IOException("closed"); + return (int) Math.min(buffer.size, Integer.MAX_VALUE); + } + + @Override public void close() throws IOException { + RealBufferedSource.this.close(); + } + + @Override public String toString() { + return RealBufferedSource.this + ".inputStream()"; + } + }; + } + + @Override public void close() throws IOException { + if (closed) return; + closed = true; + source.close(); + buffer.clear(); + } + + @Override public Timeout timeout() { + return source.timeout(); + } + + @Override public String toString() { + return "buffer(" + source + ")"; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okio/Segment.java b/contentstack/src/main/java/com/contentstack/okio/Segment.java new file mode 100755 index 00000000..54d183e4 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/Segment.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okio; + +/** + * A segment of a buffer. + * + *

Each segment in a buffer is a circularly-linked list node referencing + * the following and preceding segments in the buffer. + * + *

Each segment in the pool is a singly-linked list node referencing the rest + * of segments in the pool. + */ +final class Segment { + static final int SIZE = 2048; + final byte[] data = new byte[SIZE]; + /** The next byte of application data byte to read in this segment. */ + int pos; + + /** The first byte of available data ready to be written to. */ + int limit; + + /** Next segment in a linked or circularly-linked list. */ + Segment next; + + /** Previous segment in a circularly-linked list. */ + Segment prev; + + /** + * Removes this segment of a circularly-linked list and returns its successor. + * Returns null if the list is now empty. + */ + public Segment pop() { + Segment result = next != this ? next : null; + prev.next = next; + next.prev = prev; + next = null; + prev = null; + return result; + } + + /** + * Appends {@code segment} after this segment in the circularly-linked list. + * Returns the pushed segment. + */ + public Segment push(Segment segment) { + segment.prev = this; + segment.next = next; + next.prev = segment; + next = segment; + return segment; + } + + /** + * Splits this head of a circularly-linked list into two segments. The first + * segment contains the data in {@code [pos..pos+byteCount)}. The second + * segment contains the data in {@code [pos+byteCount..limit)}. This can be + * useful when moving partial segments from one buffer to another. + * + *

Returns the new head of the circularly-linked list. + */ + public Segment split(int byteCount) { + int aSize = byteCount; + int bSize = (limit - pos) - byteCount; + if (aSize <= 0 || bSize <= 0) throw new IllegalArgumentException(); + + // Which side of the split is larger? We want to copy as few bytes as possible. + if (aSize < bSize) { + // Create a segment of size 'aSize' before this segment. + Segment before = SegmentPool.INSTANCE.take(); + System.arraycopy(data, pos, before.data, before.pos, aSize); + pos += aSize; + before.limit += aSize; + prev.push(before); + return before; + } else { + // Create a new segment of size 'bSize' after this segment. + Segment after = SegmentPool.INSTANCE.take(); + System.arraycopy(data, pos + aSize, after.data, after.pos, bSize); + limit -= bSize; + after.limit += bSize; + push(after); + return this; + } + } + + /** + * Call this when the tail and its predecessor may both be less than half + * full. This will copy data so that segments can be recycled. + */ + public void compact() { + if (prev == this) throw new IllegalStateException(); + if ((prev.limit - prev.pos) + (limit - pos) > SIZE) return; // Cannot compact. + writeTo(prev, limit - pos); + pop(); + SegmentPool.INSTANCE.recycle(this); + } + public void writeTo(Segment sink, int byteCount) { + if (byteCount + (sink.limit - sink.pos) > SIZE) throw new IllegalArgumentException(); + + if (sink.limit + byteCount > SIZE) { + // We can't fit byteCount bytes at the sink's current position. Compact sink first. + System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos); + sink.limit -= sink.pos; + sink.pos = 0; + } + + System.arraycopy(data, pos, sink.data, sink.limit, byteCount); + sink.limit += byteCount; + pos += byteCount; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okio/SegmentPool.java b/contentstack/src/main/java/com/contentstack/okio/SegmentPool.java new file mode 100755 index 00000000..77b212a8 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/SegmentPool.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okio; + +/** + * A collection of unused segments, necessary to avoid GC churn and zero-fill. + * This pool is a thread-safe static singleton. + */ +final class SegmentPool { + static final SegmentPool INSTANCE = new SegmentPool(); + + /** The maximum number of bytes to pool. */ + static final long MAX_SIZE = 64 * 1024; // 64 KiB. + + /** Singly-linked list of segments. */ + private Segment next; + + /** Total bytes in this pool. */ + long byteCount; + + private SegmentPool() { + } + + Segment take() { + synchronized (this) { + if (next != null) { + Segment result = next; + next = result.next; + result.next = null; + byteCount -= Segment.SIZE; + return result; + } + } + return new Segment(); // Pool is empty. Don't zero-fill while holding a lock. + } + + void recycle(Segment segment) { + if (segment.next != null || segment.prev != null) throw new IllegalArgumentException(); + synchronized (this) { + if (byteCount + Segment.SIZE > MAX_SIZE) return; // Pool is full. + byteCount += Segment.SIZE; + segment.next = next; + segment.pos = segment.limit = 0; + next = segment; + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okio/Sink.java b/contentstack/src/main/java/com/contentstack/okio/Sink.java new file mode 100755 index 00000000..00cdb377 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/Sink.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okio; + +import java.io.Closeable; +import java.io.IOException; + +/** + * Receives a stream of bytes. Use this interface to write data wherever it's + * needed: to the network, storage, or a buffer in memory. Sinks may be layered + * to transform received data, such as to compress, encrypt, throttle, or add + * protocol framing. + * + *

Most application code shouldn't operate on a sink directly, but rather + * {@link BufferedSink} which is both more efficient and more convenient. Use + * {@link Okio#buffer(Sink)} to wrap any sink with a buffer. + * + *

Sinks are easy to test: just use an {@link Buffer} in your tests, and + * read from it to confirm it received the data that was expected. + * + *

Comparison with OutputStream

+ * This interface is functionally equivalent to {@link java.io.OutputStream}. + * + *

{@code OutputStream} requires multiple layers when emitted data is + * heterogeneous: a {@code DataOutputStream} for primitive values, a {@code + * BufferedOutputStream} for buffering, and {@code OutputStreamWriter} for + * charset encoding. This class uses {@code BufferedSink} for all of the above. + * + *

Sink is also easier to layer: there is no {@linkplain + * java.io.OutputStream#write(int) single-byte write} method that is awkward to + * implement efficiently. + * + *

Interop with OutputStream

+ * Use {@link Okio#sink} to adapt an {@code OutputStream} to a sink. Use {@link + * BufferedSink#outputStream} to adapt a sink to an {@code OutputStream}. + */ +public interface Sink extends Closeable { + /** Removes {@code byteCount} bytes from {@code source} and appends them to this. */ + void write(Buffer source, long byteCount) throws IOException; + + /** Pushes all buffered bytes to their final destination. */ + void flush() throws IOException; + + /** Returns the timeout for this sink. */ + Timeout timeout(); + + /** + * Pushes all buffered bytes to their final destination and releases the + * resources held by this sink. It is an error to write a closed sink. It is + * safe to close a sink more than once. + */ + @Override void close() throws IOException; +} diff --git a/contentstack/src/main/java/com/contentstack/okio/Source.java b/contentstack/src/main/java/com/contentstack/okio/Source.java new file mode 100755 index 00000000..d9eb88aa --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/Source.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okio; + +import java.io.Closeable; +import java.io.IOException; + +/** + * Supplies a stream of bytes. Use this interface to read data from wherever + * it's located: from the network, storage, or a buffer in memory. Sources may + * be layered to transform supplied data, such as to decompress, decrypt, or + * remove protocol framing. + * + *

Most applications shouldn't operate on a source directly, but rather + * {@link BufferedSource} which is both more efficient and more convenient. Use + * {@link Okio#buffer(Source)} to wrap any source with a buffer. + * + *

Sources are easy to test: just use an {@link Buffer} in your tests, and + * fill it with the data your application is to read. + * + *

Comparison with InputStream

+ * This interface is functionally equivalent to {@link java.io.InputStream}. + * + *

{@code InputStream} requires multiple layers when consumed data is + * heterogeneous: a {@code DataInputStream} for primitive values, a {@code + * BufferedInputStream} for buffering, and {@code InputStreamReader} for + * strings. This class uses {@code BufferedSource} for all of the above. + * + *

Source avoids the impossible-to-implement {@linkplain + * java.io.InputStream#available available()} method. Instead callers specify + * how many bytes they {@link BufferedSource#require require}. + * + *

Source omits the unsafe-to-compose {@linkplain java.io.InputStream#mark + * mark and reset} state that's tracked by {@code InputStream}; callers instead + * just buffer what they need. + * + *

When implementing a source, you need not worry about the {@linkplain + * java.io.InputStream#read single-byte read} method that is awkward to + * implement efficiently and that returns one of 257 possible values. + * + *

And source has a stronger {@code skip} method: {@link BufferedSource#skip} + * won't return prematurely. + * + *

Interop with InputStream

+ * Use {@link Okio#source} to adapt an {@code InputStream} to a source. Use + * {@link BufferedSource#inputStream} to adapt a source to an {@code + * InputStream}. + */ +public interface Source extends Closeable { + /** + * Removes at least 1, and up to {@code byteCount} bytes from this and appends + * them to {@code sink}. Returns the number of bytes read, or -1 if this + * source is exhausted. + */ + long read(Buffer sink, long byteCount) throws IOException; + + /** Returns the timeout for this source. */ + Timeout timeout(); + + /** + * Closes this source and releases the resources held by this source. It is an + * error to read a closed source. It is safe to close a source more than once. + */ + @Override void close() throws IOException; +} diff --git a/contentstack/src/main/java/com/contentstack/okio/Timeout.java b/contentstack/src/main/java/com/contentstack/okio/Timeout.java new file mode 100755 index 00000000..88dad820 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/Timeout.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okio; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.util.concurrent.TimeUnit; + +/** + * A policy on how much time to spend on a task before giving up. When a task + * times out, it is left in an unspecified state and should be abandoned. For + * example, if reading from a source times out, that source should be closed and + * the read should be retried later. If writing to a sink times out, the same + * rules apply: close the sink and retry later. + * + *

Timeouts and Deadlines

+ * This class offers two complementary controls to define a timeout policy. + * + *

Timeouts specify the maximum time to wait for a single + * operation to complete. Timeouts are typically used to detect problems like + * network partitions. For example, if a remote peer doesn't return any + * data for ten seconds, we may assume that the peer is unavailable. + * + *

Deadlines specify the maximum time to spend on a job, + * composed of one or more operations. Use deadlines to set an upper bound on + * the time invested on a job. For example, a battery-conscious app may limit + * how much time it spends preloading content. + */ +public class Timeout { + /** + * An empty timeout that neither tracks nor detects timeouts. Use this when + * timeouts aren't necessary, such as in implementations whose operations + * do not block. + */ + public static final Timeout NONE = new Timeout() { + @Override public Timeout timeout(long timeout, TimeUnit unit) { + return this; + } + + @Override public Timeout deadlineNanoTime(long deadlineNanoTime) { + return this; + } + + @Override public void throwIfReached() throws IOException { + } + }; + + /** + * True if {@code deadlineNanoTime} is defined. There is no equivalent to null + * or 0 for {@link System#nanoTime}. + */ + private boolean hasDeadline; + private long deadlineNanoTime; + private long timeoutNanos; + + public Timeout() { + } + + /** + * Wait at most {@code timeout} time before aborting an operation. Using a + * per-operation timeout means that as long as forward progress is being made, + * no sequence of operations will fail. + * + *

If {@code timeout == 0}, operations will run indefinitely. (Operating + * system timeouts may still apply.) + */ + public Timeout timeout(long timeout, TimeUnit unit) { + if (timeout < 0) throw new IllegalArgumentException("timeout < 0: " + timeout); + if (unit == null) throw new IllegalArgumentException("unit == null"); + this.timeoutNanos = unit.toNanos(timeout); + return this; + } + + /** Returns the timeout in nanoseconds, or {@code 0} for no timeout. */ + public long timeoutNanos() { + return timeoutNanos; + } + + /** Returns true if a deadline is enabled. */ + public boolean hasDeadline() { + return hasDeadline; + } + + /** + * Returns the {@linkplain System#nanoTime() nano time} when the deadline will + * be reached. + * + * @throws IllegalStateException if no deadline is set. + */ + public long deadlineNanoTime() { + if (!hasDeadline) throw new IllegalStateException("No deadline"); + return deadlineNanoTime; + } + + /** + * Sets the {@linkplain System#nanoTime() nano time} when the deadline will be + * reached. All operations must complete before this time. Use a deadline to + * set a maximum bound on the time spent on a sequence of operations. + */ + public Timeout deadlineNanoTime(long deadlineNanoTime) { + this.hasDeadline = true; + this.deadlineNanoTime = deadlineNanoTime; + return this; + } + + /** Set a deadline of now plus {@code duration} time. */ + public final Timeout deadline(long duration, TimeUnit unit) { + if (duration <= 0) throw new IllegalArgumentException("duration <= 0: " + duration); + if (unit == null) throw new IllegalArgumentException("unit == null"); + return deadlineNanoTime(System.nanoTime() + unit.toNanos(duration)); + } + + /** Clears the timeout. Operating system timeouts may still apply. */ + public Timeout clearTimeout() { + this.timeoutNanos = 0; + return this; + } + + /** Clears the deadline. */ + public Timeout clearDeadline() { + this.hasDeadline = false; + return this; + } + + /** + * Throws an {@link IOException} if the deadline has been reached or if the + * current thread has been interrupted. This method doesn't detect timeouts; + * that should be implemented to asynchronously abort an in-progress + * operation. + */ + public void throwIfReached() throws IOException { + if (Thread.interrupted()) { + throw new InterruptedIOException(); + } + + if (hasDeadline && System.nanoTime() > deadlineNanoTime) { + throw new IOException("deadline reached"); + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/okio/Util.java b/contentstack/src/main/java/com/contentstack/okio/Util.java new file mode 100755 index 00000000..27b06bb9 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/Util.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.okio; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +final class Util { + /** A cheap and type-safe constant for the UTF-8 Charset. */ + public static final Charset UTF_8 = StandardCharsets.UTF_8; + + private Util() { + } + + public static void checkOffsetAndCount(long size, long offset, long byteCount) { + if ((offset | byteCount) < 0 || offset > size || size - offset < byteCount) { + throw new ArrayIndexOutOfBoundsException( + String.format("size=%s offset=%s byteCount=%s", size, offset, byteCount)); + } + } + + public static short reverseBytesShort(short s) { + int i = s & 0xffff; + int reversed = (i & 0xff00) >>> 8 + | (i & 0x00ff) << 8; + return (short) reversed; + } + + public static int reverseBytesInt(int i) { + return (i & 0xff000000) >>> 24 + | (i & 0x00ff0000) >>> 8 + | (i & 0x0000ff00) << 8 + | (i & 0x000000ff) << 24; + } + + public static long reverseBytesLong(long v) { + return (v & 0xff00000000000000L) >>> 56 + | (v & 0x00ff000000000000L) >>> 40 + | (v & 0x0000ff0000000000L) >>> 24 + | (v & 0x000000ff00000000L) >>> 8 + | (v & 0x00000000ff000000L) << 8 + | (v & 0x0000000000ff0000L) << 24 + | (v & 0x000000000000ff00L) << 40 + | (v & 0x00000000000000ffL) << 56; + } + + /** + * Throws {@code t}, even if the declared throws clause doesn't permit it. + * This is a terrible – but terribly convenient – hack that makes it easy to + * catch and rethrow exceptions after cleanup. See Java Puzzlers #43. + */ + public static void sneakyRethrow(Throwable t) { + Util.sneakyThrow2(t); + } + + @SuppressWarnings("unchecked") + private static void sneakyThrow2(Throwable t) throws T { + throw (T) t; + } +} diff --git a/contentstack/src/main/java/com/contentstack/okio/package-info.java b/contentstack/src/main/java/com/contentstack/okio/package-info.java new file mode 100755 index 00000000..778e016a --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/okio/package-info.java @@ -0,0 +1,5 @@ +/** + * Okio complements {@link java.io} and {@link java.nio} to make it much easier to access, store, + * and process your data. + */ +package com.contentstack.okio; diff --git a/contentstack/src/main/java/com/contentstack/sdk/Asset.java b/contentstack/src/main/java/com/contentstack/sdk/Asset.java new file mode 100755 index 00000000..41425561 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/Asset.java @@ -0,0 +1,660 @@ +package com.contentstack.sdk; + +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; + +import com.contentstack.sdk.utilities.CSAppConstants; +import com.contentstack.sdk.utilities.CSAppUtils; +import com.contentstack.sdk.utilities.CSController; +import com.contentstack.sdk.utilities.ContentstackUtil; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.util.Calendar; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * Asset class to fetch files details on Conentstack server. + * + * @author contentstack.com + */ +public class Asset { + + private final static String TAG = "Asset"; + protected String assetUid = null; + protected String contentType = null; + protected String fileSize = null; + protected String fileName = null; + protected String uploadUrl = null; + protected JSONObject json = null; + protected String[] tagsArray = null; + JSONObject urlQueries = new JSONObject(); + protected ArrayMap headerGroup_app; + protected ArrayMap headerGroup_local; + protected Stack stackInstance; + private CachePolicy cachePolicyForCall = CachePolicy.IGNORE_CACHE; + + private final long maxCacheTimeForCall = 0; + private final long defaultCacheTimeInterval = 0; + + protected Asset() { + this.headerGroup_local = new ArrayMap<>(); + this.headerGroup_app = new ArrayMap<>(); + } + + protected Asset(String assetUid) { + this.assetUid = assetUid; + this.headerGroup_local = new ArrayMap<>(); + this.headerGroup_app = new ArrayMap<>(); + } + + protected void setStackInstance(Stack stack) { + this.stackInstance = stack; + this.headerGroup_app = stack.localHeader; + } + + /** + * Creates new instance of {@link Asset} from valid {@link JSONObject}. + * If JSON object is not appropriate then it will return null. + * + * @param jsonObject json object of particular file attached in the built object.
+ *

+ * {@link Asset} can be generate using of data filled {@link Entry} and {@link #configure(JSONObject)}.
+ * + *

Example :
+ *
1. Single Attachment :-
+ *

+     *            Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken",  config);
+     *            Asset assetObject = stack.asset("assetUid");
+ * assetObject.configure(entryObject.getJSONObject(attached_image_field_uid));
+ * + *
2. Multiple Attachment :-
+ * + *
+     *            JSONArray array = entryObject.getJSONArray(Attach_Image_Field_Uid);
+ * for (int i = 0; i < array.length(); i++) {
+ * Asset assetObject = stack.asset("assetUid");
+ * assetObject.configure(entryObject.getJSONObject(attached_image_field_uid));
+ * }
+ *
+ * @return {@link Asset} instance. + */ + public Asset configure(JSONObject jsonObject) { + + AssetModel model = null; + + model = new AssetModel(jsonObject, true, false); + + this.contentType = model.contentType; + this.fileSize = model.fileSize; + this.uploadUrl = model.uploadUrl; + this.fileName = model.fileName; + this.json = model.json; + this.assetUid = model.uploadedUid; + this.setTags(model.tags); + + model = null; + + return this; + } + + + /** + * To set headers for Contentstack rest calls. + *
+ * Scope is limited to this object only. + * + * @param key header name. + * @param value header value against given header name. + * + *

Example :
+ *
+     *                                        Asset assetObject = Contentstack.stack(context, "apiKey", "deliveryToken",  config).asset("assetUid");
+     *                                        assetObject.setHeader("custom_header_key", "custom_header_value");
+     *                                        
+ */ + public void setHeader(String key, String value) { + + if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(value)) { + removeHeader(key); + headerGroup_local.put(key, value); + } + } + + /** + * Remove a header for a given key from headers. + *
+ * Scope is limited to this object only. + * + * @param key header key. + * + *

Example :
+ *
+     *                                  Asset assetObject = Contentstack.stack(context, "apiKey", "deliveryToken",  config).asset("assetUid");
+     *                                  assetObject.removeHeader("custom_header_key");
+     *                                  
+ */ + public void removeHeader(String key) { + if (headerGroup_local != null) { + + if (!TextUtils.isEmpty(key)) { + if (headerGroup_local.containsKey(key)) { + headerGroup_local.remove(key); + } + } + } + } + + /** + * To set uid of media file which is uploaded on Contentstack server. + * + * @param assetUid upload uid. + * + *

Example :
+ *
+     *                                                 Asset assetObject = Contentstack.stack(context, "apiKey", "deliveryToken",   config).asset("assetUid");*
+     *                                                 assetObject.setUid("upload_uid");
+     *                                                 
+ */ + protected void setUid(String assetUid) { + if (!TextUtils.isEmpty(assetUid)) { + this.assetUid = assetUid; + } + } + + /** + * Returns media file upload uid. You will get uploaded uid after uploading media file on Contentstack server. + * + *

Example :
+ *
+     * String uid = assetObject.getAssetUid();
+     * 
+ */ + public String getAssetUid() { + return assetUid; + } + + /** + * Returns content type of the uploaded file. + * + *

Example :
+ *
+     * String contentType = assetObject.getFileType();
+     * 
+ */ + public String getFileType() { + return contentType; + } + + /** + * Returns file queueSize of the uploaded file. + * + *

+ *
Note :
file size will receive in bytes number. + * + *

Example :
+ *

+     * String queueSize = assetObject.getFileSize();
+     * 
+ */ + public String getFileSize() { + return fileSize; + } + + /** + * Returns file name of the uploaded file. + * + *

Example :
+ *
+     * String fileName = assetObject.getFileName();
+     * 
+ */ + public String getFileName() { + return fileName; + } + + /** + * Returns upload url by which you can download media file uploaded on Contentstack server. + * You will get uploaded url after uploading media file on Contentstack server. + * + *

Example :
+ *
+     * String url = assetObject.getUrl();
+     * 
+ */ + public String getUrl() { + return uploadUrl; + } + + /** + * Returns JSON representation of this {@link Asset} instance data. + * + *

Example :
+ *
+     *  JSONObject json = assetObject.toJSON();
+     * 
+ */ + public JSONObject toJSON() { + return json; + } + + + /** + * Get {@link Calendar} value of creation time of entry. + * + * + *

Example :
+ *
+     * Calendar createdAt = assetObject.getCreateAt("key");
+     * 
+ */ + public Calendar getCreateAt() { + + try { + String value = json.optString("created_at"); + return ContentstackUtil.parseDate(value, null); + } catch (Exception e) { + CSAppUtils.showLog(TAG, "-----------------getCreateAtDate|" + e); + } + return null; + } + + /** + * Get uid who created this entry. + * + *

Example :
+ *
+     * String createdBy_uid = assetObject.getCreatedBy();
+     * 
+ */ + public String getCreatedBy() { + + return json.optString("created_by"); + } + + /** + * Get {@link Calendar} value of updating time of entry. + * + * + *

Example :
+ *
+     * Calendar updatedAt = assetObject.getUpdateAt("key");
+     * 
+ */ + public Calendar getUpdateAt() { + + try { + String value = json.optString("updated_at"); + return ContentstackUtil.parseDate(value, null); + } catch (Exception e) { + CSAppUtils.showLog(TAG, e.getLocalizedMessage()); + } + return null; + } + + /** + * Get uid who updated this entry. + * + *

Example :
+ *
+     * String updatedBy_uid = assetObject.getUpdatedBy();
+     * 
+ */ + public String getUpdatedBy() { + + return json.optString("updated_by"); + } + + /** + * Get {@link Calendar} value of deletion time of entry. + * + * + *

Example :
+ *
+     * Calendar updatedAt = entry.getUpdateAt("key");
+     * 
+ */ + public Calendar getDeleteAt() { + + try { + String value = json.optString("deleted_at"); + return ContentstackUtil.parseDate(value, null); + } catch (Exception e) { + CSAppUtils.showLog(TAG, e.getLocalizedMessage()); + } + return null; + } + + /** + * Get uid who deleted this entry. + * + *

Example :
+ *
+     * String deletedBy_uid = assetObject.getDeletedBy();
+     * 
+ */ + public String getDeletedBy() { + return json.optString("deleted_by"); + } + + + /** + * Get tags. + * + *

Example :
+ *
+     * String[] tags = assetObject.getURL();
+     * 
+ */ + public String[] getTags() { + return tagsArray; + } + + /** + * To set cache policy using {@link Query} instance. + * + * @param cachePolicy {@link CachePolicy} instance. + *

+ * {@link Query} object, so you can chain this call. + * + *

Example :
+ *

+     *                                                               Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken",  "stag", false);
+     *                                                               Asset assetObject = stack.asset("assetUid");
+ * assetObject.setCachePolicy(NETWORK_ELSE_CACHE); + *
+ */ + public void setCachePolicy(CachePolicy cachePolicy) { + this.cachePolicyForCall = cachePolicy; + } + + /** + * Fetch a particular asset using uid. + * + * @param callback {@link FetchResultCallback} instance for success and failure result. + * + *

Example :
+ *
+     *                                                  Asset asset = stack.asset("assetUid");
+     *                                                  asset.fetch(new FetchResultCallback() {
+     *                                                    @Override
+     *                                                    public void onCompletion(ResponseType responseType, Error error) {
+     *                                                          if(error == null){
+     *                                                            //Success Block.
+     *                                                          }else {
+     *                                                            //Fail Block.
+     *                                                          }
+     *                                                    }
+     *                                                  });
+     *                                                 
+ */ + public void fetch(FetchResultCallback callback) { + + try { + + String URL = "/" + stackInstance.VERSION + "/assets/" + assetUid; + ArrayMap headers = getHeader(headerGroup_local); + if (headers.containsKey("environment")) { + urlQueries.put("environment", headers.get("environment")); + } + + String mainStringForMD5 = URL + new JSONObject().toString() + headers.toString(); + String md5Value = new CSAppUtils().getMD5FromString(mainStringForMD5.trim()); + File cacheFile = new File(CSAppConstants.cacheFolderName + File.separator + md5Value); + + switch (cachePolicyForCall) { + + case IGNORE_CACHE: + + case NETWORK_ONLY: + fetchFromNetwork(URL, urlQueries, headers, cacheFile.getPath(), callback); + break; + + case CACHE_ONLY: + fetchFromCache(cacheFile, callback); + break; + + case CACHE_ELSE_NETWORK: + + if (cacheFile.exists()) { + boolean needToSendCall = false; + // if (maxCacheTimeForCall > 0) { + // needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) maxCacheTimeForCall); + // } else { + // needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) defaultCacheTimeInterval); + // } + needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) maxCacheTimeForCall); + + if (needToSendCall) { + fetchFromNetwork(URL, urlQueries, headers, cacheFile.getPath(), callback); + + } else { + setCacheModel(cacheFile, callback); + } + + } else { + fetchFromNetwork(URL, urlQueries, headers, cacheFile.getPath(), callback); + } + + break; + + case CACHE_THEN_NETWORK: + if (cacheFile.exists()) { + setCacheModel(cacheFile, callback); + } + + // from network + fetchFromNetwork(URL, urlQueries, headers, cacheFile.getPath(), callback); + break; + + case NETWORK_ELSE_CACHE: + + if (CSAppConstants.isNetworkAvailable) { + fetchFromNetwork(URL, urlQueries, headers, cacheFile.getPath(), callback); + } else { + fetchFromCache(cacheFile, callback); + } + + break; + } + + } catch (Exception e) { + Error error = new Error(); + error.setErrorMessage(CSAppConstants.ErrorMessage_JsonNotProper); + callback.onRequestFail(ResponseType.UNKNOWN, error); + } + } + + private void fetchFromNetwork(String URL, JSONObject urlQueries, ArrayMap headers, String cacheFilePath, FetchResultCallback callback) { + if (callback != null) { + HashMap urlParams = getUrlParams(urlQueries); + new CSBackgroundTask(this, stackInstance, CSController.FETCHASSETS, URL, headers, urlParams, new JSONObject(), cacheFilePath, CSAppConstants.callController.ASSET.toString(), false, CSAppConstants.RequestMethod.GET, callback); + } + } + + + private void fetchFromCache(File cacheFile, FetchResultCallback callback) { + Error error = null; + if (cacheFile.exists()) { + boolean needToSendCall = false; + + // if (maxCacheTimeForCall > 0) { + // needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) maxCacheTimeForCall); + // } else { + // needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) defaultCacheTimeInterval); + // } + needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) maxCacheTimeForCall); + + if (needToSendCall) { + error = new Error(); + error.setErrorMessage(CSAppConstants.ErrorMessage_EntryNotFoundInCache); + + } else { + setCacheModel(cacheFile, callback); + } + } else { + error = new Error(); + error.setErrorMessage(CSAppConstants.ErrorMessage_EntryNotFoundInCache); + } + + if (callback != null && error != null) { + callback.onRequestFail(ResponseType.CACHE, error); + } + } + + //Asset modeling from cache. + private void setCacheModel(File cacheFile, FetchResultCallback callback) { + + AssetModel model = new AssetModel(CSAppUtils.getJsonFromCacheFile(cacheFile), false, true); + + this.contentType = model.contentType; + this.fileSize = model.fileSize; + this.uploadUrl = model.uploadUrl; + this.fileName = model.fileName; + this.json = model.json; + this.assetUid = model.uploadedUid; + this.setTags(model.tags); + + model = null; + + if (callback != null) { + callback.onRequestFinish(ResponseType.CACHE); + } + } + + /** + * To set tags for this objects + * + * @param tags array of tag. + * @return {@link Asset} object, so you can chain this call. + *

Example :
+ *
+     * Asset assetObject = Contentstack.stack(context, "apiKey", "deliveryToken",  config).asset("assetUid");
+     * assetObject.setTags(new String[]{"tag1", "tag2"});
+     * 
+ */ + protected Asset setTags(String[] tags) { + tagsArray = tags; + return this; + } + + private HashMap getUrlParams(JSONObject urlQueriesJSON) { + HashMap hashMap = new HashMap<>(); + if (urlQueriesJSON != null && urlQueriesJSON.length() > 0) { + Iterator iter = urlQueriesJSON.keys(); + while (iter.hasNext()) { + String key = iter.next(); + try { + Object value = urlQueriesJSON.opt(key); + hashMap.put(key, value); + } catch (Exception e) { + CSAppUtils.showLog(TAG, "----------------setQueryJson" + e.toString()); + } + } + return hashMap; + } + return null; + } + + private ArrayMap getHeader(ArrayMap localHeader) { + ArrayMap mainHeader = headerGroup_app; + ArrayMap classHeaders = new ArrayMap<>(); + if (localHeader != null && localHeader.size() > 0) { + if (mainHeader != null && mainHeader.size() > 0) { + for (Map.Entry entry : localHeader.entrySet()) { + String key = entry.getKey(); + classHeaders.put(key, entry.getValue()); + } + for (Map.Entry entry : mainHeader.entrySet()) { + String key = entry.getKey(); + if (!classHeaders.containsKey(key)) { + classHeaders.put(key, entry.getValue()); + } + } + return classHeaders; + } else { + return localHeader; + } + } else { + return headerGroup_app; + } + } + + /** + * This method adds key and value to an Entry. + * + * @param key The key as string which needs to be added to an Asset + * @param value The value as string which needs to be added to an Asset + * @return {@link Asset} + * + *

Example :
+ *
+     * final Asset asset = stack.asset("assetUid");
+     * asset.addParam("key", "some_value");
+     *
+     *  asset.fetch(new FetchResultCallback() {
+     *    @Override
+     *    public void onCompletion(ResponseType responseType, Error error) {
+     *          if(error == null){
+     *            //Success Block.
+     *          }else {
+     *            //Fail Block.
+     *          }
+     *    }
+     *  });
+     * 
+ */ + public Asset addParam(String key, String value) { + if (key != null && value != null) { + try { + urlQueries.put(key, value); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + } + return this; + } + + + /** + * Include the dimensions (height and width) of the image in the response. + * Supported image types: JPG, GIF, PNG, WebP, BMP, TIFF, SVG, and PSD + * + * @return Asset + */ + public Asset includeDimension() { + try { + urlQueries.put("include_dimension", true); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + return this; + } + + + /** + * Retrieve the published content of the fallback locale if an entry is not localized in specified locale + * + * @return {@link Asset} object, so you can chain this call. + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "environment");
+     *     final Asset asset = stack.asset("asset_uid");
+     *     asset.includeFallback();
+     * 
+ */ + public Asset includeFallback() { + try { + urlQueries.put("include_fallback", true); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + return this; + } + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/AssetLibrary.java b/contentstack/src/main/java/com/contentstack/sdk/AssetLibrary.java new file mode 100644 index 00000000..1c16ea01 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/AssetLibrary.java @@ -0,0 +1,462 @@ +package com.contentstack.sdk; + + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; + +import com.contentstack.sdk.utilities.CSAppConstants; +import com.contentstack.sdk.utilities.CSAppUtils; +import com.contentstack.sdk.utilities.CSController; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * AssetLibrary class to fetch all files details on Conentstack server. + * + * @author contentstack.com. Inc + */ +public class AssetLibrary implements INotifyClass { + + private final static String TAG = "AssetLibrary"; + private Stack stackInstance; + private ArrayMap stackHeader; + private ArrayMap localHeader; + private JSONObject urlQueries; + private FetchAssetsCallback assetsCallback; + private int count; + private static CachePolicy cachePolicyForCall = CachePolicy.IGNORE_CACHE; + private long maxCacheTimeForCall = 0; + private long defaultCacheTimeInterval = 0; + + /** + * Sorting order enum for {@link AssetLibrary}. + * + * @author Contentstack + */ + public enum ORDERBY { + ASCENDING, + DESCENDING + } + + protected AssetLibrary() { + this.localHeader = new ArrayMap(); + this.urlQueries = new JSONObject(); + } + + protected void setStackInstance(Stack stack) { + this.stackInstance = stack; + this.stackHeader = stack.localHeader; + } + + /** + * To set headers for Contentstack rest calls. + *
+ * Scope is limited to this object only. + * + * @param key header name. + * @param value header value against given header name. + * + *

Example :
+ *
+     *  AssetLibrary assetLibObject = Contentstack.stack(context, "apiKey", "deliveryToken",  config).assetLibrary();
+     *  assetLibObject.setHeader("custom_header_key", "custom_header_value");
+     *  
+ */ + public void setHeader(String key, String value) { + if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(value)) { + localHeader.put(key, value); + } + } + + /** + * Remove a header for a given key from headers. + *
+ * Scope is limited to this object only. + * + * @param key header key. + * + *

Example :
+ *
+     *                                                                              AssetLibrary assetLibObject = Contentstack.stack(context, "apiKey", "deliveryToken",  config).assetLibrary();
+     *                                                                              assetLibObject.removeHeader("custom_header_key");
+     *                                                                              
+ */ + public void removeHeader(String key) { + if (!TextUtils.isEmpty(key)) { + localHeader.remove(key); + } + } + + /** + * Sort assets by fieldUid. + * + * @param key field Uid. + * @param orderby {@link ORDERBY} value for ascending or descending. + * @return {@link AssetLibrary} object, so you can chain this call. + * + *

Example :
+ *
+     * AssetLibrary assetLibObject = Contentstack.stack(context, "apiKey", "deliveryToken",  config).assetLibrary();
+     *
+     * assetLibObject.sort("fieldUid", AssetLibrary.ORDERBY.ASCENDING);
+     * 
+ */ + public AssetLibrary sort(String key, ORDERBY orderby) { + try { + switch (orderby) { + case ASCENDING: + urlQueries.put("asc", key); + break; + + case DESCENDING: + urlQueries.put("desc", key); + break; + } + } catch (Exception e) { + throwException("sort", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + + return this; + } + + /** + * Retrieve count and data of assets in result. + * + * @return {@link AssetLibrary} object, so you can chain this call. + * + *

Example :
+ *
+     *    AssetLibrary assetLibObject = Contentstack.stack(context, "apiKey", "deliveryToken",  config).assetLibrary();
+     *    assetLibObject.includeCount();
+     * 
+ */ + public AssetLibrary includeCount() { + try { + urlQueries.put("include_count", "true"); + } catch (Exception e) { + throwException("includeCount", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + return this; + } + + /** + * Retrieve relative urls objects in result. + * + * @return {@link AssetLibrary} object, so you can chain this call. + * + *

Example :
+ *
+     *    AssetLibrary assetLibObject = Contentstack.stack(context, "deliveryToken", "deliveryToken",  config).assetLibrary();
+     *    assetLibObject.includeRelativeUrl();
+     * 
+ */ + public AssetLibrary includeRelativeUrl() { + try { + urlQueries.put("relative_urls", "true"); + } catch (Exception e) { + throwException("relative_urls", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + return this; + } + + /** + * Get a count of assets in success callback of {@link FetchAssetsCallback}. + */ + public int getCount() { + return count; + } + + + /** + * To set cache policy using {@link Query} instance. + * + * @param cachePolicy {@link CachePolicy} instance. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *      Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "stag", false);
+     *      {@link AssetLibrary} assetLibObject = stack.assetLibrary();
+ * assetLibObject.setCachePolicy(NETWORK_ELSE_CACHE); + *
+ */ + public void setCachePolicy(CachePolicy cachePolicy) { + this.cachePolicyForCall = cachePolicy; + } + + /** + * Fetch a all asset. + * + * @param assetsCallback {@link FetchAssetsCallback} instance for success and failure result. + * + *

Example :
+ *
+     *                         AssetLibrary assetLibObject = Contentstack.stack(context, "apiKey", "deliveryToken",  config).assetLibrary();
+     *                         assetLibObject.fetchAll(new FetchAssetsCallback() {
+     *                         public void onCompletion(ResponseType responseType, List assets, Error error) {
+     *                            if (error == null) {
+     *                                 //Success Block.
+     *                           } else {
+     *                                //Error Block.
+     *                           }
+     *                         }
+     *                          });
+     *                          
+ */ + public void fetchAll(FetchAssetsCallback assetsCallback) { + try { + this.assetsCallback = assetsCallback; + String URL = "/" + stackInstance.VERSION + "/assets"; + ArrayMap headers = getHeader(localHeader); + if (headers.containsKey("environment")) { + urlQueries.put("environment", headers.get("environment")); + } + String mainStringForMD5 = URL + new JSONObject().toString() + headers.toString(); + String md5Value = new CSAppUtils().getMD5FromString(mainStringForMD5.trim()); + File cacheFile = new File(CSAppConstants.cacheFolderName + File.separator + md5Value); + switch (cachePolicyForCall) { + case IGNORE_CACHE: + fetchFromNetwork(URL, urlQueries, headers, cacheFile.getPath(), assetsCallback); + break; + case NETWORK_ONLY: + fetchFromNetwork(URL, urlQueries, headers, cacheFile.getPath(), assetsCallback); + break; + case CACHE_ONLY: + fetchFromCache(cacheFile, assetsCallback); + break; + case CACHE_ELSE_NETWORK: + if (cacheFile.exists()) { + boolean needToSendCall = false; + // if (maxCacheTimeForCall > 0) { + // needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) maxCacheTimeForCall); + // } else { + // needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) defaultCacheTimeInterval); + // } + needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) maxCacheTimeForCall); + if (needToSendCall) { + fetchFromNetwork(URL, urlQueries, headers, cacheFile.getPath(), assetsCallback); + } else { + setCacheModel(cacheFile, assetsCallback); + } + } else { + fetchFromNetwork(URL, urlQueries, headers, cacheFile.getPath(), assetsCallback); + } + break; + case CACHE_THEN_NETWORK: + if (cacheFile.exists()) { + setCacheModel(cacheFile, assetsCallback); + } + fetchFromNetwork(URL, urlQueries, headers, cacheFile.getPath(), assetsCallback); + break; + case NETWORK_ELSE_CACHE: + if (CSAppConstants.isNetworkAvailable) { + fetchFromNetwork(URL, urlQueries, headers, cacheFile.getPath(), assetsCallback); + } else { + fetchFromCache(cacheFile, assetsCallback); + } + break; + } + + } catch (Exception e) { + CSAppUtils.showLog(TAG, e.toString()); + } + } + + private void fetchFromNetwork(String URL, JSONObject urlQueries, ArrayMap headers, String cacheFilePath, FetchAssetsCallback callback) { + if (callback != null) { + HashMap urlParams = getUrlParams(urlQueries); + new CSBackgroundTask(this, stackInstance, CSController.FETCHALLASSETS, URL, headers, urlParams, new JSONObject(), cacheFilePath, CSAppConstants.callController.ASSETLIBRARY.toString(), false, CSAppConstants.RequestMethod.GET, assetsCallback); + } + } + + private void fetchFromCache(File cacheFile, FetchAssetsCallback callback) { + Error error = null; + if (cacheFile.exists()) { + boolean needToSendCall = false; + // if (maxCacheTimeForCall > 0) { + // needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) maxCacheTimeForCall); + // } else { + // needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) defaultCacheTimeInterval); + // } + needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) maxCacheTimeForCall); + if (needToSendCall) { + error = new Error(); + error.setErrorMessage(CSAppConstants.ErrorMessage_EntryNotFoundInCache); + } else { + setCacheModel(cacheFile, callback); + } + } else { + error = new Error(); + error.setErrorMessage(CSAppConstants.ErrorMessage_EntryNotFoundInCache); + } + + if (callback != null && error != null) { + callback.onRequestFail(ResponseType.CACHE, error); + } + } + + //Asset modeling from cache. + private void setCacheModel(File cacheFile, FetchAssetsCallback callback) { + + AssetsModel assetsModel = new AssetsModel(CSAppUtils.getJsonFromCacheFile(cacheFile), true); + List objectList = assetsModel.objects; + assetsModel = null; + count = objectList.size(); + List assets = new ArrayList(); + if (objectList != null && objectList.size() > 0) { + for (Object object : objectList) { + AssetModel model = (AssetModel) object; + Asset asset = stackInstance.asset(); + + asset.contentType = model.contentType; + asset.fileSize = model.fileSize; + asset.uploadUrl = model.uploadUrl; + asset.fileName = model.fileName; + asset.json = model.json; + asset.assetUid = model.uploadedUid; + asset.setTags(model.tags); + model = null; + + assets.add(asset); + } + } + + if (assetsCallback != null) { + assetsCallback.onRequestFinish(ResponseType.CACHE, assets); + } + } + + + private HashMap getUrlParams(JSONObject urlQueriesJSON) { + + HashMap hashMap = new HashMap<>(); + + if (urlQueriesJSON != null && urlQueriesJSON.length() > 0) { + Iterator iter = urlQueriesJSON.keys(); + while (iter.hasNext()) { + String key = iter.next(); + try { + Object value = urlQueriesJSON.opt(key); + hashMap.put(key, value); + } catch (Exception e) { + CSAppUtils.showLog(TAG, e.toString()); + } + } + + return hashMap; + } + + return null; + } + + private void throwException(@NonNull String tag, @Nullable String messageString, @Nullable Exception e) { + Error error = new Error(); + if (messageString != null) { + error.setErrorMessage(messageString); + Log.d(tag, messageString); + } + if (e != null) { + error.setErrorMessage(e.getLocalizedMessage()); + Log.d(tag, messageString); + } + } + + private ArrayMap getHeader(ArrayMap localHeader) { + ArrayMap mainHeader = stackHeader; + ArrayMap classHeaders = new ArrayMap<>(); + + if (localHeader != null && localHeader.size() > 0) { + if (mainHeader != null && mainHeader.size() > 0) { + for (Map.Entry entry : localHeader.entrySet()) { + String key = entry.getKey(); + classHeaders.put(key, entry.getValue()); + } + + for (Map.Entry entry : mainHeader.entrySet()) { + String key = entry.getKey(); + if (!classHeaders.containsKey(key)) { + classHeaders.put(key, entry.getValue()); + } + } + + return classHeaders; + + } else { + return localHeader; + } + + } else { + return stackHeader; + } + } + + @Override + public void getResult(Object object, String controller) { + } + + @Override + public void getResultObject(List objects, JSONObject jsonObject, boolean isSingleEntry) { + + if (jsonObject != null && jsonObject.has("count")) { + count = jsonObject.optInt("count"); + } + + List assets = new ArrayList(); + + if (objects != null && objects.size() > 0) { + for (Object object : objects) { + AssetModel model = (AssetModel) object; + Asset asset = stackInstance.asset(); + + asset.contentType = model.contentType; + asset.fileSize = model.fileSize; + asset.uploadUrl = model.uploadUrl; + asset.fileName = model.fileName; + asset.json = model.json; + asset.assetUid = model.uploadedUid; + asset.setTags(model.tags); + model = null; + + assets.add(asset); + } + } + + if (assetsCallback != null) { + assetsCallback.onRequestFinish(ResponseType.NETWORK, assets); + } + } + + + /** + * Retrieve the published content of the fallback locale if an entry is not localized in specified locale + * + * @return {@link AssetLibrary} object, so you can chain this call. + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken",  environment_name);
+     *     AssetLibrary assetLibObject = stack.assetLibrary();
+     *     assetLibObject.includeFallback();
+     * 
+ */ + public AssetLibrary includeFallback() { + try { + urlQueries.put("include_fallback", true); + } catch (JSONException e) { + Log.d("AssetLibrary", e.getLocalizedMessage()); + throwException("AssetLibrary", null, e); + } + return this; + } + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/AssetModel.java b/contentstack/src/main/java/com/contentstack/sdk/AssetModel.java new file mode 100755 index 00000000..bd069495 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/AssetModel.java @@ -0,0 +1,79 @@ +package com.contentstack.sdk; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.Iterator; +import java.util.WeakHashMap; + +/** + * Created by contentstack.com, Inc. + */ +class AssetModel { + + String uploadedUid; + String contentType; + String fileSize; + String fileName; + String uploadUrl; + String[] tags; + JSONObject json; + int totalCount = 0; + int count = 0; + + protected WeakHashMap _metadata = null; + + public AssetModel(JSONObject responseJSON, boolean isArray, boolean isFromCache) { + + if (isFromCache) { + json = responseJSON.opt("response") == null ? responseJSON : responseJSON.optJSONObject("response"); + } else { + json = responseJSON; + } + + if (isArray) { + json = responseJSON; + } else { + json = responseJSON.optJSONObject("asset"); + } + + uploadedUid = (String) json.opt("uid"); + contentType = (String) json.opt("content_type"); + fileSize = (String) json.opt("file_size"); + fileName = (String) json.opt("filename"); + uploadUrl = (String) json.opt("url"); + + if (json.opt("tags") instanceof JSONArray) { + if ((json.has("tags")) && (json.opt("tags") != null) && (!(json.opt("tags").equals("")))) { + + JSONArray tagsArray = (JSONArray) json.opt("tags"); + if (tagsArray.length() > 0) { + int count = tagsArray.length(); + tags = new String[count]; + for (int i = 0; i < count; i++) { + tags[i] = (String) tagsArray.opt(i); + } + } + } + } + + if (json != null && json.has("_metadata")) { + JSONObject _metadataJSON = json.optJSONObject("_metadata"); + Iterator iterator = _metadataJSON.keys(); + _metadata = new WeakHashMap<>(); + while (iterator.hasNext()) { + String key = iterator.next(); + _metadata.put(key, _metadataJSON.optString(key)); + } + } + + if (responseJSON.has("count")) { + count = responseJSON.optInt("count"); + } + + if (responseJSON.has("objects")) { + totalCount = responseJSON.optInt("objects"); + } + } + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/AssetsModel.java b/contentstack/src/main/java/com/contentstack/sdk/AssetsModel.java new file mode 100644 index 00000000..e96de11a --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/AssetsModel.java @@ -0,0 +1,32 @@ +package com.contentstack.sdk; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by contentstack.com, Inc. + */ + +class AssetsModel { + + List objects = new ArrayList(); + + public AssetsModel(JSONObject jsonObject, boolean isFromCache) { + + jsonObject = !isFromCache && jsonObject.opt("response") == null ? jsonObject : jsonObject.optJSONObject("response"); + + JSONArray jsonArray = jsonObject != null && jsonObject.has("assets") ? jsonObject.optJSONArray("assets") : null; + + if (jsonArray != null) { + for (int i = 0; i < jsonArray.length(); i++) { + + AssetModel model = new AssetModel(jsonArray.optJSONObject(i), true, false); + objects.add(model); + model = null; + } + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/CSBackgroundTask.java b/contentstack/src/main/java/com/contentstack/sdk/CSBackgroundTask.java new file mode 100755 index 00000000..14458615 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/CSBackgroundTask.java @@ -0,0 +1,149 @@ +package com.contentstack.sdk; + +import android.util.ArrayMap; + +import com.contentstack.sdk.utilities.CSAppConstants; + +import org.json.JSONObject; + +import java.util.HashMap; + +/** + * @author contentstack.com, Inc + */ +class CSBackgroundTask { + + public CSBackgroundTask(Query queryInstance, Stack stackInstance, String controller, String url, ArrayMap headers, HashMap urlQueries, JSONObject jsonMain, String cacheFilePath, String requestInfo, CSAppConstants.RequestMethod method, ResultCallBack callback) { + + if (CSAppConstants.isNetworkAvailable) { + if (headers != null && headers.size() > 0) { + + String URL = stackInstance.URLSCHEMA + stackInstance.URL + url; + + CSConnectionRequest csConnectionRequest = new CSConnectionRequest(queryInstance); + csConnectionRequest.setQueryInstance(queryInstance); + csConnectionRequest.setURLQueries(urlQueries); + csConnectionRequest.setParams(URL, method, controller, jsonMain, headers, cacheFilePath, requestInfo, callback); + + } else { + sendErrorForHeader(callback); + } + } else { + sendErrorToUser(callback); + } + } + + public CSBackgroundTask(Entry entryInstance, Stack stackInstance, String controller, String url, ArrayMap headers, HashMap urlQueries, JSONObject jsonMain, String cacheFilePath, String requestInfo, boolean isOffline, CSAppConstants.RequestMethod method, ResultCallBack callBack) { + if (CSAppConstants.isNetworkAvailable) { + if (headers != null && headers.size() > 0) { + + String URL = stackInstance.URLSCHEMA + stackInstance.URL + url; + + CSConnectionRequest csConnectionRequest = new CSConnectionRequest(entryInstance); + csConnectionRequest.setURLQueries(urlQueries); + csConnectionRequest.setParams(URL, method, controller, jsonMain, headers, cacheFilePath, requestInfo, callBack); + + } else { + sendErrorForHeader(callBack); + } + } else { + sendErrorToUser(callBack); + } + } + + public CSBackgroundTask(AssetLibrary assetLibrary, Stack stackInstance, String controller, String url, ArrayMap headers, HashMap urlQueries, JSONObject jsonMain, String cacheFilePath, String requestInfo, boolean isOffline, CSAppConstants.RequestMethod method, ResultCallBack callback) { + if (CSAppConstants.isNetworkAvailable) { + if (headers != null && headers.size() > 0) { + + String URL = stackInstance.URLSCHEMA + stackInstance.URL + url; + + CSConnectionRequest csConnectionRequest = new CSConnectionRequest(assetLibrary); + csConnectionRequest.setURLQueries(urlQueries); + csConnectionRequest.setParams(URL, method, controller, jsonMain, headers, cacheFilePath, requestInfo, callback); + + } else { + sendErrorForHeader(callback); + } + } else { + sendErrorToUser(callback); + } + } + + public CSBackgroundTask(Asset asset, Stack stackInstance, String controller, String url, ArrayMap headers, HashMap urlQueries, JSONObject jsonMain, String cacheFilePath, String requestInfo, boolean isOffline, CSAppConstants.RequestMethod method, ResultCallBack callback) { + if (CSAppConstants.isNetworkAvailable) { + if (headers != null && headers.size() > 0) { + + String URL = stackInstance.URLSCHEMA + stackInstance.URL + url; + + CSConnectionRequest csConnectionRequest = new CSConnectionRequest(asset); + csConnectionRequest.setURLQueries(urlQueries); + csConnectionRequest.setParams(URL, method, controller, jsonMain, headers, cacheFilePath, requestInfo, callback); + + } else { + sendErrorForHeader(callback); + } + } else { + sendErrorToUser(callback); + } + } + + + public CSBackgroundTask(Stack stack, Stack stackInstance, String controller, String url, ArrayMap headers, HashMap urlParams, JSONObject jsonMain, String cacheFilePath, String requestInfo, boolean b, CSAppConstants.RequestMethod method, ResultCallBack callback) { + + if (CSAppConstants.isNetworkAvailable) { + if (headers != null && headers.size() > 0) { + + String URL = stackInstance.URLSCHEMA + stackInstance.URL + url; + + CSConnectionRequest csConnectionRequest = new CSConnectionRequest(stack); + csConnectionRequest.setStackInstance(stack); + csConnectionRequest.setURLQueries(urlParams); + csConnectionRequest.setParams(URL, method, controller, jsonMain, headers, cacheFilePath, requestInfo, callback); + + } else { + sendErrorForHeader(callback); + } + } else { + sendErrorToUser(callback); + } + } + + + public CSBackgroundTask(ContentType contentType, Stack stackInstance, String controller, String url, ArrayMap headers, HashMap urlParams, JSONObject jsonMain, String cacheFilePath, String requestInfo, boolean b, CSAppConstants.RequestMethod method, ResultCallBack callback) { + + if (CSAppConstants.isNetworkAvailable) { + if (headers != null && headers.size() > 0) { + + String URL = stackInstance.URLSCHEMA + stackInstance.URL + url; + + CSConnectionRequest csConnectionRequest = new CSConnectionRequest(contentType); + csConnectionRequest.setContentTypeInstance(contentType); + csConnectionRequest.setURLQueries(urlParams); + csConnectionRequest.setParams(URL, method, controller, jsonMain, headers, cacheFilePath, requestInfo, callback); + + } else { + sendErrorForHeader(callback); + } + } else { + sendErrorToUser(callback); + } + } + + + private void sendErrorToUser(ResultCallBack callbackObject) { + Error error = new Error(); + error.setErrorCode(CSAppConstants.NONETWORKCONNECTION); + error.setErrorMessage(CSAppConstants.ErrorMessage_NoNetwork); + if (callbackObject != null) { + callbackObject.onRequestFail(ResponseType.UNKNOWN, error); + } + } + + private void sendErrorForHeader(ResultCallBack callbackObject) { + Error error = new Error(); + error.setErrorMessage(CSAppConstants.ErrorMessage_CalledDefaultMethod); + if (callbackObject != null) { + callbackObject.onRequestFail(ResponseType.UNKNOWN, error); + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/CSConnectionRequest.java b/contentstack/src/main/java/com/contentstack/sdk/CSConnectionRequest.java new file mode 100755 index 00000000..9a61476d --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/CSConnectionRequest.java @@ -0,0 +1,323 @@ +package com.contentstack.sdk; + + +import android.util.ArrayMap; + +import com.contentstack.sdk.utilities.CSAppConstants; +import com.contentstack.sdk.utilities.CSAppUtils; +import com.contentstack.sdk.utilities.CSController; + +import org.json.JSONObject; + +import java.io.File; +import java.io.FileWriter; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +/** + * @author contentstack.com, Inc + */ +class CSConnectionRequest implements IRequestModelHTTP { + + private static final String TAG = "CSConnectionRequest"; + + private String urlToCall; + private CSAppConstants.RequestMethod method; + private String controller; + private JSONObject paramsJSON; + private ArrayMap header; + private HashMap urlQueries; + private String cacheFileName; + private String requestInfo; + private ResultCallBack callBackObject; + private CSHttpConnection connection; + private JSONObject responseJSON; + private INotifyClass notifyClass; + private INotifyClass assetLibrary; + + private Stack stackInstance; + private Entry entryInstance; + private Query queryInstance; + private Asset assetInstance; + private ContentType contentTypeInstance; + private JSONObject errorJObject; + private Error errorObject = new Error(); + + public CSConnectionRequest() { + } + + public CSConnectionRequest(Query queryInstance) { + notifyClass = queryInstance; + } + + public CSConnectionRequest(Entry entryInstance) { + this.entryInstance = entryInstance; + } + + public CSConnectionRequest(INotifyClass assetLibrary) { + this.assetLibrary = assetLibrary; + } + + public CSConnectionRequest(Asset asset) { + this.assetInstance = asset; + } + + public CSConnectionRequest(ContentType contentType) { + this.contentTypeInstance = contentType; + } + + public void setQueryInstance(Query queryInstance) { + this.queryInstance = queryInstance; + } + + public void setURLQueries(HashMap urlQueries) { + this.urlQueries = urlQueries; + } + + public void setStackInstance(Stack stackInstance) { + this.stackInstance = stackInstance; + } + + public void setContentTypeInstance(ContentType contentTypeInstance) { + this.contentTypeInstance = contentTypeInstance; + } + + public void setParams(Object... objects) { + CSAppUtils.showLog(TAG, "ParallelTasks------|" + objects[0] + " started"); + + this.urlToCall = (String) objects[0]; + this.method = (CSAppConstants.RequestMethod) objects[1]; + this.controller = (String) objects[2]; + paramsJSON = (JSONObject) objects[3]; + this.header = (ArrayMap) objects[4]; + + if (objects[5] != null) { + cacheFileName = (String) objects[5]; + } + + if (objects[6] != null) { + requestInfo = (String) objects[6]; + } + + if (objects[7] != null) { + + callBackObject = (ResultCallBack) objects[7]; + } + + sendRequest(); + } + + @Override + public void sendRequest() { + connection = new CSHttpConnection(urlToCall, this); + connection.setController(controller); + connection.setHeaders(header); + connection.setInfo(requestInfo); + connection.setFormParamsPOST(paramsJSON); + connection.setCallBackObject(callBackObject); + + if (urlQueries != null && urlQueries.size() > 0) { + connection.setFormParams(urlQueries); + } + + connection.setRequestMethod(method); + connection.send(); + + } + + @Override + public void onRequestFailed(JSONObject error, int statusCode, ResultCallBack callBackObject) { + + String errorMessage = null; + int errorCode = statusCode; + HashMap resultHashMap = null; + + try { + errorJObject = error; + + if (errorJObject != null) { + errorMessage = (errorJObject).isNull("error_message") ? "" : (errorJObject).optString("error_message"); + + if ((!errorJObject.isNull("error_code")) && (!errorJObject.optString("error_code").contains(" "))) { + errorCode = (Integer) errorJObject.opt("error_code"); + } + + if (!errorJObject.isNull("errors")) { + resultHashMap = new HashMap(); + if (errorJObject.opt("errors") instanceof JSONObject) { + JSONObject errorsJsonObj = errorJObject.optJSONObject("errors"); + Iterator iterator = errorsJsonObj.keys(); + while (iterator.hasNext()) { + String key = iterator.next(); + Object value = errorsJsonObj.opt(key); + resultHashMap.put(key, value); + } + } else { + resultHashMap.put("errors", errorJObject.get("errors")); + } + } + } + + } catch (Exception e) { + CSAppUtils.showLog(TAG, "------------------catch 210 urlReq---|" + e); + errorMessage = e.getLocalizedMessage(); + } + if (errorMessage == null || (!(errorMessage.length() > 0))) { + errorMessage = CSAppConstants.ErrorMessage_Default; + } + errorObject.setErrorCode(errorCode); + errorObject.setErrorMessage(errorMessage); + errorObject.setErrors(resultHashMap); + + if (this.callBackObject != null) { + this.callBackObject.onRequestFail(ResponseType.NETWORK, errorObject); + } + + } + + @Override + public void onRequestFinished(CSHttpConnection request) { + + responseJSON = request.getResponse(); + + String controller = request.getController(); + if (cacheFileName != null) { + createFileIntoCacheDir(responseJSON); + } + + if (controller.equalsIgnoreCase(CSController.QUERYOBJECT)) { + + EntriesModel model = new EntriesModel(responseJSON, null, false); + notifyClass.getResult(model.formName, null); + notifyClass.getResultObject(model.objectList, responseJSON, false); + model = null; + + } else if (controller.equalsIgnoreCase(CSController.SINGLEQUERYOBJECT)) { + + EntriesModel model = new EntriesModel(responseJSON, null, false); + notifyClass.getResult(model.formName, null); + notifyClass.getResultObject(model.objectList, responseJSON, true); + model = null; + + } else if (controller.equalsIgnoreCase(CSController.FETCHENTRY)) { + + EntryModel model = new EntryModel(responseJSON, null, false, false, false); + entryInstance.resultJson = model.jsonObject; + entryInstance.ownerEmailId = model.ownerEmailId; + entryInstance.ownerUid = model.ownerUid; + entryInstance.title = model.title; + entryInstance.url = model.url; + entryInstance.language = model.language; + if (model.ownerMap != null) { + entryInstance.owner = new HashMap<>(model.ownerMap); + } + if (model._metadata != null) { + entryInstance._metadata = new HashMap<>(model._metadata); + } + entryInstance.uid = model.entryUid; + entryInstance.setTags(model.tags); + model = null; + + if (request.getCallBackObject() != null) { + ((EntryResultCallBack) request.getCallBackObject()).onRequestFinish(ResponseType.NETWORK); + } + + } else if (controller.equalsIgnoreCase(CSController.FETCHALLASSETS)) { + AssetsModel assetsModel = new AssetsModel(responseJSON, false); + List objectList = assetsModel.objects; + assetsModel = null; + + assetLibrary.getResultObject(objectList, responseJSON, false); + + } else if (controller.equalsIgnoreCase(CSController.FETCHASSETS)) { + AssetModel model = new AssetModel(responseJSON, false, false); + + assetInstance.contentType = model.contentType; + assetInstance.fileSize = model.fileSize; + assetInstance.uploadUrl = model.uploadUrl; + assetInstance.fileName = model.fileName; + assetInstance.json = model.json; + assetInstance.assetUid = model.uploadedUid; + assetInstance.setTags(model.tags); + + model = null; + if (request.getCallBackObject() != null) { + ((FetchResultCallback) request.getCallBackObject()).onRequestFinish(ResponseType.NETWORK); + } + } else if (controller.equalsIgnoreCase(CSController.FETCHSYNC)) { + + SyncStack model = new SyncStack(); + model.setJSON(responseJSON); + if (request.getCallBackObject() != null) { + ((SyncResultCallBack) request.getCallBackObject()).onRequestFinish(model); + } + + } else if (controller.equalsIgnoreCase(CSController.FETCHCONTENTTYPES)) { + + ContentTypesModel model = new ContentTypesModel(); + model.setJSON(responseJSON); + if (request.getCallBackObject() != null) { + ((ContentTypesCallback) request.getCallBackObject()).onRequestFinish(model); + } + + } + + } + + protected void createFileIntoCacheDir(Object jsonObject) { + try { + JSONObject jsonObj = new JSONObject(); + JSONObject mainJsonObj = new JSONObject(); + JSONObject headerJson = new JSONObject(); + + + jsonObj = paramsJSON; + + Calendar cal = Calendar.getInstance(); + cal.setTimeZone(TimeZone.getTimeZone("UTC")); + cal.setTime(new Date()); + long gmtTime = cal.getTimeInMillis(); + + mainJsonObj.put("url", urlToCall.toString().trim()); + mainJsonObj.put("timestamp", gmtTime); + mainJsonObj.put("params", jsonObj); + mainJsonObj.put("response", jsonObject); + if (requestInfo != null) { + mainJsonObj.put("classUID", requestInfo); + } + + for (Map.Entry entry : header.entrySet()) { + String key = entry.getKey(); + headerJson.put(key, entry.getValue()); + } + mainJsonObj.put("header", headerJson); + + File cacheFile = new File(cacheFileName); + + if (cacheFile.exists()) { + cacheFile.delete(); + } + // deepcode ignore DontCloseInTry: + FileWriter file = new FileWriter(cacheFile); + file.write(mainJsonObj.toString()); + file.flush(); + file.close(); + } catch (Exception e) { + Error error = new Error(); + error.setErrorMessage(CSAppConstants.ErrorMessage_SavingNetworkCallResponseForCache); + HashMap hashMap = new HashMap(); + hashMap.put("error", e.getLocalizedMessage()); + error.setErrors(hashMap); + if (callBackObject != null) { + callBackObject.onRequestFail(ResponseType.CACHE, error); + } + CSAppUtils.showLog(TAG, e.getLocalizedMessage()); + } + } + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/CSHttpConnection.java b/contentstack/src/main/java/com/contentstack/sdk/CSHttpConnection.java new file mode 100755 index 00000000..c9d07818 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/CSHttpConnection.java @@ -0,0 +1,396 @@ +package com.contentstack.sdk; + +import android.util.ArrayMap; +import android.text.TextUtils; + +import com.android.volley.AuthFailureError; +import com.android.volley.DefaultRetryPolicy; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonObjectRequest; +import com.contentstack.sdk.utilities.CSAppConstants; +import com.contentstack.sdk.utilities.CSAppUtils; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * A class that defines a query that is used to query for {@link Entry} instance. + * + * @author contentstack.com, Inc + */ +class CSHttpConnection implements IURLRequestHTTP { + + private static final String TAG = "CSHttpConnection"; + private String urlPath; + private String controller; + private ArrayMap headers; + private String info; + private JSONObject requestJSON; + private IRequestModelHTTP connectionRequest; + private ResultCallBack callBackObject; + private CSAppConstants.RequestMethod requestMethod; + private JSONObject responseJSON; + + public HashMap getFormParams() { + return formParams; + } + + public void setFormParams(HashMap formParams) { + this.formParams = formParams; + } + + private HashMap formParams; + private JSONUTF8Request jsonObjectRequest; + private boolean treatDuplicateKeysAsArrayItems; + + public CSHttpConnection(String urlToCall, IRequestModelHTTP csConnectionRequest) { + this.urlPath = urlToCall; + this.connectionRequest = csConnectionRequest; + } + + @Override + public void setController(String controller) { + this.controller = controller; + } + + @Override + public String getController() { + return controller; + } + + @Override + public void setHeaders(ArrayMap headers) { + this.headers = headers; + } + + @Override + public ArrayMap getHeaders() { + return headers; + } + + @Override + public void setInfo(String info) { + this.info = info; + } + + @Override + public String getInfo() { + return info; + } + + public void setFormParamsPOST(JSONObject requestJSON) { + this.requestJSON = null; + this.requestJSON = requestJSON; + } + + @Override + public void setCallBackObject(ResultCallBack callBackObject) { + this.callBackObject = callBackObject; + } + + @Override + public ResultCallBack getCallBackObject() { + return callBackObject; + } + + @Override + public void setTreatDuplicateKeysAsArrayItems(boolean treatDuplicateKeysAsArrayItems) { + this.treatDuplicateKeysAsArrayItems = treatDuplicateKeysAsArrayItems; + } + + @Override + public boolean getTreatDuplicateKeysAsArrayItems() { + return treatDuplicateKeysAsArrayItems; + } + + @Override + public void setRequestMethod(CSAppConstants.RequestMethod requestMethod) { + this.requestMethod = requestMethod; + } + + @Override + public CSAppConstants.RequestMethod getRequestMethod() { + return requestMethod; + } + + @Override + public JSONObject getResponse() { + return responseJSON; + } + + + public String setFormParamsGET(HashMap params) { + if (params != null && params.size() > 0) { + String urlParams = null; + + urlParams = info.equalsIgnoreCase(CSAppConstants.callController.QUERY.name()) || info.equalsIgnoreCase(CSAppConstants.callController.ENTRY.name()) ? getParams(params) : null; + if (TextUtils.isEmpty(urlParams)) { + for (Map.Entry e : params.entrySet()) { + + if (urlParams == null) { + urlParams = "?" + e.getKey() + "=" + e.getValue(); + } else { + urlParams += "&" + e.getKey() + "=" + e.getValue(); + } + } + } + return urlParams; + } + return null; + } + + + private String getParams(HashMap params) { + String urlParams = "?"; + for (Map.Entry e : params.entrySet()) { + + String key = e.getKey(); + Object value = e.getValue(); + + try { + + if (key.equalsIgnoreCase("include[]")) { + key = URLEncoder.encode(key, "UTF-8"); + JSONArray array = (JSONArray) value; + + for (int i = 0; i < array.length(); i++) { + urlParams += urlParams.equals("?") ? key + "=" + array.opt(i) : "&" + key + "=" + array.opt(i); + } + + } else if (key.equalsIgnoreCase("only[BASE][]")) { + key = URLEncoder.encode(key, "UTF-8"); + JSONArray array = (JSONArray) value; + + for (int i = 0; i < array.length(); i++) { + urlParams += urlParams.equals("?") ? key + "=" + array.opt(i) : "&" + key + "=" + array.opt(i); + } + } else if (key.equalsIgnoreCase("except[BASE][]")) { + key = URLEncoder.encode(key, "UTF-8"); + JSONArray array = (JSONArray) value; + + for (int i = 0; i < array.length(); i++) { + urlParams += urlParams.equals("?") ? key + "=" + array.opt(i) : "&" + key + "=" + array.opt(i); + } + } else if (key.equalsIgnoreCase("only")) { + JSONObject onlyJSON = (JSONObject) value; + + Iterator iter = onlyJSON.keys(); + while (iter.hasNext()) { + String innerKey = iter.next(); + JSONArray array = (JSONArray) onlyJSON.optJSONArray(innerKey); + innerKey = URLEncoder.encode("only[" + innerKey + "][]", "UTF-8"); + for (int i = 0; i < array.length(); i++) { + urlParams += urlParams.equals("?") ? innerKey + "=" + array.opt(i) : "&" + innerKey + "=" + array.opt(i); + } + } + + } else if (key.equalsIgnoreCase("except")) { + JSONObject onlyJSON = (JSONObject) value; + + Iterator iter = onlyJSON.keys(); + while (iter.hasNext()) { + String innerKey = iter.next(); + JSONArray array = (JSONArray) onlyJSON.optJSONArray(innerKey); + innerKey = URLEncoder.encode("except[" + innerKey + "][]", "UTF-8"); + for (int i = 0; i < array.length(); i++) { + urlParams += urlParams.equals("?") ? innerKey + "=" + array.opt(i) : "&" + innerKey + "=" + array.opt(i); + } + } + + } else if (key.equalsIgnoreCase("query")) { + JSONObject queryJSON = (JSONObject) value; + + urlParams += urlParams.equals("?") ? key + "=" + URLEncoder.encode(queryJSON.toString(), "UTF-8") : "&" + key + "=" + URLEncoder.encode(queryJSON.toString(), "UTF-8"); + + } else { + urlParams += urlParams.equals("?") ? key + "=" + value : "&" + key + "=" + value; + } + + } catch (Exception e1) { + CSAppUtils.showLog(TAG, "--------------------getQueryParam--||" + e1.toString()); + } + } + + return urlParams; + } + + + @Override + public void send() { + String url = null; + String protocol = CSAppConstants.URLSCHEMA_HTTPS; + int requestId = getRequestId(requestMethod); + final HashMap headers = new HashMap<>(); + int count = this.headers.size(); + + if (requestMethod == CSAppConstants.RequestMethod.GET) { + String params = setFormParamsGET(formParams); + if (params != null) { + url = urlPath + params; + } else { + url = urlPath; + } + } else { + url = urlPath; + } + + + for (Map.Entry entry : this.headers.entrySet()) { + String key = entry.getKey(); + headers.put(key, (String) entry.getValue()); + } + + headers.put("Content-Type", "application/json"); + headers.put("User-Agent", defaultUserAgent()); + headers.put("X-User-Agent", "contentstack-android/" + CSAppConstants.SDK_VERSION); + + + jsonObjectRequest = new JSONUTF8Request(requestId, url, requestJSON, new Response.Listener() { + + @Override + public void onResponse(JSONObject response) { + responseJSON = response; + if (responseJSON != null) { + connectionRequest.onRequestFinished(CSHttpConnection.this); + } + } + }, new Response.ErrorListener() { + + @Override + public void onErrorResponse(VolleyError error) { + generateBuiltError(error); + } + }) { + + @Override + public Map getHeaders() throws AuthFailureError { + return headers; + } + + }; + + jsonObjectRequest.setRetryPolicy(new DefaultRetryPolicy(CSAppConstants.TimeOutDuration, CSAppConstants.NumRetry, CSAppConstants.BackOFMultiplier)); + jsonObjectRequest.setShouldCache(false); + Contentstack.addToRequestQueue(protocol, jsonObjectRequest, info); + + } + + + private void httpRequest(String url) { + + JsonObjectRequest jsonObjectRequest = new JsonObjectRequest + (Request.Method.GET, url, requestJSON, new Response.Listener() { + + @Override + public void onResponse(JSONObject response) { + responseJSON = response; + if (responseJSON != null) { + connectionRequest.onRequestFinished(CSHttpConnection.this); + } + } + }, new Response.ErrorListener() { + + @Override + public void onErrorResponse(VolleyError error) { + generateBuiltError(error); + + } + }); + + jsonObjectRequest.setRetryPolicy(new DefaultRetryPolicy(CSAppConstants.TimeOutDuration, CSAppConstants.NumRetry, CSAppConstants.BackOFMultiplier)); + jsonObjectRequest.setShouldCache(false); + Contentstack.addToRequestQueue("https://", jsonObjectRequest, info); + + } + + private String defaultUserAgent() { + String agent = System.getProperty("http.agent"); + return agent != null ? agent : ("Android" + System.getProperty("java.version")); + } + + private int getRequestId(CSAppConstants.RequestMethod requestMethod) { + switch (requestMethod) { + case GET: + return 0; + case POST: + return 1; + case PUT: + return 2; + case DELETE: + return 3; + default: + return 1; + } + } + + private void generateBuiltError(VolleyError error) { + try { + int statusCode = 0; + responseJSON = new JSONObject(); + responseJSON.put("error_message", CSAppConstants.ErrorMessage_Default); + + if (error != null) { + + try { + if (error.networkResponse != null && error.networkResponse.data != null) { + statusCode = error.networkResponse.statusCode; + String responseBody = new String(error.networkResponse.data, "utf-8"); + responseJSON = responseBody != null ? new JSONObject(responseBody) : new JSONObject(); + + } else { + if (error.toString().equalsIgnoreCase("NoConnectionError")) { + + responseJSON.put("error_message", CSAppConstants.ErrorMessage_VolleyNoConnectionError); + + } else if (error.toString().equalsIgnoreCase("AuthFailureError")) { + + responseJSON.put("error_message", CSAppConstants.ErrorMessage_VolleyAuthFailureError); + + } else if (error.toString().equalsIgnoreCase("NetworkError")) { + + responseJSON.put("error_message", CSAppConstants.ErrorMessage_NoNetwork); + + } else if (error.toString().equalsIgnoreCase("ParseError")) { + + responseJSON.put("error_message", CSAppConstants.ErrorMessage_VolleyParseError); + + } else if (error.toString().equalsIgnoreCase("ServerError")) { + + responseJSON.put("error_message", CSAppConstants.ErrorMessage_VolleyServerError); + + } else if (error.toString().equalsIgnoreCase("TimeoutError")) { + + responseJSON.put("error_message", CSAppConstants.ErrorMessage_VolleyServerError); + + } else { + if (error.getMessage() != null) { + responseJSON.put("error_message", error.getMessage()); + } + } + + JSONObject jsonObject = new JSONObject(); + jsonObject.put("errors", error.toString()); + responseJSON.put("errors", jsonObject); + + } + connectionRequest.onRequestFailed(responseJSON, statusCode, callBackObject); + + } catch (Exception e) { + connectionRequest.onRequestFailed(responseJSON, 0, callBackObject); + } + } else { + connectionRequest.onRequestFailed(responseJSON, 0, callBackObject); + } + } catch (Exception exception) { + CSAppUtils.showLog(TAG, exception.toString()); + } + } + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/CachePolicy.java b/contentstack/src/main/java/com/contentstack/sdk/CachePolicy.java new file mode 100755 index 00000000..27fb5e0e --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/CachePolicy.java @@ -0,0 +1,35 @@ +package com.contentstack.sdk; + +public enum CachePolicy { + + /** + * To fetch data from cache. + */ + CACHE_ONLY, + + /** + * To fetch data from network and response will be saved in cache. + */ + NETWORK_ONLY, + + /** + * To fetch data from cache if data not available in cache then it will send a network call and response will be saved in cache. + */ + CACHE_ELSE_NETWORK, + + /** + * To fetch data from network and response will be saved in cache ; if network not available then it will fetch data from cache. + */ + NETWORK_ELSE_CACHE, + + /** + * To fetch data from cache and send a network call and response will be saved in cache. + */ + CACHE_THEN_NETWORK, + + /** + * To fetch data from network call and response will not be saved cache. + */ + IGNORE_CACHE; + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/ClearCache.java b/contentstack/src/main/java/com/contentstack/sdk/ClearCache.java new file mode 100755 index 00000000..bdb48d5b --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/ClearCache.java @@ -0,0 +1,69 @@ +package com.contentstack.sdk; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import com.contentstack.sdk.utilities.CSAppUtils; + +import org.json.JSONObject; + +import java.io.File; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; + +/** + * Cache clear class. + * + * @author contentstack.com, Inc + */ +public class ClearCache extends BroadcastReceiver { + + public ClearCache() { + } + + @Override + public void onReceive(Context context, Intent intent) { + + File cacheResponseFolder = new File(context.getDir("ContentstackCache", 0).getPath()); + + if (cacheResponseFolder.isDirectory()) { + File[] childFiles = cacheResponseFolder.listFiles(); + + for (File child : childFiles) { + File file = new File(cacheResponseFolder, child.getName()); + + File sessionFile = new File(cacheResponseFolder.getPath() + File.separator + "Session"); + File installationFile = new File(cacheResponseFolder.getPath() + File.separator + "Installation"); + + if ((file.getName().equalsIgnoreCase(sessionFile.getName())) || (file.getName().equalsIgnoreCase(installationFile.getName()))) { + + } else { + + if (file.exists()) { + JSONObject jsonObj = CSAppUtils.getJsonFromCacheFile(file); + if (jsonObj != null) { + if (jsonObj.optString("timestamp") != null) { + long responseTime = Long.parseLong(jsonObj.optString("timestamp")); + Date responseDate = new Date(responseTime); + Calendar cal = Calendar.getInstance(); + cal.setTimeZone(TimeZone.getTimeZone("UTC")); + cal.setTime(new Date()); + Date currentDate = new Date(cal.getTimeInMillis()); + long hourBetween = TimeUnit.MILLISECONDS.toHours(currentDate.getTime() - responseDate.getTime()); + if (hourBetween >= 24) { + file.delete(); + } + } + } + } else { + CSAppUtils.showLog("ClearCache", "--------------------no offline network calls"); + } + } + } + } + } + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/Config.java b/contentstack/src/main/java/com/contentstack/sdk/Config.java new file mode 100755 index 00000000..2dd8cdfc --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/Config.java @@ -0,0 +1,147 @@ +package com.contentstack.sdk; + +import android.text.TextUtils; + +import com.contentstack.sdk.utilities.CSAppConstants; + +/** + * Set Configuration for stack instance creation. + * + * @author contentstack.com, Inc + */ +public class Config { + + protected String URLSCHEMA = "https://"; + protected String URL = "cdn.contentstack.io"; + protected String VERSION = "v3"; + protected String environment = null; + protected ContentstackRegion region = ContentstackRegion.US; + + public enum ContentstackRegion {US, EU} + + + public ContentstackRegion getRegion() { + return this.region; + } + + /** + * Sets region allow you to set your region for the Contentstack server. + * + * @param region

+ * Note: Default region sets to us + * + *

Example :
+ *

+     *               config.setRegion(ContentstackRegion.US);
+     *               
+ */ + + public ContentstackRegion setRegion(ContentstackRegion region) { + this.region = region; + return this.region; + } + + /** + * Config constructor + * + *

Example :
+ *
+     * Config config = new Config();
+     * 
+ */ + + public Config() { + } + + /** + * Sets host name of the Contentstack server. + * + * @param hostName host name. + * + *

+ * Note: Default hostname sets to cdn.contentstack.io + * and default protocol is HTTPS. + *

Example :
+ *

+     *                 config.setHost("cdn.contentstack.io");
+     *                 
+ */ + + public void setHost(String hostName) { + if (!TextUtils.isEmpty(hostName)) { + URL = hostName; + } + } + + + /** + * Get URL. + * + * + *

Example :
+ *
+     * String url = config.getHost();
+     * 
+ */ + public String getHost() { + return URL; + } + + + /** + * Get version of the Contentstack server. + * + *

Example :
+ *
+     * String version = config.getVersion();
+     * 
+ */ + public String getVersion() { + return VERSION; + } + + /** + * Changes the Contentstack version to be used in the final URL. + * + * @param version version string. + * + *

Example :
+ *
+     *                     config.setVersion("v3");
+     *                
+ */ + private void setVersion(String version) { + if (!TextUtils.isEmpty(version)) { + VERSION = version; + } + } + + /** + * set environment. + * + * @param environment environment uid/name + * + *

Example :
+ *
+     *                     config.setEnvironment("stag", false);
+     *                    
+ */ + protected void setEnvironment(String environment) { + if (!TextUtils.isEmpty(environment)) { + this.environment = environment; + } + } + + /** + * Get environment. + *

Example :
+ *
+     *  String environment = config.getEnvironment();
+     * 
+ */ + public String getEnvironment() { + return environment; + } + + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/ConnectionStatus.java b/contentstack/src/main/java/com/contentstack/sdk/ConnectionStatus.java new file mode 100755 index 00000000..8b9aa1ed --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/ConnectionStatus.java @@ -0,0 +1,85 @@ +package com.contentstack.sdk; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import com.contentstack.sdk.utilities.CSAppConstants; +import com.contentstack.sdk.utilities.CSAppUtils; + +import org.json.JSONObject; + +import java.io.File; +import java.util.HashMap; + +/** + * Connection Status. + * + * @author contentstack.com, Inc + */ +public class ConnectionStatus extends BroadcastReceiver { + + public ConnectionStatus() { + + } + + @Override + public void onReceive(Context context, Intent intent) { + + Contentstack.isNetworkAvailable(context); + + if (!CSAppConstants.isNetworkAvailable) { + //no net connection + CSAppConstants.isNetworkAvailable = false; + + } else { + try { + JSONObject jsonObj = null; + JSONObject headerObject = null; + HashMap headerGroup = new HashMap(); + + CSAppConstants.isNetworkAvailable = true; + + File offlineCallsFolder = new File(context.getDir("OfflineCalls", 0).getPath()); + + if (offlineCallsFolder.isDirectory()) { + File[] childFiles = offlineCallsFolder.listFiles(); + for (File child : childFiles) { + File file = new File(offlineCallsFolder, child.getName()); + if (file.exists()) { + jsonObj = CSAppUtils.getJsonFromCacheFile(file); + if (jsonObj != null) { + headerObject = jsonObj.optJSONObject("headers"); + int count = headerObject.names().length(); + for (int i = 0; i < count; i++) { + String key = headerObject.names().getString(i); + headerGroup.put(key, headerObject.optString(key)); + } + CSConnectionRequest connectionRequest = new CSConnectionRequest(); + connectionRequest.setParams( + jsonObj.opt("url").toString(), + CSAppConstants.RequestMethod.POST, + jsonObj.opt("controller").toString(), + jsonObj.optJSONObject("params"), + headerGroup, + jsonObj.opt("cacheFileName"), + jsonObj.opt("requestInfo"), + null + ); + } + child.delete(); + } else { + CSAppUtils.showLog("ConnectionStatus", "--------------------no offline network calls"); + } + } + } + } catch (Exception e) { + CSAppUtils.showLog("ConnectionStatus", "-----content stack----------send saved network calls-------catch|" + e); + } + } + + CSAppUtils.showLog("ConnectionStatus", "---------------------BuiltAppConstants.isNetworkAvailable|" + CSAppConstants.isNetworkAvailable); + + } + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/ContentType.java b/contentstack/src/main/java/com/contentstack/sdk/ContentType.java new file mode 100755 index 00000000..7d3a6f96 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/ContentType.java @@ -0,0 +1,266 @@ +package com.contentstack.sdk; + +import android.util.ArrayMap; +import android.text.TextUtils; + +import com.contentstack.sdk.utilities.CSAppConstants; +import com.contentstack.sdk.utilities.CSAppUtils; +import com.contentstack.sdk.utilities.CSController; + +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * BuiltClass provides {@link Entry} and {@link Query} instance.
+ * + * @author contentstack + */ +public class ContentType { + + protected String TAG = ContentType.class.getSimpleName(); + protected String contentTypeName = null; + protected Stack stackInstance = null; + private ArrayMap localHeader = null; + private ArrayMap stackHeader = null; + + private ContentType() { + } + + protected ContentType(String contentTypeName) { + this.contentTypeName = contentTypeName; + this.localHeader = new ArrayMap(); + } + + protected void setStackInstance(Stack stack) { + this.stackInstance = stack; + this.stackHeader = stack.localHeader; + } + + + /** + * To set headers for Contentstack rest calls. + *
+ * Scope is limited to this object and followed classes. + * + * @param key header name. + * @param value header value against given header name. + * + *

Example :
+ *
+     *               Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "environment");
+     *               ContentType contentType = stack.contentType("form_name");
+ * contentType.setHeader("custom_key", "custom_value"); + *
+ */ + public void setHeader(String key, String value) { + if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(value)) { + localHeader.put(key, value); + } + } + + /** + * Remove header key. + * + * @param key custom_header_key + * + *

Example :
+ *
+     *             Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "environment");
+     *             ContentType contentType = stack.contentType("form_name");
+ * contentType.removeHeader("custom_header_key"); + *
+ */ + public void removeHeader(String key) { + if (!TextUtils.isEmpty(key)) { + localHeader.remove(key); + } + } + + protected Entry entry() { + Entry entry = new Entry(contentTypeName); + entry.formHeader = getHeader(localHeader); + entry.setContentTypeInstance(this); + + return entry; + } + + /** + * Represents a {@link Entry}. + * Create {@link Entry} instance. + * + * @param entryUid Set entry uid. + * @return {@link Entry} instance. + * + *

Example :
+ *
+     *  SStack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "environment");
+     *  ContentType contentType = stack.contentType("form_name");
+ * ENTRY entry = contentType.entry("entryUid"); + *
+ */ + public Entry entry(String entryUid) { + Entry entry = new Entry(contentTypeName); + entry.formHeader = getHeader(localHeader); + entry.setContentTypeInstance(this); + entry.setUid(entryUid); + + return entry; + } + + /** + * Represents a {@link Query}. + * Create {@link Query} instance. + * + * @return {@link Query} instance. + * + *

Example :
+ *
+     *  Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "environment");
+     *  ContentType contentType = stack.contentType("form_name");
+ * Query csQuery = contentType.query(); + *
+ */ + public Query query() { + Query query = new Query(contentTypeName); + query.formHeader = getHeader(localHeader); + query.setContentTypeInstance(this); + + return query; + } + + + /** + * @param params query parameters + * @param callback ContentTypesCallback + * This call returns information of a specific content type. It returns the content type schema, but does not include its entries. + * + *

Example :
+ *
+     *                 ContentType  contentType = stack.contentType("content_type_uid");
+     *                 JSONObject params = new JSONObject();
+     *                 params.put("include_snippet_schema", true);
+     *                 params.put("limit", 3);
+     *                 contentType.fetch(new ContentTypesCallback() {
+     *                 @Override
+     *                 public void onCompletion(ContentTypesModel contentTypesModel, Error error) {
+     *                 if (error==null){
+     *
+     *                 }else {
+     *
+     *                 }
+     *                 }
+     *                 });
+     *                 
+ */ + + + public void fetch(JSONObject params, final ContentTypesCallback callback) { + + try { + + String URL = "/" + stackInstance.VERSION + "/content_types/" + contentTypeName; + ArrayMap headers = getHeader(localHeader); + if (params == null) { + params = new JSONObject(); + } + + Iterator keys = params.keys(); + while (keys.hasNext()) { + // loop to get the dynamic key + String key = (String) keys.next(); + // get the value of the dynamic key + Object value = params.opt(key); + // do something here with the value... + params.put(key, value); + } + + if (headers.containsKey("environment")) { + params.put("environment", headers.get("environment")); + } + + if (contentTypeName != null && !contentTypeName.isEmpty()) { + fetchContentTypes(URL, params, headers, null, callback); + } else { + Error error = new Error(); + error.setErrorMessage(CSAppConstants.ErrorMessage_JsonNotProper); + callback.onRequestFail(ResponseType.UNKNOWN, error); + } + + + } catch (Exception e) { + + Error error = new Error(); + error.setErrorMessage(CSAppConstants.ErrorMessage_JsonNotProper); + callback.onRequestFail(ResponseType.UNKNOWN, error); + } + + } + + + private void fetchContentTypes(String urlString, JSONObject urlQueries, ArrayMap headers, String cacheFilePath, ContentTypesCallback callback) { + + if (callback != null) { + + HashMap urlParams = getUrlParams(urlQueries); + new CSBackgroundTask(this, stackInstance, CSController.FETCHCONTENTTYPES, urlString, headers, urlParams, new JSONObject(), cacheFilePath, CSAppConstants.callController.CONTENTTYPES.toString(), false, CSAppConstants.RequestMethod.GET, callback); + } + } + + + private HashMap getUrlParams(JSONObject urlQueriesJSON) { + + HashMap hashMap = new HashMap<>(); + + if (urlQueriesJSON != null && urlQueriesJSON.length() > 0) { + Iterator iter = urlQueriesJSON.keys(); + while (iter.hasNext()) { + String key = iter.next(); + try { + Object value = urlQueriesJSON.opt(key); + hashMap.put(key, value); + } catch (Exception e) { + CSAppUtils.showLog(TAG, "------setQueryJson" + e.toString()); + } + } + + return hashMap; + } + + return null; + } + + + private ArrayMap getHeader(ArrayMap localHeader) { + ArrayMap mainHeader = stackHeader; + ArrayMap classHeaders = new ArrayMap<>(); + + if (localHeader != null && localHeader.size() > 0) { + if (mainHeader != null && mainHeader.size() > 0) { + for (Map.Entry entry : localHeader.entrySet()) { + String key = entry.getKey(); + classHeaders.put(key, entry.getValue()); + } + + for (Map.Entry entry : mainHeader.entrySet()) { + String key = entry.getKey(); + if (!classHeaders.containsKey(key)) { + classHeaders.put(key, entry.getValue()); + } + } + + return classHeaders; + + } else { + return localHeader; + } + + } else { + return stackHeader; + } + } + + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/ContentTypesCallback.java b/contentstack/src/main/java/com/contentstack/sdk/ContentTypesCallback.java new file mode 100755 index 00000000..9cff368e --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/ContentTypesCallback.java @@ -0,0 +1,24 @@ +package com.contentstack.sdk; + +/** + * @author Contentstack.com, Inc callback. + */ +public abstract class ContentTypesCallback extends ResultCallBack { + + public abstract void onCompletion(ContentTypesModel contentTypesModel, Error error); + + void onRequestFinish(ContentTypesModel contentTypesModel) { + onCompletion(contentTypesModel, null); + } + + @Override + void onRequestFail(ResponseType responseType, Error error) { + onCompletion(null, error); + } + + @Override + public void always() { + + } + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/ContentTypesModel.java b/contentstack/src/main/java/com/contentstack/sdk/ContentTypesModel.java new file mode 100644 index 00000000..dceed4af --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/ContentTypesModel.java @@ -0,0 +1,45 @@ +package com.contentstack.sdk; + +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class ContentTypesModel { + + private JSONObject responseJSON = new JSONObject(); + private JSONArray responseJSONArray = new JSONArray(); + private final String TAG = ContentTypesModel.class.getSimpleName(); + + public void setJSON(JSONObject responseJSON) { + + if (responseJSON != null) { + + if (responseJSON.has("content_type")) { + try { + this.responseJSON = responseJSON.getJSONObject("content_type"); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + } + + if (responseJSON.has("content_types")) { + try { + this.responseJSONArray = responseJSON.getJSONArray("content_types"); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + } + + } + } + + public JSONObject getResponse() { + return responseJSON; + } + + public JSONArray getResultArray() { + return responseJSONArray; + } +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/Contentstack.java b/contentstack/src/main/java/com/contentstack/sdk/Contentstack.java new file mode 100755 index 00000000..5e49bdc1 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/Contentstack.java @@ -0,0 +1,231 @@ +package com.contentstack.sdk; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.SystemClock; +import android.text.TextUtils; + +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.toolbox.Volley; +import com.contentstack.sdk.utilities.CSAppConstants; +import com.contentstack.sdk.utilities.CSAppUtils; + +import java.io.File; + +/** + * Contains all Contentstack API classes and functions. + * + * @author contentstack.com, Inc + */ +public class Contentstack { + + private static final String TAG = "Contentstack"; + protected static RequestQueue requestQueue; + private static Contentstack instance; + protected static Context context = null; + + private Contentstack() { + } + + public Contentstack(Context applicationContext) { + context = applicationContext; + } + + /** + * Authenticates the stack api key of your stack. + * This must be called before your stack uses Contentstack sdk. + *
+ * You can find your stack api key from web. + * + * @param context application context. + * @param stackApiKey application api Key of your application on Contentstack. + * @param accessToken access token + * @param environment environment name + * @return {@link Stack} instance. + * + * + *

Example :
+ *
+     * Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "stag");
+     * 
+ */ + public static Stack stack(Context context, String stackApiKey, String accessToken, String environment) throws Exception { + if (context != null) { + if (!TextUtils.isEmpty(stackApiKey)) { + if (!TextUtils.isEmpty(accessToken)) { + if (!TextUtils.isEmpty(environment)) { + Config config = new Config(); + config.setEnvironment(environment); + return initializeStack(context, stackApiKey, accessToken, config); + } else { + throw new Exception(CSAppConstants.ErrorMessage_Stack_Environment_IsNull); + } + } else { + throw new Exception(CSAppConstants.ErrorMessage_Stack_AccessToken_IsNull); + } + } else { + throw new Exception(CSAppConstants.ErrorMessage_StackApiKeyIsNull); + } + } else { + throw new Exception(CSAppConstants.ErrorMessage_StackContextIsNull); + } + } + + /** + * Authenticates the stack api key of your stack. + * This must be called before your stack uses Contentstack sdk. + *
+ * You can find your stack api key from web. + * + * @param context application context. + * @param stackApiKey application api Key of your application on Contentstack. + * @param accessToken access token + * @param config {@link Config} instance to set environment and other configuration details. + * @return {@link Stack} instance. + * + * + *

Example :
+ *
+     * Config config = new Config();
+     * config.setEnvironment("stag");
+     * Stack stack = Contentstack.stack(context, "apiKey", "delierytoken", config);
+     */
+
+
+    public static Stack stack(Context context, String stackApiKey, String accessToken,
+                              String environment, Config config) throws Exception {
+        if (context != null) {
+            if (!TextUtils.isEmpty(stackApiKey)) {
+                if (!TextUtils.isEmpty(accessToken)) {
+                    if (!TextUtils.isEmpty(environment)) {
+
+                        if (config != null) {
+                            config.setEnvironment(environment);
+                        } else {
+                            config = new Config();
+                            config.setEnvironment(environment);
+                        }
+                        return initializeStack(context, stackApiKey, accessToken, config);
+                    } else {
+                        throw new Exception(CSAppConstants.ErrorMessage_Stack_Environment_IsNull);
+                    }
+                } else {
+                    throw new Exception(CSAppConstants.ErrorMessage_Stack_AccessToken_IsNull);
+                }
+            } else {
+                throw new Exception(CSAppConstants.ErrorMessage_StackApiKeyIsNull);
+            }
+        } else {
+            throw new Exception(CSAppConstants.ErrorMessage_StackContextIsNull);
+        }
+    }
+
+
+    private static Stack initializeStack(Context mContext, String stackApiKey, String accessToken, Config config) {
+        Stack stack = new Stack(stackApiKey.trim());
+        stack.setHeader("api_key", stackApiKey);
+        stack.setHeader("access_token", accessToken);
+        context = mContext;
+        stack.setConfig(config);
+
+        if (context != null) {
+            try {
+
+                //cache folder
+                File queryCacheFile = context.getDir("ContentstackCache", 0);
+                CSAppConstants.cacheFolderName = queryCacheFile.getPath();
+
+                clearCache(context);
+            } catch (Exception e) {
+                CSAppUtils.showLog(TAG, "-------------------stack-Contentstack-" + e.toString());
+            }
+        }
+        return stack;
+    }
+
+
+    /********************************************************************************************************
+     *
+     * // Private Functionality
+     *
+     ********************************************************************************************************/
+
+
+    public static synchronized Contentstack getInstance(Context context) {
+        if (instance == null) {
+            instance = new Contentstack(context);
+        }
+        return instance;
+    }
+
+    public RequestQueue getRequestQueue() {
+        if (requestQueue == null) {
+            // getApplicationContext() is key, it keeps you from leaking the
+            // Activity or BroadcastReceiver if someone passes one in.
+            requestQueue = Volley.newRequestQueue(context);
+        }
+        return requestQueue;
+    }
+
+    public  void addToRequestQueue(Request req) {
+        getRequestQueue().add(req);
+    }
+
+    //////////////////////////////////////////
+
+
+    protected static RequestQueue getRequestQueue(String protocol) {
+
+        if (requestQueue == null) {
+            requestQueue = Volley.newRequestQueue(context);
+        }
+        return requestQueue;
+
+
+    }
+
+    protected static  void addToRequestQueue(String protocol, Request req, String tag) {
+        // set the default tag if tag is empty
+        req.setTag(TextUtils.isEmpty(tag) ? TAG : tag);
+        getRequestQueue(protocol).add(req);
+    }
+
+    /**
+     * To start schedule for clearing cache.
+     *
+     * @param context application context.
+     */
+
+    private static void clearCache(Context context) {
+
+        Intent alarmIntent = new Intent("StartContentStackClearingCache");
+        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+        alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), AlarmManager.INTERVAL_DAY, pendingIntent);
+    }
+
+    /**
+     * To check network availability.
+     */
+    protected static void isNetworkAvailable(Context context) {
+
+        ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        if (connectivityManager.getNetworkInfo(0) != null || connectivityManager.getNetworkInfo(1).getState() != null) {
+            if (connectivityManager.getActiveNetworkInfo() == null) {
+                CSAppConstants.isNetworkAvailable = false;
+            } else {
+                CSAppConstants.isNetworkAvailable = true;
+            }
+        } else if (connectivityManager.getNetworkInfo(0).getState() == NetworkInfo.State.CONNECTED ||
+                connectivityManager.getNetworkInfo(1).getState() == NetworkInfo.State.CONNECTED) {
+            CSAppConstants.isNetworkAvailable = true;
+        } else {
+            CSAppConstants.isNetworkAvailable = false;
+        }
+    }
+}
diff --git a/contentstack/src/main/java/com/contentstack/sdk/ContentstackResultCallback.java b/contentstack/src/main/java/com/contentstack/sdk/ContentstackResultCallback.java
new file mode 100755
index 00000000..953e5c35
--- /dev/null
+++ b/contentstack/src/main/java/com/contentstack/sdk/ContentstackResultCallback.java
@@ -0,0 +1,25 @@
+package com.contentstack.sdk;
+
+/**
+ * @author contentstack.com, Inc
+ */
+public abstract class ContentstackResultCallback extends ResultCallBack {
+
+
+    public abstract void onCompletion(ResponseType responseType, Error error);
+
+    public void onRequestFinish(ResponseType responseType) {
+        onCompletion(responseType, null);
+    }
+
+    @Override
+    void onRequestFail(ResponseType responseType, Error error) {
+        onCompletion(responseType, error);
+    }
+
+    @Override
+    void always() {
+
+    }
+
+}
diff --git a/contentstack/src/main/java/com/contentstack/sdk/EntriesModel.java b/contentstack/src/main/java/com/contentstack/sdk/EntriesModel.java
new file mode 100755
index 00000000..03d1013e
--- /dev/null
+++ b/contentstack/src/main/java/com/contentstack/sdk/EntriesModel.java
@@ -0,0 +1,48 @@
+package com.contentstack.sdk;
+
+import com.contentstack.sdk.utilities.CSAppUtils;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author contentstack.com, Inc
+ */
+class EntriesModel {
+
+    protected JSONObject jsonObject;
+    protected String formName;
+    protected List objectList;
+
+    protected EntriesModel(JSONObject responseJSON, String formName, boolean isFromCache) {
+
+        try {
+            if (isFromCache) {
+                this.jsonObject = (responseJSON.opt("response") == null ? null : responseJSON.optJSONObject("response"));
+            } else {
+                this.jsonObject = responseJSON;
+            }
+
+            this.formName = formName;
+            objectList = new ArrayList();
+
+            JSONArray entriesArray = jsonObject.opt("entries") == null ? null : jsonObject.optJSONArray("entries");
+
+            if (entriesArray != null && entriesArray.length() > 0) {
+                int count = entriesArray.length();
+                for (int i = 0; i < count; i++) {
+                    if (entriesArray.opt(i) != null && entriesArray.opt(i) instanceof JSONObject) {
+                        EntryModel entry = new EntryModel(entriesArray.optJSONObject(i), null, true, isFromCache, false);
+                        objectList.add(entry);
+                    }
+                }
+            }
+        } catch (Exception localException) {
+            CSAppUtils.showLog("EntriesModel", "----------------------parsing error|" + localException);
+        }
+
+    }
+}
diff --git a/contentstack/src/main/java/com/contentstack/sdk/Entry.java b/contentstack/src/main/java/com/contentstack/sdk/Entry.java
new file mode 100755
index 00000000..159202de
--- /dev/null
+++ b/contentstack/src/main/java/com/contentstack/sdk/Entry.java
@@ -0,0 +1,1503 @@
+package com.contentstack.sdk;
+
+import android.util.ArrayMap;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.contentstack.sdk.utilities.CSAppConstants;
+import com.contentstack.sdk.utilities.CSAppUtils;
+import com.contentstack.sdk.utilities.CSController;
+import com.contentstack.sdk.utilities.ContentstackUtil;
+import com.contentstack.txtmark.Configuration;
+import com.contentstack.txtmark.Processor;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/*** Entry is used to create, update and delete contentType's entries on the Contentstack.
+ *
+ * @author Contentstack.com, Inc
+ *
+ */
+public class Entry {
+
+    private static final String TAG = "Entry";
+
+    private String contentTypeName = null;
+    private ArrayMap localHeader = null;
+    protected ArrayMap formHeader = null;
+    private ContentType contentTypeInstance = null;
+    private String[] tags = null;
+
+    protected String uid = null;
+    protected JSONObject resultJson = null;
+    protected String ownerEmailId = null;
+    protected String ownerUid = null;
+    protected HashMap owner = null;
+    protected HashMap _metadata = null;
+    protected String title = null;
+    protected String url = null;
+    protected String language = null;
+    private JSONArray referenceArray;
+    protected JSONObject otherPostJSON;
+    private JSONArray objectUidForOnly;
+    private JSONArray objectUidForExcept;
+    private JSONObject onlyJsonObject;
+    private JSONObject exceptJsonObject;
+    private CachePolicy cachePolicyForCall = CachePolicy.NETWORK_ONLY;
+
+    private long maxCacheTimeForCall = 0; //local cache time interval
+    private long defaultCacheTimeInterval = 0;
+    protected boolean isDelete = false;
+
+    private Entry() {
+    }
+
+    protected Entry(String contentTypeName) {
+        this.contentTypeName = contentTypeName;
+        this.localHeader = new ArrayMap<>();
+        this.otherPostJSON = new JSONObject();
+    }
+
+    protected void setContentTypeInstance(ContentType contentTypeInstance) {
+        this.contentTypeInstance = contentTypeInstance;
+    }
+
+    public Entry configure(JSONObject jsonObject) {
+        EntryModel model = new EntryModel(jsonObject, null, true, false, false);
+        this.resultJson = model.jsonObject;
+        this.ownerEmailId = model.ownerEmailId;
+        this.ownerUid = model.ownerUid;
+        this.title = model.title;
+        this.url = model.url;
+        this.language = model.language;
+        if (model.ownerMap != null) {
+            this.owner = new HashMap<>(model.ownerMap);
+        }
+        if (model._metadata != null) {
+            this._metadata = new HashMap<>(model._metadata);
+        }
+
+        this.uid = model.entryUid;
+        this.setTags(model.tags);
+        model = null;
+
+        return this;
+    }
+
+    /**
+     * Set headers.
+     *
+     * @param key   custom_header_key
+     * @param value custom_header_value
+     *
+     *              

Example :
+ *
+     *              Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken",  "stag");
+     *              Entry entry = stack.contentType("form_name").entry("entry_uid");
+     *              entry.setHeader("custom_header_key", "custom_header_value");
+     *              
+ */ + public void setHeader(String key, String value) { + if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(value)) { + localHeader.put(key, value); + } + } + + /** + * Remove header key. + * + * @param key custom_header_key + * + *

Example :
+ *
+     *            Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken",  "stag");
+     *            Entry entry = stack.contentType("form_name").entry("entry_uid");
+     *            entry.removeHeader("custom_header_key");
+     *            
+ */ + public void removeHeader(String key) { + if (!TextUtils.isEmpty(key)) { + localHeader.remove(key); + } + } + + /** + * Get title string + * + *

+ *

Example :
+ *

+     * String title = entry.getTitle();
+     * 
+ *

+ */ + public String getTitle() { + return title; + } + + /** + * Get url string + * + *

+ *

Example :
+ *

+     * String url = entry.getURL();
+     * 
+ *

+ */ + public String getURL() { + return url; + } + + /** + * Get tags. + * + *

Example :
+ *
+     * String[] tags = entry.getURL();
+     * 
+ */ + public String[] getTags() { + return tags; + } + + /** + * Get contentType name. + * + *

Example :
+ *
+     * String contentType = entry.getFileType();
+     * 
+ */ + public String getContentType() { + return contentTypeName; + } + + /** + * Get uid. + * + *

Example :
+ *
+     * String uid = entry.getUid();
+     * 
+ */ + public String getUid() { + return uid; + } + + /** + * Get metadata of entry. + * + *

Example :
+ *
+     * HashMap metaData = entry.getMetadata();
+     * 
+ */ + private HashMap getMetadata() { + return _metadata; + } + + /** + * Get {@link Language} instance + * + *

+ *

Example :
+ *

+     * Language local = entry.getLanguage();
+     * 
+ *

+ */ + @Deprecated + public Language getLanguage() { + String localeCode = null; + + if (_metadata != null && _metadata.size() > 0 && _metadata.containsKey("locale")) { + localeCode = (String) _metadata.get("locale"); + } else if (resultJson.has("locale")) { + localeCode = (String) resultJson.optString("locale"); + } + + if (localeCode != null) { + localeCode = localeCode.replace("-", "_"); + LanguageCode codeValue = LanguageCode.valueOf(localeCode); + int localeValue = codeValue.ordinal(); + Language[] language = Language.values(); + + return language[localeValue]; + } + return null; + } + + + /** + *

+ *

Example :
+ *

+     * String local = entry.getLocale();
+     * 
+ *

+ */ + public String getLocale() { + + if (resultJson.has("locale")) { + return (String) resultJson.optString("locale"); + } + + return this.language; + } + + + /** + *

+ *

Example :
+ *

+     * Entry entry = entry.setLocale("en-hi");
+     * 
+ *

+ */ + public Entry setLocale(String locale) { + + if (locale != null) { + try { + otherPostJSON.put("locale", locale); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + } + + return this; + } + + + public HashMap getOwner() { + return owner; + } + + /** + * Get entry representation in json + * + *

+ *

Example :
+ *

+     * JSONObject json = entry.toJSON();
+     * 
+ *

+ */ + public JSONObject toJSON() { + return resultJson; + } + + /** + * Get object value for key. + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            Object obj = entry.get("key");
+     *            
+ */ + public Object get(String key) { + try { + if (resultJson != null && key != null) { + return resultJson.get(key); + } else { + return null; + } + } catch (Exception e) { + CSAppUtils.showLog(TAG, e.getLocalizedMessage()); + return null; + } + } + + /** + * Get html text for markdown data type + * + * @param markdownKey field_uid as key. + * @return html text in string format. + * + *

Example :
+ *
+     * String htmlText = entry.getHtmlText("markdownKey");
+     * 
+ */ + public String getHtmlText(String markdownKey) { + try { + return Processor.process(getString(markdownKey), Configuration.builder().forceExtentedProfile().build()); + } catch (Exception e) { + CSAppUtils.showLog(TAG, e.getLocalizedMessage()); + return null; + } + } + + /** + * Get html text for markdown data type which is multiple true + * + * @param markdownKey field_uid as key. + * @return html text in string format. + * + *

Example :
+ *
+     * ArrayList<String> htmlTexts = entry.getMultipleHtmlText("markdownKey");
+     * 
+ */ + public ArrayList getMultipleHtmlText(String markdownKey) { + try { + ArrayList multipleHtmlStrings = new ArrayList<>(); + JSONArray jsonArray = getJSONArray(markdownKey); + for (int i = 0; i < jsonArray.length(); i++) { + multipleHtmlStrings.add(Processor.process(jsonArray.getString(i), Configuration.builder().forceExtentedProfile().build())); + } + return multipleHtmlStrings; + } catch (Exception e) { + CSAppUtils.showLog(TAG, e.getLocalizedMessage()); + return null; + } + } + + /** + * Get string value for key. + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            String value = entry.getString("key");
+     *            
+ */ + public String getString(String key) { + Object value = get(key); + if (value != null) { + if (value instanceof String) { + return (String) value; + } + } + return null; + } + + /** + * Get boolean value for key. + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            Boolean value = entry.getBoolean("key");
+     *            
+ */ + public Boolean getBoolean(String key) { + Object value = get(key); + if (value != null) { + if (value instanceof Boolean) { + return (Boolean) value; + } + } + return false; + } + + /** + * Get {@link JSONArray} value for key + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            JSONArray value = entry.getJSONArray("key");
+     *            
+ */ + public JSONArray getJSONArray(String key) { + Object value = get(key); + if (value != null) { + if (value instanceof JSONArray) { + return (JSONArray) value; + } + } + return null; + } + + /** + * Get {@link JSONObject} value for key + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            JSONObject value = entry.getJSONObject("key");
+     *            
+ */ + public JSONObject getJSONObject(String key) { + Object value = get(key); + if (value != null) { + if (value instanceof JSONObject) { + return (JSONObject) value; + } + } + return null; + } + + /** + * Get {@link JSONObject} value for key + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            JSONObject value = entry.getJSONObject("key");
+     *            
+ */ + public Number getNumber(String key) { + Object value = get(key); + if (value != null) { + if (value instanceof Number) { + return (Number) value; + } + } + return null; + } + + /** + * Get integer value for key + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            int value = entry.getInt("key");
+     *            
+ */ + public int getInt(String key) { + Number value = getNumber(key); + if (value != null) { + return value.intValue(); + } + return 0; + } + + /** + * Get integer value for key + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            float value = entry.getFloat("key");
+     *            
+ */ + public float getFloat(String key) { + Number value = getNumber(key); + if (value != null) { + return value.floatValue(); + } + return (float) 0; + } + + /** + * Get double value for key + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            double value = entry.getDouble("key");
+     *            
+ */ + public double getDouble(String key) { + Number value = getNumber(key); + if (value != null) { + return value.doubleValue(); + } + return (double) 0; + } + + /** + * Get long value for key + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            long value = entry.getLong("key");
+     *            
+ */ + public long getLong(String key) { + Number value = getNumber(key); + if (value != null) { + return value.longValue(); + } + return (long) 0; + } + + /** + * Get short value for key + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            short value = entry.getShort("key");
+     *            
+ */ + public short getShort(String key) { + Number value = getNumber(key); + if (value != null) { + return value.shortValue(); + } + return (short) 0; + } + + /** + * Get {@link Calendar} value for key + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            Calendar value = entry.getDate("key");
+     *            
+ */ + public Calendar getDate(String key) { + + try { + String value = getString(key); + return ContentstackUtil.parseDate(value, null); + } catch (Exception e) { + CSAppUtils.showLog(TAG, e.getLocalizedMessage()); + } + return null; + } + + /** + * Get {@link Calendar} value of creation time of entry. + * + * + *

Example :
+ *
+     * Calendar createdAt = entry.getCreateAt("key");
+     * 
+ */ + public Calendar getCreateAt() { + + try { + String value = getString("created_at"); + return ContentstackUtil.parseDate(value, null); + } catch (Exception e) { + CSAppUtils.showLog(TAG, e.getLocalizedMessage()); + } + return null; + } + + /** + * Get uid who created this entry. + * + *

Example :
+ *
+     * String createdBy_uid = entry.getCreatedBy();
+     * 
+ */ + public String getCreatedBy() { + + return getString("created_by"); + } + + /** + * Get {@link Calendar} value of updating time of entry. + * + * + *

Example :
+ *
+     * Calendar updatedAt = entry.getUpdateAt("key");
+     * 
+ */ + public Calendar getUpdateAt() { + + try { + String value = getString("updated_at"); + return ContentstackUtil.parseDate(value, null); + } catch (Exception e) { + CSAppUtils.showLog(TAG, e.getLocalizedMessage()); + } + return null; + } + + /** + * Get uid who updated this entry. + * + *

Example :
+ *
+     * String updatedBy_uid = entry.getUpdatedBy();
+     * 
+ */ + public String getUpdatedBy() { + return getString("updated_by"); + } + + /** + * Get {@link Calendar} value of deletion time of entry. + * + * + *

Example :
+ *
+     * Calendar updatedAt = entry.getUpdateAt("key");
+     * 
+ */ + public Calendar getDeleteAt() { + + try { + String value = getString("deleted_at"); + return ContentstackUtil.parseDate(value, null); + } catch (Exception e) { + CSAppUtils.showLog(TAG, e.getLocalizedMessage()); + } + return null; + } + + /** + * Get uid who deleted this entry. + * + *

Example :
+ *
+     * String deletedBy_uid = entry.getDeletedBy();
+     * 
+ */ + public String getDeletedBy() { + + return getString("deleted_by"); + } + + /** + * Get an asset from the entry + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            Asset asset = entry.getAsset("key");
+     *            
+ */ + public Asset getAsset(String key) { + + JSONObject assetObject = getJSONObject(key); + Asset asset = contentTypeInstance.stackInstance.asset().configure(assetObject); + + return asset; + } + + /** + * Get an assets from the entry. This works with multiple true fields + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            List asset = entry.getAssets("key");
+     *            
+ */ + public List getAssets(String key) { + List assets = new ArrayList<>(); + JSONArray assetArray = getJSONArray(key); + + for (int i = 0; i < assetArray.length(); i++) { + if (assetArray.opt(i) instanceof JSONObject) { + Asset asset = contentTypeInstance.stackInstance.asset().configure(assetArray.optJSONObject(i)); + assets.add(asset); + } + } + return assets; + } + + /** + * Get a group from entry. + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            Group innerGroup = entry.getGroup("key");
+     *            
+ */ + public Group getGroup(String key) { + + if (!TextUtils.isEmpty(key) && resultJson.has(key) && resultJson.opt(key) instanceof JSONObject) { + return new Group(contentTypeInstance.stackInstance, resultJson.optJSONObject(key)); + } + return null; + } + + /** + * Get a list of group from entry. + * + *

+ * Note :- This will work when group is multiple true. + * + * @param key field_uid as key. + * + *

Example :
+ *

+     *            Group innerGroup = entry.getGroups("key");
+     *            
+ */ + public List getGroups(String key) { + + if (!TextUtils.isEmpty(key) && resultJson.has(key) && resultJson.opt(key) instanceof JSONArray) { + JSONArray array = resultJson.optJSONArray(key); + List groupList = new ArrayList<>(); + + for (int i = 0; i < array.length(); i++) { + if (array.opt(i) instanceof JSONObject) { + Group group = new Group(contentTypeInstance.stackInstance, array.optJSONObject(i)); + groupList.add(group); + } + } + + return groupList; + } + return null; + } + + /** + * Get value for the given reference key. + * + * @param refKey key of a reference field. + * @param refContentType class uid. + * @return {@link ArrayList} of {@link Entry} instances. + * Also specified contentType value will be set as class uid for all {@link Entry} instance. + * + * + *

Example :
+ *
+     *  Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken",  "stag");
+     *  Query csQuery = stack.contentType("contentType_name").query();
+     *
+     * csQuery.includeReference("for_bug");
+     *
+     * csQuery.find(new QueryResultsCallBack() {
+ * @Override + * public void onCompletion(ResponseType responseType, QueryResult queryResult, Error error) {
+ * + * if(error == null){ + * List<Entry> list = builtqueryresult.getResultObjects(); + * for (int i = 0; i < list.queueSize(); i++) { + * Entry entry = list.get(i); + * Entry taskEntry = entry.getAllEntries("for_task", "task"); + * } + * } + * + * } + * });
+ * + *
+ */ + public ArrayList getAllEntries(String refKey, String refContentType) { + try { + if (resultJson != null) { + + if (resultJson.get(refKey) instanceof JSONArray) { + + int count = ((JSONArray) resultJson.get(refKey)).length(); + ArrayList builtObjectList = new ArrayList(); + for (int i = 0; i < count; i++) { + + EntryModel model = new EntryModel(((JSONArray) resultJson.get(refKey)).getJSONObject(i), null, false, false, true); + Entry entryInstance = null; + try { + entryInstance = contentTypeInstance.stackInstance.contentType(refContentType).entry(); + } catch (Exception e) { + entryInstance = new Entry(refContentType); + CSAppUtils.showLog(TAG, e.getLocalizedMessage()); + } + entryInstance.setUid(model.entryUid); + entryInstance.ownerEmailId = model.ownerEmailId; + entryInstance.ownerUid = model.ownerUid; + if (model.ownerMap != null) { + entryInstance.owner = new HashMap<>(model.ownerMap); + } + entryInstance.resultJson = model.jsonObject; + entryInstance.setTags(model.tags); + + builtObjectList.add(entryInstance); + model = null; + } + + return builtObjectList; + + } + } + } catch (Exception e) { + CSAppUtils.showLog(TAG, e.getLocalizedMessage()); + return null; + } + + return null; + } + + /** + * Specifies list of field uids that would be 'excluded' from the response. + * + * @param fieldUid field uid which get 'excluded' from the response. + * @return {@link Entry} object, so you can chain this call. + * + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken",  "stag");
+     *     Entry entry = stack.contentType("form_name").entry("entry_uid");
+ * entry.except(new String[]{"name", "description"}); + *
+ */ + public Entry except(String[] fieldUid) { + try { + if (fieldUid != null && fieldUid.length > 0) { + + if (objectUidForExcept == null) { + objectUidForExcept = new JSONArray(); + } + + int count = fieldUid.length; + for (int i = 0; i < count; i++) { + objectUidForExcept.put(fieldUid[i]); + } + } + } catch (Exception e) { + CSAppUtils.showLog(TAG, "--except-catch|" + e); + } + return this; + } + + /** + * Add a constraint that requires a particular reference key details. + * + * @param referenceField key that to be constrained. + * @return {@link Entry} object, so you can chain this call. + * + *

Example :
+ *
+     *    Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken",  "stag");
+     *    Entry entry = stack.contentType("form_name").entry("entry_uid");
+ * entry.includeReference("referenceUid"); + *
+ */ + public Entry includeReference(String referenceField) { + try { + if (!TextUtils.isEmpty(referenceField)) { + if (referenceArray == null) { + referenceArray = new JSONArray(); + } + + referenceArray.put(referenceField); + + otherPostJSON.put("include[]", referenceArray); + } + } catch (Exception e) { + CSAppUtils.showLog(TAG, "--include Reference-catch|" + e.getLocalizedMessage()); + } + + return this; + } + + /** + * Add a constraint that requires a particular reference key details. + * + * @param referenceFields array key that to be constrained. + * @return {@link Entry} object, so you can chain this call. + * + *

Example :
+ *
+     *    Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken",  "stag");
+     *    Entry entry = stack.contentType("form_name").entry("entry_uid");
+ * entry.includeReference(new String[]{"referenceUid_A", "referenceUid_B"}); + *
+ */ + public Entry includeReference(String[] referenceFields) { + try { + if (referenceFields != null && referenceFields.length > 0) { + if (referenceArray == null) { + referenceArray = new JSONArray(); + } + for (int i = 0; i < referenceFields.length; i++) { + referenceArray.put(referenceFields[i]); + } + + otherPostJSON.put("include[]", referenceArray); + } + } catch (Exception e) { + CSAppUtils.showLog(TAG, "--include Reference-catch|" + e.getLocalizedMessage()); + } + + return this; + } + + /** + * Specifies an array of 'only' keys in BASE object that would be 'included' in the response. + * + * @param fieldUid Array of the 'only' reference keys to be included in response. + * @return {@link Entry} object, so you can chain this call. + * + *

Example :
+ *
+     *    Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken",  "stag");
+     *    Entry entry = stack.contentType("form_name").entry("entry_uid");
+ * entry.only(new String[]{"name", "description"}); + *
+ */ + public Entry only(String[] fieldUid) { + try { + if (fieldUid != null && fieldUid.length > 0) { + if (objectUidForOnly == null) { + objectUidForOnly = new JSONArray(); + } + + int count = fieldUid.length; + for (int i = 0; i < count; i++) { + objectUidForOnly.put(fieldUid[i]); + } + } + } catch (Exception e) { + CSAppUtils.showLog(TAG, "--include Reference-catch|" + e.getLocalizedMessage()); + } + + return this; + } + + /** + * Specifies an array of 'only' keys that would be 'included' in the response. + * + * @param fieldUid Array of the 'only' reference keys to be included in response. + * @param referenceFieldUid Key who has reference to some other class object.. + * @return {@link Entry} object, so you can chain this call. + * + *

Example :
+ *
+     *    Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken",  "stag");
+     *    Entry entry = stack.contentType("form_name").entry("entry_uid");
+ * ArrayList<String> array = new ArrayList<String>(); + * array.add("description"); + * array.add("name");
+ * entry.onlyWithReferenceUid(array, "referenceUid"); + *
+ */ + public Entry onlyWithReferenceUid(ArrayList fieldUid, String referenceFieldUid) { + try { + if (fieldUid != null && referenceFieldUid != null) { + if (onlyJsonObject == null) { + onlyJsonObject = new JSONObject(); + } + JSONArray fieldValueArray = new JSONArray(); + int count = fieldUid.size(); + for (int i = 0; i < count; i++) { + fieldValueArray.put(fieldUid.get(i)); + } + onlyJsonObject.put(referenceFieldUid, fieldValueArray); + includeReference(referenceFieldUid); + } + } catch (Exception e) { + CSAppUtils.showLog(TAG, "--onlyWithReferenceUid-catch|" + e.getLocalizedMessage()); + } + return this; + } + + /** + * Specifies an array of 'except' keys that would be 'excluded' in the response. + * + * @param fieldUid Array of the 'except' reference keys to be excluded in response. + * @param referenceFieldUid Key who has reference to some other class object. + * @return {@link Entry} object, so you can chain this call. + * + *

Example :
+ *
+     *    Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken",  "stag");
+     *    Entry entry = stack.contentType("form_name").entry("entry_uid");
+ * ArrayList<String> array = new ArrayList<String>(); + * array.add("description"); + * array.add("name");
+ * entry.onlyWithReferenceUid(array, "referenceUid"); + *
+ */ + public Entry exceptWithReferenceUid(ArrayList fieldUid, String referenceFieldUid) { + try { + if (fieldUid != null && referenceFieldUid != null) { + if (exceptJsonObject == null) { + exceptJsonObject = new JSONObject(); + } + JSONArray fieldValueArray = new JSONArray(); + int count = fieldUid.size(); + for (int i = 0; i < count; i++) { + fieldValueArray.put(fieldUid.get(i)); + } + + exceptJsonObject.put(referenceFieldUid, fieldValueArray); + + includeReference(referenceFieldUid); + } + } catch (Exception e) { + CSAppUtils.showLog(TAG, "--exceptWithReferenceUid-catch|" + e.getLocalizedMessage()); + } + return this; + } + + + protected void setTags(String[] tags) { + this.tags = tags; + } + + protected void setUid(String uid) { + this.uid = uid; + } + + /** + * Fetches the latest version of the entries from content stack + * + * @param callBack {@link EntryResultCallBack} object to notify the application when the request has completed. + *

+ * {@link Entry} object, so you can chain this call. + * + *

Example :
+ *

+     *                    Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken",  "stag");
+     *                    Entry entry = stack.contentType("form_name").entry("entry_uid");
+ * entry.fetch(new BuiltResultCallBack() {
+ * @Override + * public void onCompletion(ResponseType responseType, BuiltError builtError) { + * + * }
+ * });
+ */ + public void fetch(EntryResultCallBack callBack) { + try { + if (!TextUtils.isEmpty(uid)) { + + String URL = "/" + contentTypeInstance.stackInstance.VERSION + "/content_types/" + contentTypeName + "/entries/" + uid; + + ArrayMap headers = getHeader(localHeader); + HashMap headerAll = new HashMap(); + JSONObject urlQueries = new JSONObject(); + + if (headers != null && headers.size() > 0) { + for (Map.Entry entry : headers.entrySet()) { + headerAll.put(entry.getKey(), (String) entry.getValue()); + } + + if (headers.containsKey("environment")) { + urlQueries.put("environment", headers.get("environment")); + } + } + + String mainStringForMD5 = URL + new JSONObject().toString() + headerAll.toString(); + String md5Value = new CSAppUtils().getMD5FromString(mainStringForMD5.trim()); + + File cacheFile = new File(CSAppConstants.cacheFolderName + File.separator + md5Value); + + + switch (cachePolicyForCall) { + + case IGNORE_CACHE: + fetchFromNetwork(URL, urlQueries, cacheFile.getPath(), callBack); + break; + + case NETWORK_ONLY: + fetchFromNetwork(URL, urlQueries, cacheFile.getPath(), callBack); + break; + + case CACHE_ONLY: + fetchFromCache(cacheFile, callBack); + break; + + case CACHE_ELSE_NETWORK: + + if (cacheFile.exists()) { + boolean needToSendCall = false; + + // if (maxCacheTimeForCall > 0) { + // needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) maxCacheTimeForCall); + // } else { + // needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) defaultCacheTimeInterval); + // } + needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) maxCacheTimeForCall); + + if (needToSendCall) { + fetchFromNetwork(URL, urlQueries, cacheFile.getPath(), callBack); + + } else { + setCacheModel(cacheFile, callBack); + } + + } else { + fetchFromNetwork(URL, urlQueries, cacheFile.getPath(), callBack); + } + + break; + + case CACHE_THEN_NETWORK: + if (cacheFile.exists()) { + setCacheModel(cacheFile, callBack); + } + + // from network + fetchFromNetwork(URL, urlQueries, cacheFile.getPath(), callBack); + break; + + case NETWORK_ELSE_CACHE: + + if (CSAppConstants.isNetworkAvailable) { + fetchFromNetwork(URL, urlQueries, cacheFile.getPath(), callBack); + } else { + fetchFromCache(cacheFile, callBack); + } + + break; + + } + + } else { + throwException(CSAppConstants.ErrorMessage_EntryUID, null, callBack); + } + } catch (Exception e) { + throwException(null, e, callBack); + } + } + + private void fetchFromNetwork(String URL, JSONObject urlQueries, String cacheFilePath, EntryResultCallBack callBack) { + try { + + JSONObject mainJson = new JSONObject(); + + setIncludeJSON(urlQueries, callBack); + mainJson.put("query", urlQueries); + + mainJson.put("_method", CSAppConstants.RequestMethod.GET.toString()); + + HashMap urlParams = getUrlParams(mainJson); + + new CSBackgroundTask(this, contentTypeInstance.stackInstance, CSController.FETCHENTRY, URL, getHeader(localHeader), urlParams, new JSONObject(), cacheFilePath, CSAppConstants.callController.ENTRY.toString(), false, CSAppConstants.RequestMethod.GET, callBack); + + } catch (Exception e) { + throwException(null, e, callBack); + } + } + + //fetch from cache. + private void fetchFromCache(File cacheFile, EntryResultCallBack callback) { + Error error = null; + if (cacheFile.exists()) { + boolean needToSendCall = false; + + // if (maxCacheTimeForCall > 0) { + // needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) maxCacheTimeForCall); + // } else { + // needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) defaultCacheTimeInterval); + // } + needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) maxCacheTimeForCall); + + if (needToSendCall) { + error = new Error(); + error.setErrorMessage(CSAppConstants.ErrorMessage_EntryNotFoundInCache); + + } else { + setCacheModel(cacheFile, callback); + } + } else { + error = new Error(); + error.setErrorMessage(CSAppConstants.ErrorMessage_EntryNotFoundInCache); + } + + if (callback != null && error != null) { + callback.onRequestFail(ResponseType.CACHE, error); + } + } + + //Entry modeling from cache. + private void setCacheModel(File cacheFile, EntryResultCallBack callback) { + + EntryModel model = new EntryModel(CSAppUtils.getJsonFromCacheFile(cacheFile), null, false, true, false); + this.resultJson = model.jsonObject; + this.ownerEmailId = model.ownerEmailId; + this.ownerUid = model.ownerUid; + this.title = model.title; + this.url = model.url; + if (model.ownerMap != null) { + this.owner = new HashMap<>(model.ownerMap); + } + if (model._metadata != null) { + this._metadata = new HashMap<>(model._metadata); + } + + this.uid = model.entryUid; + setTags(model.tags); + model = null; + + if (callback != null) { + callback.onRequestFinish(ResponseType.CACHE); + } + } + + /** + * To cancel all {@link Entry} network requests. + * + *

Example :
+ *
+     *     entry.cancelRequest();
+     * 
+ */ + + public void cancelRequest() { + CSAppConstants.cancelledCallController.add(CSAppConstants.callController.ENTRY.toString()); + + if (Contentstack.requestQueue != null) { + Contentstack.requestQueue.cancelAll(CSAppConstants.callController.ENTRY.toString()); + } + } + + /** + * To set cache policy using {@link Query} instance. + * + * @param cachePolicy {@link CachePolicy} instance. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *      Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken",  "stag");
+     *      Entry entry = stack.contentType("form_name").entry();
+ * entry.setCachePolicy(NETWORK_ELSE_CACHE); + *
+ */ + public void setCachePolicy(CachePolicy cachePolicy) { + this.cachePolicyForCall = cachePolicy; + } + + private HashMap getUrlParams(JSONObject jsonMain) { + + JSONObject queryJSON = jsonMain.optJSONObject("query"); + HashMap hashMap = new HashMap<>(); + + if (queryJSON != null && queryJSON.length() > 0) { + Iterator iter = queryJSON.keys(); + while (iter.hasNext()) { + String key = iter.next(); + try { + Object value = queryJSON.opt(key); + hashMap.put(key, value); + } catch (Exception e) { + CSAppUtils.showLog(TAG, e.getLocalizedMessage()); + } + } + + return hashMap; + } + + return null; + } + + private void setIncludeJSON(JSONObject mainJson, ResultCallBack callBack) { + try { + Iterator iterator = otherPostJSON.keys(); + while (iterator.hasNext()) { + String key = iterator.next(); + Object value = otherPostJSON.get(key); + mainJson.put(key, value); + } + + if (objectUidForOnly != null && objectUidForOnly.length() > 0) { + mainJson.put("only[BASE][]", objectUidForOnly); + objectUidForOnly = null; + + } + + if (objectUidForExcept != null && objectUidForExcept.length() > 0) { + mainJson.put("except[BASE][]", objectUidForExcept); + objectUidForExcept = null; + } + + if (exceptJsonObject != null && exceptJsonObject.length() > 0) { + mainJson.put("except", exceptJsonObject); + exceptJsonObject = null; + } + + if (onlyJsonObject != null && onlyJsonObject.length() > 0) { + mainJson.put("only", onlyJsonObject); + onlyJsonObject = null; + } + } catch (Exception e) { + throwException(null, e, (EntryResultCallBack) callBack); + } + } + + private void throwException(String errorMsg, Exception e, EntryResultCallBack callBack) { + + Error error = new Error(); + if (errorMsg != null) { + error.setErrorMessage(errorMsg); + } else { + error.setErrorMessage(e.toString()); + } + callBack.onRequestFail(ResponseType.UNKNOWN, error); + } + + private ArrayMap getHeader(ArrayMap localHeader) { + ArrayMap mainHeader = formHeader; + ArrayMap classHeaders = new ArrayMap<>(); + + if (localHeader != null && localHeader.size() > 0) { + if (mainHeader != null && mainHeader.size() > 0) { + for (Map.Entry entry : localHeader.entrySet()) { + String key = entry.getKey(); + classHeaders.put(key, entry.getValue()); + } + + for (Map.Entry entry : mainHeader.entrySet()) { + String key = entry.getKey(); + if (!classHeaders.containsKey(key)) { + classHeaders.put(key, entry.getValue()); + } + } + + return classHeaders; + + } else { + return localHeader; + } + + } else { + return formHeader; + } + } + + /** + * This method adds key and value to an Entry. + * + * @param key The key as string which needs to be added to an Entry + * @param value The value as string which needs to be added to an Entry + * @return {@link Entry} + * + *

Example :
+ *
+     *    {@code
+     *    Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken",  "stag");
+     *    final Entry entry = stack.contentType("user").entry("entryUid"); 
+ * entry.addParam("include_dimensions", "true");
+ * entry.fetch(new ResultCallBack() {
+ * @Override + * public void onCompletion(ResponseType responseType, Error error) { + * + * }
+ * });
+ * + * } + *
+ */ + + public Entry addParam(String key, String value) { + + if (key != null && value != null) { + try { + otherPostJSON.put(key, value); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + } + + return this; + } + + + /** + * This method also includes the content type UIDs of the referenced entries returned in the response + * + * @return {@link Entry} + * + *

Example :
+ *
+     * {@code
+     * Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken",  "stag");
+     * final Entry entry = stack.contentType("user").entry("apiKey"); 
+ * entry.includeReferenceContentTypeUID;
+ * entry.fetch(new BuiltResultCallBack() { + *
@ + * Override + * public void onCompletion(ResponseType responseType, BuiltError builtError) { + * }
+ * });
+ * } + *
+ */ + public Entry includeReferenceContentTypeUID() { + try { + otherPostJSON.put("include_reference_content_type_uid", "true"); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + return this; + } + + + /** + * Include Content Type of all returned objects along with objects themselves. + * + * @return {@link Entry} object, so you can chain this call. + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken",  "stag");
+     *     final Entry entry = stack.contentType("user").entry("entryUid");
+     *     entry.includeContentType();
+     * 
+ */ + public Entry includeContentType() { + try { + if (otherPostJSON.has("include_schema")) { + otherPostJSON.remove("include_schema"); + } + otherPostJSON.put("include_content_type", true); + otherPostJSON.put("include_global_field_schema", true); + } catch (Exception e) { + Log.e(TAG, e.getLocalizedMessage()); + } + return this; + } + + + /** + * Retrieve the published content of the fallback locale if an entry is not localized in specified locale. + * + * @return {@link Entry} object, so you can chain this call. + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "ApiKey", "deliveryToken", "environment");
+     *     final Entry entry = stack.contentType("user").entry("entryUid");
+     *     entry.includeFallback();
+     * 
+ */ + public Entry includeFallback() { + try { + otherPostJSON.put("include_fallback", true); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + return this; + } + + + /** + * includeEmbeddedItems instance of Entry + * Include Embedded Objects (Entries and Assets) along with entry/entries details.
+ * Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "stag"); + * final Entry entry = stack.contentType("user").entry("entry_uid"); + * entry = entry.includeEmbeddedObjects() + * + * @return {@link Entry} + */ + public Entry includeEmbeddedItems() { + try { + otherPostJSON.put("include_embedded_items[]", "BASE"); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + return this; + } +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/EntryModel.java b/contentstack/src/main/java/com/contentstack/sdk/EntryModel.java new file mode 100755 index 00000000..e8ccbc0d --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/EntryModel.java @@ -0,0 +1,128 @@ +package com.contentstack.sdk; + +import com.contentstack.sdk.utilities.CSAppUtils; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.WeakHashMap; + +/** + * @author Contentstack.com, Inc + */ +class EntryModel { + + protected JSONObject jsonObject = null; + protected String entryUid = null; + protected String ownerEmailId = null; + protected String ownerUid = null; + protected String title = null; + protected String url = null; + protected String language = null; + protected String[] tags = null; + protected WeakHashMap ownerMap = null; + protected WeakHashMap _metadata = null; + private JSONArray tagsArray = null; + + public EntryModel(JSONObject jsonObj, String entryUid, boolean isFromObjectsModel, boolean isFromCache, boolean isFromDeltaResponse) { + + try { + this.entryUid = entryUid; + if (isFromObjectsModel) { + jsonObject = jsonObj; + this.entryUid = (String) (jsonObject.isNull("uid") ? " " : jsonObject.opt("uid")); + } else { + + if (isFromCache) { + jsonObject = jsonObj.opt("response") == null ? null : jsonObj.optJSONObject("response"); + } else { + jsonObject = jsonObj; + } + + if (isFromDeltaResponse) { + this.entryUid = (String) (jsonObject != null && jsonObject.isNull("uid") ? " " : jsonObject.opt("uid")); + } else { + jsonObject = jsonObject != null && jsonObject.opt("entry") == null ? null : jsonObject.optJSONObject("entry"); + } + } + if (jsonObject != null && jsonObject.has("uid")) { + this.entryUid = (String) (jsonObject.isNull("uid") ? " " : jsonObject.opt("uid")); + } + + if (jsonObject != null && jsonObject.has("title")) { + this.title = (String) (jsonObject.isNull("title") ? " " : jsonObject.opt("title")); + } + + if (jsonObject != null && jsonObject.has("url")) { + this.url = (String) (jsonObject.isNull("url") ? " " : jsonObject.opt("url")); + } + + if (jsonObject != null && jsonObject.has("locale")) { + this.language = (String) (jsonObject.isNull("locale") ? " " : jsonObject.opt("locale")); + } + + if (jsonObject != null && jsonObject.has("_metadata")) { + JSONObject _metadataJSON = jsonObject.optJSONObject("_metadata"); + Iterator iterator = _metadataJSON.keys(); + _metadata = new WeakHashMap<>(); + while (iterator.hasNext()) { + String key = iterator.next(); + if (key.equalsIgnoreCase("uid")) { + this.entryUid = _metadataJSON.optString(key); + } + _metadata.put(key, _metadataJSON.optString(key)); + } + } else if (jsonObject != null && jsonObject.has("publish_details")) { + + JSONArray publishArray = jsonObject.optJSONArray("publish_details"); + for (int i = 0; i < publishArray.length(); i++) { + JSONObject jsonObject = publishArray.optJSONObject(i); + Iterator iterator = jsonObject.keys(); + HashMap hashMap = new HashMap<>(); + while (iterator.hasNext()) { + String key = iterator.next(); + hashMap.put(key, jsonObject.opt(key)); + } + } + + _metadata = new WeakHashMap<>(); + _metadata.put("publish_details", publishArray); + } + + + if (jsonObject != null && jsonObject.has("_owner") && (jsonObject.opt("_owner") != null) && (!jsonObject.opt("_owner").toString().equalsIgnoreCase("null"))) { + JSONObject ownerObject = jsonObject.optJSONObject("_owner"); + if (ownerObject.has("email") && ownerObject.opt("email") != null) { + ownerEmailId = (String) ownerObject.opt("email"); + } + + if (ownerObject.has("uid") && ownerObject.opt("uid") != null) { + ownerUid = ownerObject.opt("uid").toString(); + } + JSONObject owner = jsonObject.optJSONObject("_owner"); + Iterator iterator = owner.keys(); + ownerMap = new WeakHashMap<>(); + while (iterator.hasNext()) { + String key = iterator.next(); + ownerMap.put(key, owner.optString(key)); + } + } + + tagsArray = (JSONArray) jsonObject.opt("tags"); + if (tagsArray != null && tagsArray.length() > 0) { + int count = tagsArray.length(); + tags = new String[count]; + for (int i = 0; i < count; i++) { + tags[i] = (String) tagsArray.opt(i); + } + } + + + } catch (Exception e) { + CSAppUtils.showLog("EntryModel", e.getLocalizedMessage()); + } + + } +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/EntryResultCallBack.java b/contentstack/src/main/java/com/contentstack/sdk/EntryResultCallBack.java new file mode 100755 index 00000000..1575ed2d --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/EntryResultCallBack.java @@ -0,0 +1,29 @@ +package com.contentstack.sdk; + +/** + * @author Contentstack.com, Inc + */ +public abstract class EntryResultCallBack extends ResultCallBack { + + /** + * Triggered after call execution complete. + * + * @param responseType call response from cache or network. + * @param error {@link Error} instance if call failed else null. + */ + public abstract void onCompletion(ResponseType responseType, Error error); + + void onRequestFinish(ResponseType responseType) { + onCompletion(responseType, null); + } + + @Override + void onRequestFail(ResponseType responseType, Error error) { + onCompletion(responseType, error); + + } + + @Override + void always() { + } +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/Error.java b/contentstack/src/main/java/com/contentstack/sdk/Error.java new file mode 100755 index 00000000..9c73990a --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/Error.java @@ -0,0 +1,66 @@ +package com.contentstack.sdk; + +import java.util.HashMap; + +/** + * To retrieve information related to network call failure. + * + * @author Contentstack.com, Inc + */ +public class Error { + + String errorMessage = null; + int errorCode = 0; + HashMap errorHashMap = new HashMap<>(); + + /** + * Returns error in string format. + * + *

Example :
+ *
+     *  String errorString = error.getErrorMessage();
+     *  
+ */ + public String getErrorMessage() { + return errorMessage; + } + + protected void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + /** + * Returns error code. + * + * @return int value. + * + *

Example :
+ *
+     *  int errorCode = error.getErrorCode();
+     *  
+ */ + public int getErrorCode() { + return errorCode; + } + + protected void setErrorCode(int errorCode) { + this.errorCode = errorCode; + } + + /** + * Returns error in {@linkplain HashMap} format where error is key and its respective information as HashMap's value. + * + *

Example :
+ *
+     *  HashMap<String, Object> errorHashMap = error.getErrors();
+     * 
+ */ + public HashMap getErrors() { + return errorHashMap; + } + + protected void setErrors(HashMap errorHashMap) { + this.errorHashMap = errorHashMap; + } + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/FetchAssetsCallback.java b/contentstack/src/main/java/com/contentstack/sdk/FetchAssetsCallback.java new file mode 100644 index 00000000..19054a6c --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/FetchAssetsCallback.java @@ -0,0 +1,26 @@ +package com.contentstack.sdk; + +import java.util.List; + +/** + * @author Contentstack.com, Inc + */ + +public abstract class FetchAssetsCallback extends ResultCallBack { + + public abstract void onCompletion(ResponseType responseType, List assets, Error error); + + public void onRequestFinish(ResponseType responseType, List assets) { + onCompletion(responseType, assets, null); + } + + @Override + void onRequestFail(ResponseType responseType, Error error) { + onCompletion(responseType, null, error); + } + + @Override + void always() { + + } +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/FetchResultCallback.java b/contentstack/src/main/java/com/contentstack/sdk/FetchResultCallback.java new file mode 100644 index 00000000..da5cc7c4 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/FetchResultCallback.java @@ -0,0 +1,30 @@ +package com.contentstack.sdk; + +/** + * @author Contentstack.com, Inc + */ + +public abstract class FetchResultCallback extends ResultCallBack { + + /** + * Triggered after call execution complete. + * + * @param responseType call response from cache or network. + * @param error {@link Error} instance if call failed else null. + */ + public abstract void onCompletion(ResponseType responseType, Error error); + + void onRequestFinish(ResponseType responseType) { + onCompletion(responseType, null); + } + + @Override + void onRequestFail(ResponseType responseType, Error error) { + onCompletion(responseType, error); + } + + @Override + void always() { + } + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/Group.java b/contentstack/src/main/java/com/contentstack/sdk/Group.java new file mode 100644 index 00000000..51f8edd3 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/Group.java @@ -0,0 +1,500 @@ +package com.contentstack.sdk; + +import android.text.TextUtils; + +import com.contentstack.sdk.utilities.CSAppUtils; +import com.contentstack.sdk.utilities.ContentstackUtil; +import com.contentstack.txtmark.Configuration; +import com.contentstack.txtmark.Processor; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; + +/*** + * Group class is represent group field uid value. + * + * @author Contentstack.com, Inc + * + */ +public class Group { + + private static final String TAG = "Group"; + private JSONObject resultJson; + private Stack stackInstance; + + protected Group(Stack stack, JSONObject jsonObject) { + resultJson = jsonObject; + stackInstance = stack; + } + + /** + * Get group representation in json + * + *

+ *

Example :
+ *

+     * JSONObject json = group.toJSON();
+     * 
+ *

+ */ + public JSONObject toJSON() { + return resultJson; + } + + /** + * Get object value for key. + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            Object obj = group.get("key");
+     *            
+ */ + public Object get(String key) { + try { + if (resultJson != null && key != null) { + return resultJson.get(key); + } else { + return null; + } + } catch (Exception e) { + CSAppUtils.showLog(TAG, "-----------------get|" + e); + return null; + } + } + + /** + * Get html text for markdown data type + * + * @param markdownKey field_uid as key. + * @return html text in string format. + * + *

Example :
+ *
+     * String htmlText = group.getHtmlText("markdownKey");
+     * 
+ */ + public String getHtmlText(String markdownKey) { + try { + return Processor.process(getString(markdownKey), Configuration.builder().forceExtentedProfile().build()); + } catch (Exception e) { + CSAppUtils.showLog(TAG, "-----------------getHtmlText|" + e); + return null; + } + } + + /** + * Get html text for markdown data type which is multiple true + * + * @param markdownKey field_uid as key. + * @return html text in string format. + * + *

Example :
+ *
+     * ArrayList<String> htmlTexts = group.getMultipleHtmlText("markdownKey");
+     * 
+ */ + public ArrayList getMultipleHtmlText(String markdownKey) { + try { + ArrayList multipleHtmlStrings = new ArrayList<>(); + JSONArray jsonArray = getJSONArray(markdownKey); + + for (int i = 0; i < jsonArray.length(); i++) { + multipleHtmlStrings.add(Processor.process(jsonArray.getString(i), Configuration.builder().forceExtentedProfile().build())); + } + + return multipleHtmlStrings; + } catch (Exception e) { + CSAppUtils.showLog(TAG, e.getLocalizedMessage()); + return null; + } + } + + /** + * Get string value for key. + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            String value = group.getString("key");
+     *            
+ */ + public String getString(String key) { + Object value = get(key); + if (value != null) { + if (value instanceof String) { + return (String) value; + } + } + return null; + } + + /** + * Get boolean value for key. + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            Boolean value = group.getBoolean("key");
+     *            
+ */ + public Boolean getBoolean(String key) { + Object value = get(key); + if (value != null) { + if (value instanceof Boolean) { + return (Boolean) value; + } + } + return false; + } + + /** + * Get {@link JSONArray} value for key + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            JSONArray value = group.getJSONArray("key");
+     *            
+ */ + public JSONArray getJSONArray(String key) { + Object value = get(key); + if (value != null) { + if (value instanceof JSONArray) { + return (JSONArray) value; + } + } + return null; + } + + /** + * Get {@link JSONObject} value for key + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            JSONObject value = group.getJSONObject("key");
+     *            
+ */ + public JSONObject getJSONObject(String key) { + Object value = get(key); + if (value != null) { + if (value instanceof JSONObject) { + return (JSONObject) value; + } + } + return null; + } + + /** + * Get {@link JSONObject} value for key + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            JSONObject value = group.getJSONObject("key");
+     *            
+ */ + public Number getNumber(String key) { + Object value = get(key); + if (value != null) { + if (value instanceof Number) { + return (Number) value; + } + } + return null; + } + + /** + * Get integer value for key + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            int value = group.getInt("key");
+     *            
+ */ + public int getInt(String key) { + Number value = getNumber(key); + if (value != null) { + return value.intValue(); + } + return 0; + } + + /** + * Get integer value for key + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            float value = group.getFloat("key");
+     *            
+ */ + public float getFloat(String key) { + Number value = getNumber(key); + if (value != null) { + return value.floatValue(); + } + return (float) 0; + } + + /** + * Get double value for key + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            double value = group.getDouble("key");
+     *            
+ */ + public double getDouble(String key) { + Number value = getNumber(key); + if (value != null) { + return value.doubleValue(); + } + return (double) 0; + } + + /** + * Get long value for key + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            long value = group.getLong("key");
+     *            
+ */ + public long getLong(String key) { + Number value = getNumber(key); + if (value != null) { + return value.longValue(); + } + return (long) 0; + } + + /** + * Get short value for key + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            short value = group.getShort("key");
+     *            
+ */ + public short getShort(String key) { + Number value = getNumber(key); + if (value != null) { + return value.shortValue(); + } + return (short) 0; + } + + /** + * Get {@link Calendar} value for key + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            Calendar value = group.getDate("key");
+     *            
+ */ + public Calendar getDate(String key) { + + try { + String value = getString(key); + return ContentstackUtil.parseDate(value, null); + } catch (Exception e) { + CSAppUtils.showLog(TAG, "-----------------getDate|" + e); + } + return null; + } + + /** + * Get an asset from the group + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            Asset asset = group.getAsset("key");
+     *            
+ */ + public Asset getAsset(String key) { + + JSONObject assetObject = getJSONObject(key); + + return stackInstance.asset().configure(assetObject); + } + + /** + * Get an assets from the group. This works with multiple true fields + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            List asset = group.getAssets("key");
+     *            
+ */ + public List getAssets(String key) { + List assets = new ArrayList<>(); + JSONArray assetArray = getJSONArray(key); + + for (int i = 0; i < assetArray.length(); i++) { + + if (assetArray.opt(i) instanceof JSONObject) { + Asset asset = stackInstance.asset().configure(assetArray.optJSONObject(i)); + assets.add(asset); + } + + } + return assets; + } + + + /** + * Get a group from the group. + * + * @param key field_uid as key. + * + *

Example :
+ *
+     *            Group innerGroup = group.getGroup("key");
+     *            
+ */ + public Group getGroup(String key) { + + if (!TextUtils.isEmpty(key) && resultJson.has(key) && resultJson.opt(key) instanceof JSONObject) { + return new Group(stackInstance, resultJson.optJSONObject(key)); + } + return null; + } + + /** + * Get a list of group from the group. + * + *

+ * Note :- This will work when group is multiple true. + * + * @param key field_uid as key. + * + *

Example :
+ *

+     *            Group innerGroup = group.getGroups("key");
+     *            
+ */ + public List getGroups(String key) { + + if (!TextUtils.isEmpty(key) && resultJson.has(key) && resultJson.opt(key) instanceof JSONArray) { + JSONArray array = resultJson.optJSONArray(key); + List groupList = new ArrayList<>(); + + for (int i = 0; i < array.length(); i++) { + if (array.opt(i) instanceof JSONObject) { + Group group = new Group(stackInstance, array.optJSONObject(i)); + groupList.add(group); + } + } + + return groupList; + } + return null; + } + + /** + * Get value for the given reference key. + * + * @param refKey key of a reference field. + * @param refContentType class uid. + * @return {@link ArrayList} of {@link Entry} instances. + * Also specified contentType value will be set as class uid for all {@link Entry} instance. + * + * + *

Example :
+ *
+     *  Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "stag", false);
+     *  Query csQuery = stack.contentType("contentType_name").query();
+     *
+     * csQuery.includeReference("for_bug");
+     *
+     * csQuery.find(new QueryResultsCallBack() {
+ * @Override + * public void onCompletion(ResponseType responseType, QueryResult queryResult, Error error) {
+ * + * if(error == null){ + * List<Entry> list = builtqueryresult.getResultObjects(); + * for (int i = 0; i < list.queueSize(); i++) { + * Entry entry = list.get(i); + * Group group = entry.getGroup("fieldUid"); + * Entry taskEntry = entry.getAllEntries("for_task", "task"); + * } + * } + * + * } + * });
+ * + *
+ */ + public ArrayList getAllEntries(String refKey, String refContentType) { + try { + if (resultJson != null) { + + if (resultJson.get(refKey) instanceof JSONArray) { + + int count = ((JSONArray) resultJson.get(refKey)).length(); + ArrayList builtObjectList = new ArrayList(); + for (int i = 0; i < count; i++) { + + EntryModel model = new EntryModel(((JSONArray) resultJson.get(refKey)).getJSONObject(i), null, false, false, true); + Entry entryInstance = null; + try { + entryInstance = stackInstance.contentType(refContentType).entry(); + } catch (Exception e) { + entryInstance = new Entry(refContentType); + CSAppUtils.showLog("BuiltObject", e.getLocalizedMessage()); + } + entryInstance.setUid(model.entryUid); + entryInstance.ownerEmailId = model.ownerEmailId; + entryInstance.ownerUid = model.ownerUid; + if (model.ownerMap != null) { + entryInstance.owner = new HashMap<>(model.ownerMap); + } + entryInstance.resultJson = model.jsonObject; + entryInstance.setTags(model.tags); + + builtObjectList.add(entryInstance); + model = null; + } + + return builtObjectList; + + } + } + } catch (Exception e) { + CSAppUtils.showLog(TAG, e.getLocalizedMessage()); + return null; + } + + return null; + } + + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/INotifyClass.java b/contentstack/src/main/java/com/contentstack/sdk/INotifyClass.java new file mode 100755 index 00000000..8cd7230a --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/INotifyClass.java @@ -0,0 +1,18 @@ +package com.contentstack.sdk; + +import org.json.JSONObject; + +import java.util.List; + +/** + * To notify class which initiate network call when network call complete. + * + * @author Contentstack.com, Inc + */ +public interface INotifyClass { + + public void getResult(Object object, String controller); + + public void getResultObject(List object, JSONObject jsonObject, boolean isSingleEntry); + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/IRequestModelHTTP.java b/contentstack/src/main/java/com/contentstack/sdk/IRequestModelHTTP.java new file mode 100755 index 00000000..dc31dd09 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/IRequestModelHTTP.java @@ -0,0 +1,17 @@ +package com.contentstack.sdk; + +import org.json.JSONObject; + + +/** + * @author Contentstack.com, Inc + */ +public interface IRequestModelHTTP { + + public void sendRequest(); + + public void onRequestFailed(JSONObject error, int statusCode, ResultCallBack callBackObject); + + public void onRequestFinished(CSHttpConnection request); + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/IURLRequestHTTP.java b/contentstack/src/main/java/com/contentstack/sdk/IURLRequestHTTP.java new file mode 100755 index 00000000..e0e642db --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/IURLRequestHTTP.java @@ -0,0 +1,43 @@ +package com.contentstack.sdk; + +import android.util.ArrayMap; + +import com.contentstack.sdk.utilities.CSAppConstants; + +import org.json.JSONObject; + +/** + * @author Contentstack.com, Inc + */ +public interface IURLRequestHTTP { + + public void send(); + + public void setHeaders(ArrayMap headers); + + public ArrayMap getHeaders(); + + public void setRequestMethod(CSAppConstants.RequestMethod requestMethod); + + public CSAppConstants.RequestMethod getRequestMethod(); + + public JSONObject getResponse(); + + public void setInfo(String info); + + public String getInfo(); + + public void setController(String controller); + + public String getController(); + + public void setCallBackObject(ResultCallBack builtResultCallBackObject); + + public ResultCallBack getCallBackObject(); + + public void setTreatDuplicateKeysAsArrayItems(boolean treatDuplicateKeysAsArrayItems); + + public boolean getTreatDuplicateKeysAsArrayItems(); + + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/JSONUTF8Request.java b/contentstack/src/main/java/com/contentstack/sdk/JSONUTF8Request.java new file mode 100755 index 00000000..d9bc6913 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/JSONUTF8Request.java @@ -0,0 +1,34 @@ +package com.contentstack.sdk; + +import com.android.volley.NetworkResponse; +import com.android.volley.ParseError; +import com.android.volley.Response; +import com.android.volley.toolbox.HttpHeaderParser; +import com.android.volley.toolbox.JsonObjectRequest; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.UnsupportedEncodingException; + + +class JSONUTF8Request extends JsonObjectRequest { + + protected JSONUTF8Request(int method, String url, JSONObject jsonRequest, Response.Listener listener, Response.ErrorListener errorListener) { + super(method, url, jsonRequest, listener, errorListener); + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + try { + String jsonString = new String(response.data, "UTF-8"); + return Response.success(new JSONObject(jsonString), HttpHeaderParser.parseCacheHeaders(response)); + } catch (UnsupportedEncodingException e) { + return Response.error(new ParseError(e)); + } catch (JSONException je) { + return Response.error(new ParseError(je)); + } + } + + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/Language.java b/contentstack/src/main/java/com/contentstack/sdk/Language.java new file mode 100755 index 00000000..ab442283 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/Language.java @@ -0,0 +1,284 @@ +package com.contentstack.sdk; + +/** + * Helper enum for pass language. + * Differentiated using languages and countries. + * + * @author Contentstack.com, Inc + */ +public enum Language { + + AFRIKAANS_SOUTH_AFRICA, + + ALBANIAN_ALBANIA, + + ARABIC_ALGERIA, + + ARABIC_BAHRAIN, + + ARABIC_EGYPT, + + ARABIC_IRAQ, + + ARABIC_JORDAN, + + ARABIC_KUWAIT, + + ARABIC_LEBANON, + + ARABIC_LIBYA, + + ARABIC_MOROCCO, + + ARABIC_OMAN, + + ARABIC_QATAR, + + ARABIC_SAUDI_ARABIA, + + ARABIC_SYRIA, + + ARABIC_TUNISIA, + + ARABIC_UNITED_ARAB_EMIRATES, + + ARABIC_YEMEN, + + ARMENIAN_ARMENIA, + + AZERI_CYRILLIC_ARMENIA, + + AZERI_LATIN_AZERBAIJAN, + + BASQUE_BASQUE, + + BELARUSIAN_BELARUS, + + BULGARIAN_BULGARIA, + + CATALAN_CATALAN, + + CHINESE_CHINA, + + CHINESE_HONG_KONG_SAR, + + CHINESE_MACUS_SAR, + + CHINESE_SINGAPORE, + + CHINESE_TAIWAN, + + CHINESE_SIMPLIFIED, + + CHINESE_TRADITIONAL, + + CROATIAN_CROATIA, + + CZECH_CZECH_REPUBLIC, + + DANISH_DENMARK, + + DHIVEHI_MALDIVES, + + DUTCH_BELGIUM, + + DUTCH_NETHERLANDS, + + ENGLISH_AUSTRALIA, + + ENGLISH_BELIZE, + + ENGLISH_CANADA, + + ENGLISH_CARIBBEAN, + + ENGLISH_IRELAND, + + ENGLISH_JAMAICA, + + ENGLISH_NEW_ZEALAND, + + ENGLISH_PHILIPPINES, + + ENGLISH_SOUTH_AFRICA, + + ENGLISH_TRINIDAD_AND_TOBAGO, + + ENGLISH_UNITED_KINGDOM, + + ENGLISH_UNITED_STATES, + + ENGLISH_ZIMBABWE, + + ESTONIAN_ESTONIA, + + FAROESE_FAROE_ISLANDS, + + FARSI_IRAN, + + FINNISH_FINLAND, + + FRENCH_BELGIUM, + + FRENCH_CANADA, + + FRENCH_FRANCE, + + FRENCH_LUXEMBOURG, + + FRENCH_MONACO, + + FRENCH_SWITZERLAND, + + GALICIAN_GALICIAN, + + GEORGIAN_GEORGIA, + + GERMEN_AUSTRIA, + + GERMEN_GERMANY, + + GERMEN_LIENCHTENSTEIN, + + GERMEN_LUXEMBOURG, + + GERMEN_SWITZERLAND, + + GREEK_GREECE, + + GUJARATI_INDIA, + + HEBREW_ISRAEL, + + HINDI_INDIA, + + HUNGARIAN_HUNGARY, + + ICELANDIC_ICELAND, + + INDONESIAN_INDONESIA, + + ITALIAN_ITALY, + + ITALIAN_SWITZERLAND, + + JAPANESE_JAPAN, + + KANNADA_INDIA, + + KAZAKH_KAZAKHSTAN, + + KONKANI_INDIA, + + KOREAN_KOREA, + + KYRGYZ_KAZAKHSTAN, + + LATVIAN_LATVIA, + + LITHUANIAN_LITHUANIA, + + MACEDONIAN_FYROM, + + MALAY_BRUNEI, + + MALAY_MALAYSIA, + + MARATHI_INDIA, + + MONGOLIAN_MONGOLIA, + + NORWEGIAN_BOKMAL_NORWAY, + + NORWEGIAN_NYNORSK_NORWAY, + + POLISH_POLAND, + + PORTUGUESE_BRAZIL, + + PORTUGUESE_PORTUGAL, + + PUNJABI_INDIA, + + ROMANIAN_ROMANIA, + + RUSSIAN_RUSSIA, + + SANSKRIT_INDIA, + + SERBIAN_CYRILLIC_SERBIA, + + SERBIAN_LATIN_SERBIA, + + SLOVAK_SLOVAKIA, + + SLOVENIAN_SLOVENIAN, + + SPANISH_ARGENTINA, + + SPANISH_BOLIVIA, + + SPANISH_CHILE, + + SPANISH_COLOMBIA, + + SPANISH_COSTA_RICA, + + SPANISH_DOMINICAN_REPUBLIC, + + SPANISH_ECUADOR, + + SPANISH_ELSALVADOR, + + SPANISH_GUATEMALA, + + SPANISH_HONDURAS, + + SPANISH_MEXICO, + + SPANISH_NICARAGUA, + + SPANISH_PANAMA, + + SPANISH_PARAGUAY, + + SPANISH_PERU, + + SPANISH_PUERTO_RICO, + + SPANISH_SPAIN, + + SPANISH_URUGUAY, + + SPANISH_VENEZUELA, + + SWAHILI_KENYA, + + SWEDISH_FINLAND, + + SWEDISH_SWEDEN, + + SYRIAC_SYRIA, + + TAMIL_INDIA, + + TATAR_RUSSIA, + + TELUGU_INDIA, + + THAI_THAILAND, + + TURKISH_TURKEY, + + UKRAINIAN_UKRAINE, + + URDU_PAKISTAN, + + UZBEK_CYRILLIC_UZBEKISTAN, + + UZBEK_LATIN_UZEBEKISTAN, + + VIETNAMESE_VIETNAM; + +} + diff --git a/contentstack/src/main/java/com/contentstack/sdk/LanguageCode.java b/contentstack/src/main/java/com/contentstack/sdk/LanguageCode.java new file mode 100755 index 00000000..a664cefb --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/LanguageCode.java @@ -0,0 +1,282 @@ +package com.contentstack.sdk; + +/** + * Helper enum for pass language code. + * + * @author Contentstack.com, Inc + */ +enum LanguageCode { + + af_za, + + sq_al, + + ar_dz, + + ar_bh, + + ar_eg, + + ar_iq, + + ar_jo, + + ar_kw, + + ar_lb, + + ar_ly, + + ar_ma, + + ar_om, + + ar_qa, + + ar_sa, + + ar_sy, + + ar_tn, + + ar_ae, + + ar_ye, + + hy_am, + + cy_az_az, + + lt_az_az, + + eu_es, + + be_by, + + bg_bg, + + ca_es, + + zh_cn, + + zh_hk, + + zh_mo, + + zh_sg, + + zh_tw, + + zh_chs, + + zh_cht, + + hr_hr, + + cs_cz, + + da_dk, + + div_mv, + + nl_be, + + nl_nl, + + en_au, + + en_bz, + + en_ca, + + en_cb, + + en_ie, + + en_jm, + + en_nz, + + en_ph, + + en_za, + + en_tt, + + en_gb, + + en_us, + + en_zw, + + et_ee, + + fo_fo, + + fa_ir, + + fi_fi, + + fr_be, + + fr_ca, + + fr_fr, + + fr_lu, + + fr_mc, + + fr_ch, + + gl_es, + + ka_ge, + + de_at, + + de_de, + + de_li, + + de_lu, + + de_ch, + + el_gr, + + gu_in, + + he_il, + + hi_in, + + hu_hu, + + is_is, + + id_id, + + it_it, + + it_ch, + + ja_jp, + + kn_in, + + kk_kz, + + kok_in, + + ko_kr, + + ky_kz, + + lv_lv, + + lt_lt, + + mk_mk, + + ms_bn, + + ms_my, + + mr_in, + + mn_mn, + + nb_no, + + nn_no, + + pl_pl, + + pt_br, + + pt_pt, + + pa_in, + + ro_ro, + + ru_ru, + + sa_in, + + cy_sr_sp, + + lt_sr_sp, + + sk_sk, + + sl_si, + + es_ar, + + es_bo, + + es_cl, + + es_co, + + es_cr, + + es_do, + + es_ec, + + es_sv, + + es_gt, + + es_hn, + + es_mx, + + es_ni, + + es_pa, + + es_py, + + es_pe, + + es_pr, + + es_es, + + es_uy, + + es_ve, + + sw_ke, + + sv_fi, + + sv_se, + + syr_sy, + + ta_in, + + tt_ru, + + te_in, + + th_th, + + tr_tr, + + uk_ua, + + ur_pk, + + cy_uz_uz, + + lt_uz_uz, + + vi_vn; + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/Query.java b/contentstack/src/main/java/com/contentstack/sdk/Query.java new file mode 100755 index 00000000..a1b2d125 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/Query.java @@ -0,0 +1,2046 @@ +package com.contentstack.sdk; + +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; + +import com.contentstack.sdk.utilities.CSAppConstants; +import com.contentstack.sdk.utilities.CSAppUtils; +import com.contentstack.sdk.utilities.CSController; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * A class that defines a query that is used to query for {@link Entry} instance. + * + * @author Contentstack + */ +public class Query implements INotifyClass { + + private static final String TAG = "Query"; + protected JSONObject mainJSON; + private String formName; + protected ContentType contentTypeInstance = null; + private JSONObject urlQueries = null; + private ArrayMap localHeader = null; + protected ArrayMap formHeader = null; + + private JSONObject queryValueJSON = null; + private JSONObject queryValue = null; + private JSONArray objectUidForInclude = null; + private JSONArray objectUidForExcept = null; + private JSONArray objectUidForOnly = null; + private boolean isJsonProper = true; + // cache policy + private long maxCacheTimeForCall = 86400000; + private long defaultCacheTimeInterval = 0; + private CachePolicy cachePolicyForCall = null; + private QueryResultsCallBack queryResultCallback; + private SingleQueryResultCallback singleQueryResultCallback; + private String errorString; + private HashMap errorHashMap; + private JSONObject onlyJsonObject; + private JSONObject exceptJsonObject; + + protected Query(String formName) { + this.formName = formName; + this.localHeader = new ArrayMap<>(); + this.urlQueries = new JSONObject(); + this.queryValue = new JSONObject(); + this.queryValueJSON = new JSONObject(); + this.mainJSON = new JSONObject(); + } + + protected void setContentTypeInstance(ContentType contentTypeInstance) { + this.contentTypeInstance = contentTypeInstance; + } + + + /** + * To set headers for Contentstack rest calls. + *
+ * Scope is limited to this object and followed classes. + * + * @param key header name. + * @param value header value against given header name. + * + *

Example :
+ *
+     *               Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *               Query csQuery = stack.contentType("contentType_name").query();
+ * csQuery.setHeader("custom_key", "custom_value"); + *
+ */ + public void setHeader(String key, String value) { + if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(value)) { + localHeader.put(key, value); + } + } + + /** + * Remove header key. + * + * @param key custom_header_key + * + *

Example :
+ *
+     *             Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *             Query csQuery = stack.contentType("contentType_name").query();
+ * csQuery.removeHeader("custom_key"); + *
+ */ + public void removeHeader(String key) { + if (!TextUtils.isEmpty(key)) { + localHeader.remove(key); + } + } + + public String getContentType() { + return contentTypeInstance.contentTypeName; + } + + /** + * Add a constraint to fetch all entries that contains given value against specified key. + * + * @param key field uid. + * @param value field value which get 'included' from the response. + * @return {@link Query} object, so you can chain this call. + * + *

+ * Note : for group field provide key in a "key.groupFieldUid" format. + * + *

Example :
+ *

+     *    Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *    Query csQuery = stack.contentType("contentType_name").query();
+     *
+     *    csQuery.where("uid", "your_entry_uid");
+     * 
+ */ + public Query where(String key, Object value) { + try { + if (key != null && value != null) { + queryValueJSON.put(key, value); + } else { + throwException("where", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + } catch (Exception e) { + throwException("where", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + + return this; + } + + /** + * Add a custom query against specified key. + * + * @param key key. + * @param value value. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *    Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *    Query csQuery = stack.contentType("contentType_name").query();
+     *
+     *    csQuery.addQuery("query_param_key", "query_param_value");
+     * 
+ */ + public Query addQuery(String key, String value) { + try { + if (key != null && value != null) { + + urlQueries.put(key, value); + } else { + throwException("and", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + } catch (Exception e) { + throwException("and", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + return this; + } + + /** + * Remove provided query key from custom query if exist. + * + * @param key Query name to remove. + * @return {@linkplain Query} object, so you can chain this call. + * + *

Example :
+ *
+     *      projectQuery.removeQuery("Query_Key");
+     * 
+ */ + public Query removeQuery(String key) { + try { + if (urlQueries.has(key)) { + urlQueries.remove(key); + } + } catch (Exception e) { + throwException("and", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + return this; + } + + /** + * Combines all the queries together using AND operator + * + * @param queryObjects list of {@link Query} instances on which AND query executes. + * @return {@link Query} object, so you can chain this call. + * + *

Example ;
+ *
+     *    Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *    Query csQuery = stack.contentType("contentType_name").query();
+     *
+     *    Query query = projectClass.query();
+     *    query.where('username','something');
+     *
+     *    Query subQuery = projectClass.query();
+     *    subQuery.where('email_address','something@email.com');
+     *
+     *    ArrayList<Query> array = new ArrayList<Query>();
+ * array.add(query); + * array.add(subQuery);
+ * projectQuery.and(array); + *
+ */ + public Query and(ArrayList queryObjects) { + if (queryObjects != null && queryObjects.size() > 0) { + try { + JSONArray orValueJson = new JSONArray(); + int count = queryObjects.size(); + + for (int i = 0; i < count; i++) { + orValueJson.put(queryObjects.get(i).queryValueJSON); + } + queryValueJSON.put("$and", orValueJson); + + } catch (Exception e) { + throwException("and", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } else { + throwException("and", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + + return this; + } + + /** + * Add a constraint to fetch all entries which satisfy any queries. + * + * @param queryObjects list of {@link Query} instances on which OR query executes. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *     Query csQuery = stack.contentType("contentType_name").query();
+     *
+     *     Query query = projectClass.query();
+     *     query.where('username','something');
+     *
+     *     Query subQuery = projectClass.query();
+     *     subQuery.where('email_address','something@email.com');
+     *
+     *     ArrayList<Query> array = new ArrayList<Query>();
+     *     array.add(query);
+     *     array.add(subQuery);
+ * csQuery.or(array); + *
+ */ + public Query or(ArrayList queryObjects) { + if (queryObjects != null && queryObjects.size() > 0) { + try { + JSONArray orValueJson = new JSONArray(); + int count = queryObjects.size(); + + for (int i = 0; i < count; i++) { + orValueJson.put(queryObjects.get(i).queryValueJSON); + } + + queryValueJSON.put("$or", orValueJson); + + } catch (Exception e) { + throwException("or", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } else { + throwException("or", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + + return this; + } + + /** + * Add a constraint to the query that requires a particular key entry to be less than the provided value. + * + * @param key the key to be constrained. + * @param value the value that provides an upper bound. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *     Query csQuery = stack.contentType("contentType_name").query();
+     *
+     *     csQuery.lessThan("due_date", "2013-06-25T00:00:00+05:30");
+     * 
+ */ + public Query lessThan(String key, Object value) { + if (key != null && value != null) { + try { + if (queryValueJSON.isNull(key)) { + + if (queryValue.length() > 0) { + queryValue = new JSONObject(); + } + + queryValue.put("$lt", value); + queryValueJSON.put(key, queryValue); + + } else if (queryValueJSON.has(key)) { + + queryValue.put("$lt", value); + queryValueJSON.put(key, queryValue); + + } + } catch (Exception e) { + throwException("lessThan", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } else { + throwException("lessThan", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + + return this; + } + + /** + * Add a constraint to the query that requires a particular key entry to be less than or equal to the provided value. + * + * @param key The key to be constrained + * @param value The value that must be equalled. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *     Query csQuery = stack.contentType("contentType_name").query();
+     *     csQuery.lessThanOrEqualTo("due_date", "2013-06-25T00:00:00+05:30");
+     * 
+ */ + public Query lessThanOrEqualTo(String key, Object value) { + + if (key != null && value != null) { + + try { + if (queryValueJSON.isNull(key)) { + + if (queryValue.length() > 0) { + queryValue = new JSONObject(); + } + + queryValue.put("$lte", value); + queryValueJSON.put(key, queryValue); + + } else if (queryValueJSON.has(key)) { + + queryValue.put("$lte", value); + queryValueJSON.put(key, queryValue); + + } + } catch (Exception e) { + throwException("lessThanOrEqualTo", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } else { + throwException("lessThanOrEqualTo", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + return this; + } + + /** + * Add a constraint to the query that requires a particular key entry to be greater than the provided value. + * + * @param key The key to be constrained. + * @param value The value that provides an lower bound. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *     Query csQuery = stack.contentType("contentType_name").query();
+     *     csQuery.greaterThan("due_date", "2013-06-25T00:00:00+05:30");
+     * 
+ */ + public Query greaterThan(String key, Object value) { + + if (key != null && value != null) { + try { + if (queryValueJSON.isNull(key)) { + + if (queryValue.length() > 0) { + queryValue = new JSONObject(); + } + + queryValue.put("$gt", value); + queryValueJSON.put(key, queryValue); + + } else if (queryValueJSON.has(key)) { + + queryValue.put("$gt", value); + queryValueJSON.put(key, queryValue); + + } + } catch (Exception e) { + throwException("greaterThan", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } else { + throwException("greaterThan", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + + return this; + } + + /** + * Add a constraint to the query that requires a particular key entry to be greater than or equal to the provided value. + * + * @param key The key to be constrained. + * @param value The value that provides an lower bound. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *     Query csQuery = stack.contentType("contentType_name").query();
+     *     csQuery.greaterThanOrEqualTo("due_date", "2013-06-25T00:00:00+05:30");
+     * 
+ */ + public Query greaterThanOrEqualTo(String key, Object value) { + + if (key != null && value != null) { + try { + if (queryValueJSON.isNull(key)) { + + if (queryValue.length() > 0) { + queryValue = new JSONObject(); + } + + queryValue.put("$gte", value); + queryValueJSON.put(key, queryValue); + + } else if (queryValueJSON.has(key)) { + + queryValue.put("$gte", value); + queryValueJSON.put(key, queryValue); + + } + } catch (Exception e) { + throwException("greaterThanOrEqualTo", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } else { + throwException("greaterThanOrEqualTo", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + + return this; + + } + + /** + * Add a constraint to the query that requires a particular key's entry to be not equal to the provided value. + * + * @param key The key to be constrained. + * @param value The object that must not be equaled. + * @return {@link Query} object, so you can chain this call. + * + *

Example ;
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *     Query csQuery = stack.contentType("contentType_name").query();
+     *     csQuery.notEqualTo("due_date", "2013-06-25T00:00:00+05:30");
+     * 
+ */ + public Query notEqualTo(String key, Object value) { + + if (key != null && value != null) { + + try { + if (queryValueJSON.isNull(key)) { + + if (queryValue.length() > 0) { + queryValue = new JSONObject(); + } + + queryValue.put("$ne", value); + queryValueJSON.put(key, queryValue); + + } else if (queryValueJSON.has(key)) { + + queryValue.put("$ne", value); + queryValueJSON.put(key, queryValue); + + } + } catch (Exception e) { + throwException("notEqualTo", null, e); + } + + } else { + throwException("notEqualTo", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + + return this; + + } + + /** + * Add a constraint to the query that requires a particular key's entry to be contained in the provided array. + * + * @param key The key to be constrained. + * @param values The possible values for the key's object. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *     Query csQuery = stack.contentType("contentType_name").query();
+     *
+     *     csQuery.containedIn("severity", new Object[]{"Show Stopper", "Critical"});
+     * 
+ */ + public Query containedIn(String key, Object[] values) { + + if (key != null && values != null) { + try { + JSONArray valuesArray = new JSONArray(); + int length = values.length; + for (int i = 0; i < length; i++) { + valuesArray.put(values[i]); + } + if (queryValueJSON.isNull(key)) { + if (queryValue.length() > 0) { + queryValue = new JSONObject(); + } + queryValue.put("$in", valuesArray); + queryValueJSON.put(key, queryValue); + } else if (queryValueJSON.has(key)) { + + queryValue.put("$in", valuesArray); + queryValueJSON.put(key, queryValue); + + } + } catch (Exception e) { + throwException("containedIn", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } else { + throwException("containedIn", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + + return this; + } + + /** + * Add a constraint to the query that requires a particular key entry's value not be contained in the provided array. + * + * @param key The key to be constrained. + * @param values The list of values the key object should not be. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *     Query csQuery = stack.contentType("contentType_name").query();
+     *     csQuery.notContainedIn("severity", new Object[]{"Show Stopper", "Critical"});
+     * 
+ */ + public Query notContainedIn(String key, Object[] values) { + + if (key != null && values != null) { + try { + JSONArray valuesArray = new JSONArray(); + int length = values.length; + for (int i = 0; i < length; i++) { + valuesArray.put(values[i]); + } + if (queryValueJSON.isNull(key)) { + if (queryValue.length() > 0) { + queryValue = new JSONObject(); + } + queryValue.put("$nin", valuesArray); + queryValueJSON.put(key, queryValue); + } else if (queryValueJSON.has(key)) { + + queryValue.put("$nin", valuesArray); + queryValueJSON.put(key, queryValue); + + } + } catch (Exception e) { + throwException("containedIn", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } else { + throwException("containedIn", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + + return this; + + } + + /** + * Add a constraint that requires, a specified key exists in response. + * + * @param key The key to be constrained. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *     Query csQuery = stack.contentType("contentType_name").query();
+     *
+     *     csQuery.exists("status");
+     * 
+ */ + public Query exists(String key) { + + if (key != null) { + try { + + if (queryValueJSON.isNull(key)) { + if (queryValue.length() > 0) { + queryValue = new JSONObject(); + } + queryValue.put("$exists", true); + queryValueJSON.put(key, queryValue); + } else if (queryValueJSON.has(key)) { + + queryValue.put("$exists", true); + queryValueJSON.put(key, queryValue); + } + } catch (Exception e) { + throwException("exists", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } else { + throwException("exists", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + + return this; + } + + /** + * Add a constraint that requires, a specified key does not exists in response. + * + * @param key The key to be constrained. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *     Query csQuery = stack.contentType("contentType_name").query();
+     *     csQuery.notExists("status");
+     * 
+ */ + public Query notExists(String key) { + + if (key != null) { + try { + + if (queryValueJSON.isNull(key)) { + if (queryValue.length() > 0) { + queryValue = new JSONObject(); + } + queryValue.put("$exists", false); + queryValueJSON.put(key, queryValue); + } else if (queryValueJSON.has(key)) { + + queryValue.put("$exists", false); + queryValueJSON.put(key, queryValue); + } + } catch (Exception e) { + throwException("notExists", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } else { + throwException("notExists", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + + return this; + } + + /** + * Add a constraint that requires a particular reference key details. + * + * @param key key that to be constrained. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *     Query csQuery = stack.contentType("contentType_name").query();
+     *     csQuery.includeReference("for_bug");
+     * 
+ */ + public Query includeReference(String key) { + if (objectUidForInclude == null) { + objectUidForInclude = new JSONArray(); + } + + objectUidForInclude.put(key); + + return this; + } + + /** + * Add a constraint that requires a particular reference key details. + * + * @param keys array keys that to be constrained. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *     Query csQuery = stack.contentType("contentType_name").query();
+     *     csQuery.includeReference(new String[]{"for_bug"});
+     * 
+ */ + public Query includeReference(String[] keys) { + if (keys != null && keys.length > 0) { + + if (objectUidForInclude == null) { + objectUidForInclude = new JSONArray(); + } + + int count = keys.length; + for (int i = 0; i < count; i++) { + objectUidForInclude.put(keys[i]); + } + } else { + throwException("includeReference", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + return this; + } + + /** + * Include tags with which to search entries. + * + * @param tags Comma separated array of tags with which to search entries. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *     Query csQuery = stack.contentType("contentType_name").query();
+     *     csQuery.tags(new String[]{"tag1","tag2"});
+     * 
+ */ + public Query tags(String[] tags) { + try { + if (tags != null) { + + String tagsvalue = null; + int count = tags.length; + for (int i = 0; i < count; i++) { + tagsvalue = tagsvalue + "," + tags[i]; + } + urlQueries.put("tags", tagsvalue); + } else { + throwException("tags", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + } catch (Exception e) { + throwException("tags", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + return this; + } + + /** + * Sort the results in ascending order with the given key. + *
+ * Sort the returned entries in ascending order of the provided key. + * + * @param key The key to order by. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *     Query csQuery = stack.contentType("contentType_name").query();
+     *     csQuery.ascending("name");
+     * 
+ */ + public Query ascending(String key) { + if (key != null) { + try { + urlQueries.put("asc", key); + } catch (Exception e) { + throwException("ascending", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } else { + throwException("ascending", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + return this; + } + + /** + * Sort the results in descending order with the given key. + *
+ * Sort the returned entries in descending order of the provided key. + * + * @param key The key to order by. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *     Query csQuery = stack.contentType("contentType_name").query();
+     *     csQuery.descending("name");
+     * 
+ */ + public Query descending(String key) { + if (key != null) { + try { + urlQueries.put("desc", key); + } catch (Exception e) { + throwException("descending", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } else { + throwException("descending", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + return this; + } + + /** + * Specifies list of field uids that would be 'excluded' from the response. + * + * @param fieldUid field uid which get 'excluded' from the response. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *     Query csQuery = stack.contentType("contentType_name").query();
+ * ArrayList<String> array = new ArrayList<String>(); + * array.add("name"); + * array.add("description");
+ * csQuery.except(array); + *
+ */ + public Query except(ArrayList fieldUid) { + try { + if (fieldUid != null && fieldUid.size() > 0) { + if (objectUidForExcept == null) { + objectUidForExcept = new JSONArray(); + } + + int count = fieldUid.size(); + for (int i = 0; i < count; i++) { + objectUidForExcept.put(fieldUid.get(i)); + } + } else { + throwException("except", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + } catch (Exception e) { + throwException("except", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + return this; + } + + /** + * Specifies list of field uids that would be 'excluded' from the response. + * + * @param fieldUids field uid which get 'excluded' from the response. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *     Query csQuery = stack.contentType("contentType_name").query();
+ * csQuery.except(new String[]{"name", "description"}); + *
+ */ + public Query except(String[] fieldUids) { + try { + if (fieldUids != null && fieldUids.length > 0) { + if (objectUidForExcept == null) { + objectUidForExcept = new JSONArray(); + } + + int count = fieldUids.length; + for (int i = 0; i < count; i++) { + objectUidForExcept.put(fieldUids[i]); + } + } else { + throwException("except", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + } catch (Exception e) { + throwException("except", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + return this; + } + + /** + * Specifies an array of 'only' keys in BASE object that would be 'included' in the response. + * + * @param fieldUid Array of the 'only' reference keys to be included in response. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *     Query csQuery = stack.contentType("contentType_name").query();
+ * csQuery.only(new String[]{"name"}); + *
+ */ + public Query only(String[] fieldUid) { + try { + if (fieldUid != null && fieldUid.length > 0) { + if (objectUidForOnly == null) { + objectUidForOnly = new JSONArray(); + } + + int count = fieldUid.length; + for (int i = 0; i < count; i++) { + objectUidForOnly.put(fieldUid[i]); + } + } else { + throwException("only", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + } catch (Exception e) { + throwException("only", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + return this; + } + + /** + * Specifies an array of 'only' keys that would be 'included' in the response. + * + * @param fieldUid Array of the 'only' reference keys to be included in response. + * @param referenceFieldUid Key who has reference to some other class object. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *     Query csQuery = stack.contentType("contentType_name").query();
+ * ArrayList<String> array = new ArrayList<String>(); + * array.add("description"); + * array.add("name");
+ * csQuery.onlyWithReferenceUid(array, "for_bug"); + *
+ */ + public Query onlyWithReferenceUid(ArrayList fieldUid, String referenceFieldUid) { + try { + if (fieldUid != null && referenceFieldUid != null) { + if (onlyJsonObject == null) { + onlyJsonObject = new JSONObject(); + } + JSONArray fieldValueArray = new JSONArray(); + int count = fieldUid.size(); + for (int i = 0; i < count; i++) { + fieldValueArray.put(fieldUid.get(i)); + } + + onlyJsonObject.put(referenceFieldUid, fieldValueArray); + + if (objectUidForInclude == null) { + objectUidForInclude = new JSONArray(); + } + objectUidForInclude.put(referenceFieldUid); + + } else { + throwException("onlyWithReferenceUid", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + } catch (Exception e) { + throwException("onlyWithReferenceUid", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + return this; + } + + /** + * Specifies an array of 'except' keys that would be 'excluded' in the response. + * + * @param fieldUid Array of the 'except' reference keys to be excluded in response. + * @param referenceFieldUid Key who has reference to some other class object. + * @return {@link Query} object, so you can chain this call. + * + *

Example :
+ *
+     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
+     *     Query csQuery = stack.contentType("contentType_name").query();
+ * ArrayList<String> array = new ArrayList<String>(); + * array.add("description"); + * array.add("name");
+ * csQuery.exceptWithReferenceUid(array, "for_bug"); + *
+ */ + public Query exceptWithReferenceUid(ArrayList fieldUid, String referenceFieldUid) { + try { + if (fieldUid != null && referenceFieldUid != null) { + if (exceptJsonObject == null) { + exceptJsonObject = new JSONObject(); + } + JSONArray fieldValueArray = new JSONArray(); + int count = fieldUid.size(); + for (int i = 0; i < count; i++) { + fieldValueArray.put(fieldUid.get(i)); + } + + exceptJsonObject.put(referenceFieldUid, fieldValueArray); + + if (objectUidForInclude == null) { + objectUidForInclude = new JSONArray(); + } + objectUidForInclude.put(referenceFieldUid); + + } else { + throwException("exceptWithReferenceUid", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + } catch (Exception e) { + throwException("exceptWithReferenceUid", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + return this; + } + + /** + * Retrieve only count of entries in result. + * + * @return {@link Query} object, so you can chain this call. + * + * Note :- + *
  • Call {@link QueryResult#getCount()} method in the success to get count of objects.
  • + * + *

    Example :
    + *
    +     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
    +     *     Query csQuery = stack.contentType("contentType_name").query();
    + * csQuery.count(); + *
    + */ + public Query count() { + try { + urlQueries.put("count", "true"); + } catch (Exception e) { + throwException("count", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + return this; + } + + /** + * Retrieve count and data of objects in result. + * + * @return {@link Query} object, so you can chain this call. + * + * Note :- + *
  • Call {@link QueryResult#getCount()} method in the success to get count of objects.
  • + * + *

    Example :
    + *
    +     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
    +     *     Query csQuery = stack.contentType("contentType_name").query();
    + * csQuery.includeCount(); + *
    + */ + public Query includeCount() { + try { + urlQueries.put("include_count", "true"); + } catch (Exception e) { + throwException("includeCount", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + return this; + } + + + /** + * Include Content Type of all returned objects along with objects themselves. + * + * @return {@link Query} object, so you can chain this call. + *

    Example :
    + *
    +     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
    +     *     Query csQuery = stack.contentType("contentType_name").query();
    + * csQuery.includeContentType(); + *
    + */ + public Query includeContentType() { + try { + if (urlQueries.has("include_schema")) { + urlQueries.remove("include_schema"); + } + urlQueries.put("include_content_type", true); + urlQueries.put("include_global_field_schema", true); + } catch (Exception e) { + throwException("include_content_type", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + return this; + } + + + /** + * Include object owner's profile in the objects data. + * + * @return {@linkplain Query} object, so you can chain this call. + * + *

    Example :
    + *
    +     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
    +     *     Query csQuery = stack.contentType("contentType_name").query();
    + * csQuery.includeOwner(); + *
    + */ + public Query includeOwner() { + try { + urlQueries.put("include_owner", true); + } catch (Exception e) { + throwException("includeUser", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + return this; + } + + /** + * Fetches all the objects before specified uid. + * + * @param uid uid before which objects should be returned. + * @return {@link Query} object, so you can chain this call. + * + *

    Example :
    + *
    +     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
    +     *     Query csQuery = stack.contentType("contentType_name").query();
    + * csQuery.beforeUid("entryUid"); + *
    + */ + private Query beforeUid(String uid) { + if (uid != null) { + try { + urlQueries.put("before_uid", uid); + } catch (Exception e) { + throwException("beforeUid", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } else { + throwException("beforeUid", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + return this; + } + + /** + * Fetches all the objects after specified uid. + * + * @param uid uid after which objects should be returned. + * @return {@link Query} object, so you can chain this call. + * + *

    Example :
    + *
    +     *      Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
    +     *      Query csQuery = stack.contentType("contentType_name").query();
    + * csQuery.afterUid("entryUid"); + *
    + */ + private Query afterUid(String uid) { + if (uid != null) { + try { + urlQueries.put("after_uid", uid); + } catch (Exception e) { + throwException("afterUid", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } else { + throwException("afterUid", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + return this; + } + + /** + * The number of objects to skip before returning any. + * + * @param number No of objects to skip from returned objects. + * @return {@link Query} object, so you can chain this call. + * + *

    + * Note: The skip parameter can be used for pagination, "skip" specifies the number of objects to skip in the response. + * + *

    Example :
    + *

    +     *      Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
    +     *      Query csQuery = stack.contentType("contentType_name").query();
    + * csQuery.skip(2); + *
    + */ + public Query skip(int number) { + try { + urlQueries.put("skip", number); + } catch (Exception e) { + throwException("skip", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + return this; + } + + /** + * A limit on the number of objects to return. + * + * @param number No of objects to limit. + * @return {@link Query} object, so you can chain this call. + *

    + * Note: The limit parameter can be used for pagination, "limit" specifies the number of objects to limit to in the response. + * + *

    Example :
    + *

    +     *      Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
    +     *      Query csQuery = stack.contentType("contentType_name").query();
    + * csQuery.limit(2); + *
    + */ + public Query limit(int number) { + try { + urlQueries.put("limit", number); + } catch (Exception e) { + throwException("limit", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + return this; + } + + /** + * Add a regular expression constraint for finding string values that match the provided regular expression. + * This may be slow for large data sets. + * + * @param key The key to be constrained. + * @param regex The regular expression pattern to match. + * @return {@link Query} object, so you can chain this call. + * + *

    Example :
    + *
    +     *      
    +     *      Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
    +     *      Query csQuery = stack.contentType("contentType_name").query();
    + * csQuery.regex("name", "^browser"); + *
    + */ + public Query regex(String key, String regex) { + if (key != null && regex != null) { + + try { + if (queryValueJSON.isNull(key)) { + + if (queryValue.length() > 0) { + queryValue = new JSONObject(); + } + + queryValue.put("$regex", regex); + + queryValueJSON.put(key, queryValue); + + } else if (queryValueJSON.has(key)) { + + queryValue.put("$regex", regex); + + queryValueJSON.put(key, queryValue); + + } + + } catch (Exception e) { + throwException("matches", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } else { + throwException("matches", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + return this; + } + + + /** + * Add a regular expression constraint for finding string values that match the provided regular expression. + * This may be slow for large data sets. + * + * @param key The key to be constrained. + * @param regex The regular expression pattern to match. + * @param modifiers Any of the following supported Regular expression modifiers. + *
  • use i for case-insensitive matching.
  • + *
  • use m for making dot match newlines.
  • + *
  • use x for ignoring whitespace in regex
  • + * @return {@link Query} object, so you can chain this call. + * + *

    Example :
    + *
    +     *      Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
    +     *      Query csQuery = stack.contentType("contentType_name").query();
    + * csQuery.regex("name", "^browser", "i"); + *
    + */ + public Query regex(String key, String regex, String modifiers) { + if (key != null && regex != null) { + + try { + if (queryValueJSON.isNull(key)) { + + if (queryValue.length() > 0) { + queryValue = new JSONObject(); + } + + queryValue.put("$regex", regex); + + if (modifiers != null) { + queryValue.put("$options", modifiers); + } + queryValueJSON.put(key, queryValue); + + } else if (queryValueJSON.has(key)) { + + queryValue.put("$regex", regex); + + if (modifiers != null) { + queryValue.put("$options", modifiers); + } + queryValueJSON.put(key, queryValue); + + } + + } catch (Exception e) { + throwException("matches", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } else { + throwException("matches", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + return this; + } + + /** + * Set {@link Language} instance. + * + * @param language {@link Language} value + * @return {@link Query} object, so you can chain this call. + * + *

    Example :
    + *
    +     *      Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
    +     *      Query csQuery = stack.contentType("contentType_name").query();
    + * csQuery.language(Language.ENGLISH_UNITED_STATES); + *
    + */ + @Deprecated + public Query language(Language language) { + if (language != null) { + try { + Language languageName = Language.valueOf(language.name()); + int localeValue = languageName.ordinal(); + LanguageCode[] languageCodeValues = LanguageCode.values(); + String localeCode = languageCodeValues[localeValue].name(); + localeCode = localeCode.replace("_", "-"); + + if (urlQueries != null) { + urlQueries.put("locale", localeCode); + } + + } catch (Exception e) { + throwException("language", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } else { + throwException("language", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + + return this; + } + + + /** + * @param locale String value + * @return {@link Query} object, so you can chain this call. + * + *

    Example :
    + *
    +     *      Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
    +     *      Query csQuery = stack.contentType("contentType_name").query();
    + * csQuery.locale("en-hi"); + *
    + */ + public Query locale(String locale) { + + if (locale != null) { + try { + if (urlQueries != null) { + urlQueries.put("locale", locale); + } + + } catch (Exception e) { + throwException("locale", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } else { + throwException("locale", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + + return this; + } + + + /** + * This method provides only the entries matching the specified value. + * + * @param value value used to match or compare + * @return {@link Query} object, so you can chain this call. + * + *

    Example :
    + *
    +     *      Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
    +     *      Query csQuery = stack.contentType("contentType_name").query();
    + * csQuery.search("header"); + *
    + */ + public Query search(String value) { + if (value != null) { + try { + + if (urlQueries.isNull(value)) { + urlQueries.put("typeahead", value); + } + } catch (Exception e) { + throwException("value", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } else { + throwException("value", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + + return this; + } + + /** + * To set cache policy using {@link Query} instance. + * + * @param cachePolicy {@link CachePolicy} instance. + * @return {@link Query} object, so you can chain this call. + * + *

    Example :
    + *
    +     *      Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
    +     *      Query csQuery = stack.contentType("contentType_name").query();
    + * csQuery.setCachePolicy(NETWORK_ELSE_CACHE); + *
    + */ + public Query setCachePolicy(CachePolicy cachePolicy) { + this.cachePolicyForCall = cachePolicy; + + return this; + } + + /** + * Execute a Query and Caches its result (Optional) + * + * @param callback {@link QueryResultsCallBack} object to notify the application when the request has completed. + * @return {@linkplain Query} object, so you can chain this call. + * + *

    Example :
    + *
    +     *      Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
    +     *      Query csQuery = stack.contentType("contentType_name").query();
    + * csQuery.find(new QueryResultsCallBack() {
    + * @Override + * public void onCompletion(ResponseType responseType, QueryResult queryResult, Error error) {
    + * + * } + * });
    + *
    + */ + public Query find(QueryResultsCallBack callback) { + Error error = null; + try { + if (isJsonProper) { + + if (!TextUtils.isEmpty(formName)) { + + execQuery(null, callback, false); + } else { + throwException("find", CSAppConstants.ErrorMessage_FormName, null); + error = new Error(); + error.setErrorMessage(errorString); + error.setErrors(errorHashMap); + } + } else { + error = new Error(); + error.setErrorMessage(errorString); + error.setErrors(errorHashMap); + } + } catch (Exception e) { + throwException("find", CSAppConstants.ErrorMessage_JsonNotProper, null); + error = new Error(); + error.setErrorMessage(errorString); + error.setErrors(errorHashMap); + } + + if (error != null && callback != null) { + callback.onRequestFail(ResponseType.UNKNOWN, error); + } + + return this; + } + + /** + * Execute a Query and Caches its result (Optional) + * + * @param callBack {@link QueryResultsCallBack} object to notify the application when the request has completed. + * @return {@linkplain Query} object, so you can chain this call. + * + *

    Example :
    + *
    +     *      Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
    +     *      Query csQuery = stack.contentType("contentType_name").query();
    + * csQuery.findOne(new QueryResultsCallBack() {
    + * @Override + * public void onCompletion(ResponseType responseType, ENTRY entry, Error error) {
    + * + * } + * });
    + *
    + */ + public Query findOne(SingleQueryResultCallback callBack) { + Error error = null; + try { + if (isJsonProper) { + if (!TextUtils.isEmpty(formName)) { + + int limit = -1; + if (urlQueries != null && urlQueries.has("limit")) { + limit = (int) urlQueries.get("limit"); + } + urlQueries.put("limit", 1); + + execQuery(callBack, null, false); + + if (limit != -1) { + urlQueries.put("limit", limit); + } + + } else { + throwException("find", CSAppConstants.ErrorMessage_FormName, null); + error = new Error(); + error.setErrorMessage(errorString); + error.setErrors(errorHashMap); + } + } else { + error = new Error(); + error.setErrorMessage(errorString); + error.setErrors(errorHashMap); + } + } catch (Exception e) { + throwException("find", CSAppConstants.ErrorMessage_JsonNotProper, null); + error = new Error(); + error.setErrorMessage(errorString); + error.setErrors(errorHashMap); + } + + if (error != null && callBack != null) { + callBack.onRequestFail(ResponseType.UNKNOWN, error); + } + + return this; + } + + /** + * To cancel all {@link Query} network requests. + * + * + *

    Example :
    + *
    +     *      csQuery.cancelRequest();
    +     * 
    + */ + public void cancelRequest() { + CSAppConstants.cancelledCallController.add(CSAppConstants.callController.QUERY.toString()); + + if (Contentstack.requestQueue != null) { + Contentstack.requestQueue.cancelAll(CSAppConstants.callController.QUERY.toString()); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void throwException(String queryName, String messageString, Exception e) { + isJsonProper = false; + errorString = messageString; + errorHashMap = new HashMap(); + if (e != null) { + errorHashMap.put(queryName, e.toString()); + } + } + + protected void setQueryJson(QueryResultsCallBack callback) { + try { + + if (queryValueJSON != null && queryValueJSON.length() > 0) { + urlQueries.put("query", queryValueJSON); + } + + if (objectUidForExcept != null && objectUidForExcept.length() > 0) { + //JSONObject exceptValueJson = new JSONObject(); + //exceptValueJson.put("BASE", objectUidForExcept); + urlQueries.put("except[BASE][]", objectUidForExcept); + objectUidForExcept = null; + + } + + if (objectUidForOnly != null && objectUidForOnly.length() > 0) { + //JSONObject onlyValueJson = new JSONObject(); + //onlyValueJson.put("BASE", objectUidForOnly); + urlQueries.put("only[BASE][]", objectUidForOnly); + objectUidForOnly = null; + + } + + if (onlyJsonObject != null && onlyJsonObject.length() > 0) { + urlQueries.put("only", onlyJsonObject); + onlyJsonObject = null; + } + + if (exceptJsonObject != null && exceptJsonObject.length() > 0) { + urlQueries.put("except", exceptJsonObject); + exceptJsonObject = null; + } + + if (objectUidForInclude != null && objectUidForInclude.length() > 0) { + urlQueries.put("include[]", objectUidForInclude); + objectUidForInclude = null; + } + + } catch (Exception e) { + throwException("find", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } + + protected void execQuery(SingleQueryResultCallback callBack, QueryResultsCallBack callback, boolean isFromLocal) { + try { + + String URL = "/" + contentTypeInstance.stackInstance.VERSION + "/content_types/" + formName + "/entries"; + queryResultCallback = callback; + singleQueryResultCallback = callBack; + setQueryJson(callback); + ArrayMap headers = getHeader(localHeader); + + if (headers.size() < 1) { + throwException("find", CSAppConstants.ErrorMessage_CalledDefaultMethod, null); + } else { + if (headers.containsKey("environment")) { + urlQueries.put("environment", headers.get("environment")); + } + mainJSON.put("query", urlQueries); + mainJSON.put("_method", CSAppConstants.RequestMethod.GET.toString()); + String mainStringForMD5 = URL + mainJSON.toString() + headers.toString(); + String md5Value = new CSAppUtils().getMD5FromString(mainStringForMD5.trim()); + File cacheFile = new File(CSAppConstants.cacheFolderName + File.separator + md5Value); + CachePolicy cachePolicy = CachePolicy.NETWORK_ONLY;//contentTypeInstance.stackInstance.globalCachePolicyForCall; + if (cachePolicyForCall != null) { + cachePolicy = cachePolicyForCall; + } + switch (cachePolicy) { + case IGNORE_CACHE: + fetchFromNetwork(URL, headers, mainJSON, null, callback, callBack); + break; + case CACHE_ONLY: + fetchFromCache(cacheFile, callback, callBack); + break; + case NETWORK_ONLY: + fetchFromNetwork(URL, headers, mainJSON, cacheFile.getPath(), callback, callBack); + break; + case CACHE_ELSE_NETWORK: + if (cacheFile.exists()) { + boolean needToSendCall = false; + if (maxCacheTimeForCall > 0) { + needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) maxCacheTimeForCall); + } else { + needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) defaultCacheTimeInterval); + } + if (needToSendCall) { + fetchFromNetwork(URL, headers, mainJSON, cacheFile.getPath(), callback, callBack); + } else { + setCacheModel(cacheFile, callback, callBack); + } + } else { + fetchFromNetwork(URL, headers, mainJSON, cacheFile.getPath(), callback, callBack); + } + break; + case NETWORK_ELSE_CACHE: + if (CSAppConstants.isNetworkAvailable) { + fetchFromNetwork(URL, headers, mainJSON, cacheFile.getPath(), callback, callBack); + } else { + fetchFromCache(cacheFile, callback, callBack); + } + break; + case CACHE_THEN_NETWORK: + if (cacheFile.exists()) { + setCacheModel(cacheFile, callback, callBack); + } + // from network + fetchFromNetwork(URL, headers, mainJSON, cacheFile.getPath(), callback, callBack); + break; + + default: + break; + } + } + + + } catch (Exception e) { + throwException("find", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } + + //fetch from network. + private void fetchFromNetwork(String URL, ArrayMap headers, JSONObject jsonMain, String cacheFilePath, ResultCallBack callback, SingleQueryResultCallback resultCallback) { + + HashMap urlParams = getUrlParams(jsonMain); + + if (resultCallback != null) { + new CSBackgroundTask(this, contentTypeInstance.stackInstance, CSController.SINGLEQUERYOBJECT, URL, headers, urlParams, new JSONObject(), cacheFilePath, CSAppConstants.callController.QUERY.toString(), CSAppConstants.RequestMethod.GET, resultCallback); + } else { + new CSBackgroundTask(this, contentTypeInstance.stackInstance, CSController.QUERYOBJECT, URL, headers, urlParams, new JSONObject(), cacheFilePath, CSAppConstants.callController.QUERY.toString(), CSAppConstants.RequestMethod.GET, callback); + } + } + + private HashMap getUrlParams(JSONObject jsonMain) { + + JSONObject queryJSON = jsonMain.optJSONObject("query"); + HashMap hashMap = new HashMap<>(); + + if (queryJSON != null && queryJSON.length() > 0) { + Iterator iter = queryJSON.keys(); + while (iter.hasNext()) { + String key = iter.next(); + try { + Object value = queryJSON.opt(key); + hashMap.put(key, value); + } catch (Exception e) { + CSAppUtils.showLog(TAG, "----------------setQueryJson" + e.toString()); + } + } + + return hashMap; + } + + return null; + } + + //fetch from cache. + private void fetchFromCache(File cacheFile, QueryResultsCallBack callback, SingleQueryResultCallback callBack) { + Error error = null; + + if (cacheFile.exists()) { + boolean needToSendCall = false; + + if (maxCacheTimeForCall > 0) { + needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) maxCacheTimeForCall); + } else { + needToSendCall = new CSAppUtils().getResponseTimeFromCacheFile(cacheFile, (int) defaultCacheTimeInterval); + } + + if (needToSendCall) { + error = new Error(); + error.setErrorMessage(CSAppConstants.ErrorMessage_EntryNotFoundInCache); + + } else { + setCacheModel(cacheFile, callback, callBack); + } + } else { + error = new Error(); + error.setErrorMessage(CSAppConstants.ErrorMessage_EntryNotFoundInCache); + } + + if (callback != null && error != null) { + callback.onRequestFail(ResponseType.CACHE, error); + } else if (callBack != null && error != null) { + callBack.onRequestFail(ResponseType.CACHE, error); + } + } + + //ENTRY modeling from cache. + private void setCacheModel(File cacheFile, QueryResultsCallBack callback, SingleQueryResultCallback callBack) { + EntriesModel model = new EntriesModel(CSAppUtils.getJsonFromCacheFile(cacheFile), null, true); + List entries = new ArrayList<>(); + List objects = model.objectList; + int countObject = objects.size(); + + for (int i = 0; i < countObject; i++) { + Entry entry = null; + try { + entry = contentTypeInstance.entry(((EntryModel) objects.get(i)).entryUid); + + } catch (Exception e) { + entry = new Entry(formName); + entry.setUid(((EntryModel) objects.get(i)).entryUid); + CSAppUtils.showLog(TAG, "----------------getResultObject" + e.toString()); + } + + entry.resultJson = ((EntryModel) objects.get(i)).jsonObject; + entry.ownerEmailId = ((EntryModel) objects.get(i)).ownerEmailId; + entry.ownerUid = ((EntryModel) objects.get(i)).ownerUid; + entry.title = ((EntryModel) objects.get(i)).title; + entry.url = ((EntryModel) objects.get(i)).url; + if (((EntryModel) objects.get(i)).ownerMap != null) { + entry.owner = new HashMap<>(((EntryModel) objects.get(i)).ownerMap); + } + if (((EntryModel) objects.get(i))._metadata != null) { + entry._metadata = new HashMap<>(((EntryModel) objects.get(i))._metadata); + } + + entry.setTags(((EntryModel) objects.get(i)).tags); + entries.add(entry); + } + + if (callback != null) { + QueryResult queryResultObject = new QueryResult(); + queryResultObject.setJSON(model.jsonObject, entries); + callback.onRequestFinish(ResponseType.CACHE, queryResultObject); + model = null; + } else if (callBack != null) { + + Entry entry = contentTypeInstance.entry(); + if (entries != null && entries.size() > 0) { + entry = entries.get(0); + } + callBack.onRequestFinish(ResponseType.CACHE, entry); + } + } + + @Override + public void getResult(Object object, String controller) { + } + + @Override + public void getResultObject(List objects, JSONObject jsonObject, boolean isSingleEntry) { + + List objectList = new ArrayList<>(); + int countObject = objects.size(); + + for (int i = 0; i < countObject; i++) { + Entry entry = null; + try { + entry = contentTypeInstance.stackInstance.contentType(formName).entry(((EntryModel) objects.get(i)).entryUid); + } catch (Exception e) { + entry = new Entry(formName); + } + entry.setUid(((EntryModel) objects.get(i)).entryUid); + entry.resultJson = ((EntryModel) objects.get(i)).jsonObject; + entry.ownerEmailId = ((EntryModel) objects.get(i)).ownerEmailId; + entry.ownerUid = ((EntryModel) objects.get(i)).ownerUid; + entry.title = ((EntryModel) objects.get(i)).title; + entry.url = ((EntryModel) objects.get(i)).url; + + if (((EntryModel) objects.get(i)).ownerMap != null) { + entry.owner = new HashMap<>(((EntryModel) objects.get(i)).ownerMap); + } + if (((EntryModel) objects.get(i))._metadata != null) { + entry._metadata = new HashMap<>(((EntryModel) objects.get(i))._metadata); + } + + + entry.setTags(((EntryModel) objects.get(i)).tags); + objectList.add(entry); + } + + if (isSingleEntry) { + Entry entry = contentTypeInstance.entry(); + if (objectList != null && objectList.size() > 0) { + entry = objectList.get(0); + } + + if (singleQueryResultCallback != null) { + singleQueryResultCallback.onRequestFinish(ResponseType.NETWORK, entry); + } + + } else { + QueryResult queryResultObject = new QueryResult(); + queryResultObject.setJSON(jsonObject, objectList); + + if (queryResultCallback != null) { + queryResultCallback.onRequestFinish(ResponseType.NETWORK, queryResultObject); + } + } + } + + private ArrayMap getHeader(ArrayMap localHeader) { + ArrayMap mainHeader = formHeader; + ArrayMap classHeaders = new ArrayMap<>(); + + if (localHeader != null && localHeader.size() > 0) { + if (mainHeader != null && mainHeader.size() > 0) { + for (Map.Entry entry : localHeader.entrySet()) { + String key = entry.getKey(); + classHeaders.put(key, entry.getValue()); + } + + for (Map.Entry entry : mainHeader.entrySet()) { + String key = entry.getKey(); + if (!classHeaders.containsKey(key)) { + classHeaders.put(key, entry.getValue()); + } + } + return classHeaders; + } else { + return localHeader; + } + + } else { + return formHeader; + } + } + + /** + * This method adds key and value to an Entry. + * + * @param key The key as string which needs to be added to the Query + * @param value The value as string which needs to be added to the Query + * @return {@link Query} + * + *

    Example :
    + *
    +     *      Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
    +     *      Query csQuery = stack.contentType("contentType_name").query();
    + * csQuery.addParam("key", "some_value"); + * csQuery.findOne(new QueryResultsCallBack() {
    + * @Override + * public void onCompletion(ResponseType responseType, ENTRY entry, Error error) {
    + * + * } + * });
    + *
    + */ + public Query addParam(String key, String value) { + try { + if (key != null && value != null) { + urlQueries.put(key, value); + } else { + throwException("and", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + } catch (Exception e) { + throwException("and", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + return this; + } + + + /** + * This method also includes the content type UIDs of the referenced entries returned in the response + * + * @return {@link Query} + * + *

    Example :
    + *
    +     *      Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
    +     *      Query csQuery = stack.contentType("contentType_name").query();
    + * csQuery.addParam("key", "some_value"); + * csQuery.findOne(new QueryResultsCallBack() {
    + * @Override + * public void onCompletion(ResponseType responseType, ENTRY entry, Error error) {
    + * + * } + * });
    + *
    + */ + public Query includeReferenceContentTypUid() { + try { + urlQueries.put("include_reference_content_type_uid", "true"); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + return this; + } + + + /** + * Get entries having values based on referenced fields. This query retrieves all entries that satisfy the query conditions made on referenced fields. + * + * @param key The key to be constrained + * @return {@link Query} object, so you can chain this call. + * + *

    Example :
    + *
    +     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
    +     *     Query csQuery = stack.contentType("contentType_name").query();
    +     *     csQuery.whereIn("due_date");
    +     * 
    + */ + public Query whereIn(String key, Query queryObject) { + + if (key != null) { + + try { + + JSONObject inQueryObj = new JSONObject(); + inQueryObj.put("$in_query", queryObject.queryValueJSON.toString()); + queryValueJSON.put(key, inQueryObj); + } catch (Exception e) { + throwException("in_query", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } else { + throwException("in_query", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + return this; + } + + + /** + * Get entries having values based on referenced fields. This query works the opposite of $in_query and retrieves all entries that does not satisfy query conditions made on referenced fields. + * + * @param key The key to be constrained + * @return {@link Query} object, so you can chain this call. + * + *

    Example :
    + *
    +     *     Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "env");
    +     *     Query csQuery = stack.contentType("contentType_name").query();
    +     *     csQuery.whereNotIn("due_date");
    +     * 
    + */ + public Query whereNotIn(String key, Query queryObject) { + + if (key != null) { + + try { + JSONObject inQueryObj = new JSONObject(); + inQueryObj.put("$nin_query", queryObject.queryValueJSON.toString()); + queryValueJSON.put(key, inQueryObj); + } catch (Exception e) { + throwException("nin_query", CSAppConstants.ErrorMessage_QueryFilterException, e); + } + } else { + throwException("nin_query", CSAppConstants.ErrorMessage_QueryFilterException, null); + } + return this; + } + + + /** + * Retrieve the published content of the fallback locale if an entry is not localized in specified locale. + * + * @return {@link Query} object, so you can chain this call. + *

    Example :
    + *
    +     *     Stack stack = Contentstack.stack(context, "ApiKey", "deliveryToken",  environment_name);
    +     *     Query csQuery = stack.contentType("contentType_name").query();
    +     *     csQuery.includeFallback();
    +     * 
    + */ + public Query includeFallback() { + try { + mainJSON.put("include_fallback", true); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + return this; + } + + /** + * includeEmbeddedItems instance of Query + * Include Embedded Objects (Entries and Assets) along with entry/entries details.
    + * Stack stack = Contentstack.stack( "ApiKey", "deliveryToken", "environment"); + * final Query query = stack.contentType("user").query(); + * query = query.includeEmbeddedObjects() + * + * @return {@link Query} + */ + public Query includeEmbeddedItems() { + try { + mainJSON.put("include_embedded_items[]", "BASE"); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + return this; + } + +} + diff --git a/contentstack/src/main/java/com/contentstack/sdk/QueryResult.java b/contentstack/src/main/java/com/contentstack/sdk/QueryResult.java new file mode 100755 index 00000000..1177ea44 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/QueryResult.java @@ -0,0 +1,117 @@ +package com.contentstack.sdk; + +import com.contentstack.sdk.utilities.CSAppUtils; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.List; + +/** + * Helper class for parsing the result of {@link Entry} + * + * @author Contentstack.com, Inc + */ +public class QueryResult { + + + private static final String TAG = "QueryResult"; + protected JSONObject receiveJson; + protected JSONArray schemaArray; + protected int count; + protected JSONObject contentObject; + protected List resultObjects; + + /** + * Returns {@link Entry} objects list. + * + *

    Example :
    + *
    +     * List<Entry> list = queryResultObject.getResultObjects();
    + *
    + */ + public List getResultObjects() { + return resultObjects; + } + + /** + * Returns count of objects available.
    + * + * Note : + * To retrieve this data, {@link Query#includeCount()} or {@link Query#count()} should be added in {@link Query} while querying. + * + *

    Example :
    + *
    +     * int count = queryResultObject.getCount();
    + *
    + */ + public int getCount() { + return count; + } + + /** + * Returns class's schema if call to fetch schema executed successfully. + * + *

    Example :
    + *
    +     * JSONArray itemsArray = queryResultObject.getSchema();
    + *
    + */ + public JSONArray getSchema() { + return schemaArray; + } + + + /** + * Returns class's content type if call to fetch contentType executed successfully. + * + * @return JSONObject contentObject + *

    Example :
    + *
    +     * JSONObject contentObject = queryResultObject.getContentType();
    + *
    + */ + public JSONObject getContentType() { + return contentObject; + } + + + protected void setJSON(JSONObject jsonobject, List objectList) { + receiveJson = jsonobject; + resultObjects = objectList; + + try { + if (receiveJson != null) { + if (receiveJson.has("schema")) { + + JSONArray jsonarray = new JSONArray(); + jsonarray = receiveJson.getJSONArray("schema"); + if (jsonarray != null) { + schemaArray = jsonarray; + } + } + + if (receiveJson.has("content_type")) { + + JSONObject jsonObject = receiveJson.getJSONObject("content_type"); + if (jsonObject != null) { + contentObject = jsonObject; + } + } + + if (receiveJson.has("count")) { + count = receiveJson.optInt("count"); + } + + if (count <= 0) { + if (receiveJson.has("entries")) { + count = receiveJson.optInt("entries"); + } + } + } + + } catch (Exception e) { + CSAppUtils.showLog(TAG, "----------------------QueryResult--setJSON--" + e.toString()); + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/QueryResultsCallBack.java b/contentstack/src/main/java/com/contentstack/sdk/QueryResultsCallBack.java new file mode 100755 index 00000000..ad612802 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/QueryResultsCallBack.java @@ -0,0 +1,26 @@ +package com.contentstack.sdk; + + +/** + * @author Contentstack.com, Inc + */ +public abstract class QueryResultsCallBack extends ResultCallBack { + + public abstract void onCompletion(ResponseType responseType, QueryResult queryresult, Error error); + + void onRequestFinish(ResponseType responseType, QueryResult queryResultObject) { + onCompletion(responseType, queryResultObject, null); + } + + @Override + void onRequestFail(ResponseType responseType, Error error) { + onCompletion(responseType, null, error); + } + + @Override + void always() { + + } + + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/ResponseType.java b/contentstack/src/main/java/com/contentstack/sdk/ResponseType.java new file mode 100755 index 00000000..6671894a --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/ResponseType.java @@ -0,0 +1,28 @@ +package com.contentstack.sdk; + +/** + * Response Type. + *

    + * In following categories. + * Network, Cache. + * + * @author Contentstack.com, Inc + */ +public enum ResponseType { + + /** + * Response from network. + */ + NETWORK, + + /** + * Response from Cache. + */ + CACHE, + + /** + * Request not reach up to network and cache. + */ + UNKNOWN, + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/ResultCallBack.java b/contentstack/src/main/java/com/contentstack/sdk/ResultCallBack.java new file mode 100755 index 00000000..866b73a8 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/ResultCallBack.java @@ -0,0 +1,15 @@ +package com.contentstack.sdk; + + +/** + * callback to notify class after network call has been executed. + * + * @author Contentstack.com, Inc + */ + +public abstract class ResultCallBack { + + abstract void onRequestFail(ResponseType responseType, Error error); + + abstract void always(); +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/SingleQueryResultCallback.java b/contentstack/src/main/java/com/contentstack/sdk/SingleQueryResultCallback.java new file mode 100755 index 00000000..36892171 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/SingleQueryResultCallback.java @@ -0,0 +1,25 @@ +package com.contentstack.sdk; + +/** + * @author Contentstack.com, Inc + */ +public abstract class SingleQueryResultCallback extends ResultCallBack { + + public abstract void onCompletion(ResponseType responseType, Entry entry, Error error); + + void onRequestFinish(ResponseType responseType, Entry entry) { + onCompletion(responseType, entry, null); + } + + @Override + void onRequestFail(ResponseType responseType, Error error) { + onCompletion(responseType, null, error); + } + + @Override + void always() { + + } + + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/Stack.java b/contentstack/src/main/java/com/contentstack/sdk/Stack.java new file mode 100755 index 00000000..7c2a5493 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/Stack.java @@ -0,0 +1,770 @@ +package com.contentstack.sdk; + +import android.util.ArrayMap; +import android.text.TextUtils; +import android.util.Log; + +import com.contentstack.sdk.utilities.CSAppConstants; +import com.contentstack.sdk.utilities.CSAppUtils; +import com.contentstack.sdk.utilities.CSController; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +/** + * To fetch stack level information of your application from Contentstack server. + *

    + * Created by Shailesh Mishra. + * Contentstack pvt Ltd + */ +public class Stack implements INotifyClass { + + private static final String TAG = "Stack"; + private String stackApiKey = null; + protected ArrayMap localHeader = null; + private String imageTransformationUrl; + private LinkedHashMap imageParams = new LinkedHashMap<>(); + protected String URLSCHEMA = "https://"; + protected String URL = "cdn.contentstack.io"; + protected String VERSION = "v3"; + protected String SYNC_KEY = "sync"; + protected Config config; + protected ArrayMap headerGroup_app; + private JSONObject syncParams = null; + protected String skip = null; + protected String limit = null; + protected String sync_token = null; + protected String pagination_token = null; + protected String contentType; + protected String localeCode; + protected PublishType publishType; + protected String start_from_date; + private SyncResultCallBack syncCallBack; + + + public enum PublishType { + entry_published, + entry_unpublished, + entry_deleted, + asset_published, + asset_unpublished, + asset_deleted, + content_type_deleted + } + + + private Stack() { + } + + + protected Stack(String stackApiKey) { + this.stackApiKey = stackApiKey; + this.localHeader = new ArrayMap(); + + } + + protected void setConfig(Config config) { + this.config = config; + URLSCHEMA = config.URLSCHEMA; + URL = config.URL; + VERSION = config.VERSION; + + if (!TextUtils.isEmpty(config.environment)) { + setHeader("environment", config.environment); + } + + if (!config.region.name().isEmpty()) { + String region = config.region.name().toLowerCase(); + if (!region.equalsIgnoreCase("us")) { + if (URL.equalsIgnoreCase("cdn.contentstack.io")) { + URL = "cdn.contentstack.com"; + } + URL = region + "-" + URL; + } + } + + } + + /** + * Represents a {@link ContentType}.
    + * Create {@link ContentType} instance. + * + * @param contentTypeName contentType name. + * @return {@link ContentType} instance. + * + *

    Example :
    + *

    +     * Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "stag");
    +     *
    +     * ContentType contentType = stack.contentType("blog");
    +     * 
    + */ + public ContentType contentType(String contentTypeName) { + ContentType contentType = new ContentType(contentTypeName); + contentType.setStackInstance(this); + + return contentType; + } + + + /** + * Create {@link Asset} instance. + * + * @return {@link ContentType} instance. + * + *

    Example :
    + *
    +     * Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "stag");
    +     * Asset asset = stack.asset("assetUid");
    +     * 
    + */ + public Asset asset(String uid) { + Asset asset = new Asset(uid); + asset.setStackInstance(this); + + return asset; + } + + /** + * Create {@link Asset} instance. + * + * @return {@link ContentType} instance. + * + *

    Example :
    + *
    +     *  Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "stag");
    +     * Asset asset = stack.asset();
    +     * 
    + */ + protected Asset asset() { + Asset asset = new Asset(); + asset.setStackInstance(this); + + return asset; + } + + /** + * Create {@link AssetLibrary} instance. + * + * @return {@link ContentType} instance. + * + *

    Example :
    + *
    +     * Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "stag");
    +     * AssetLibrary assetLib = stack.assetLibrary();
    +     * 
    + */ + public AssetLibrary assetLibrary() { + AssetLibrary library = new AssetLibrary(); + library.setStackInstance(this); + + return library; + } + + /** + * Get stack application key + * + *

    + *

    Example :
    + *

    +     * String stackApiKey = stack.getApplicationKey();
    +     * 
    + *

    + */ + public String getApplicationKey() { + return stackApiKey; + } + + /** + * Get stack access token + * + *

    + *

    Example :
    + *

    +     * String accessToken = stack.getAccessToken();
    +     * 
    + *

    + */ + public String getAccessToken() { + return localHeader != null ? (String) localHeader.get("access_token") : null; + } + + /** + * Remove header key. + * + * @param key custom_header_key + * + *

    Example :
    + *
    +     *             Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "stag");
    + * stack.removeHeader("custom_header_key"); + *
    + */ + public void removeHeader(String key) { + if (!TextUtils.isEmpty(key)) { + localHeader.remove(key); + } + } + + /** + * To set headers for Contentstack rest calls. + *
    + * Scope is limited to this object and followed classes. + * + * @param key header name. + * @param value header value against given header name. + * + *

    Example :
    + *
    +     *               Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "stag");
    + * stack.setHeader("custom_key", "custom_value"); + *
    + */ + public void setHeader(String key, String value) { + if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(value)) { + localHeader.put(key, value); + } + } + + + /** + * @param image_url on which we want to manipulate. + * @param parameters It is an second parameter in which we want to place different manipulation key and value in array form + * @return String + *

    + * ImageTransform function is define for image manipulation with different + * parameters in second parameter in array form + * + *

    Example :
    + *

    +     *  Stack stack = Contentstack.stack(context, "apiKey", "deliveryToken", "stag");
    + * // resize the image by specifying width and height + * LinkedHashMap imageParams = new LinkedHashMap(); + * imageParams.put("width", 100); + * imageParams.put("height",100); + * imageUrl = Stack.ImageTransform(image_url, parameters); + * stack.ImageTransform(image_url, parameters); + * + * + * + *
    + */ + public String ImageTransform(String image_url, LinkedHashMap parameters) { + imageTransformationUrl = image_url; + imageParams = parameters; + return getImageUrl(); + } + + + private String getImageUrl() { + + if (imageParams == null || imageParams.size() == 0) { + return imageTransformationUrl; + } + + for (Map.Entry param : imageParams.entrySet()) { + try { + + String paramKey = param.getKey(); + String paramValue = param.getValue().toString(); + + final String encodedKey = URLEncoder.encode(paramKey, "UTF-8"); + final String encodedValue = URLEncoder.encode(paramValue, "UTF-8"); + if (!imageTransformationUrl.contains("?")) { + imageTransformationUrl += "?" + encodedKey + "=" + encodedValue; + } else { + imageTransformationUrl += "&" + encodedKey + "=" + encodedValue; + } + + } catch (UnsupportedEncodingException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + } + + return imageTransformationUrl; + } + + + /** + * @param params query parameters + * @param callback ContentTypesCallback + * This call returns comprehensive information of all the content types available in a particular stack in your account. + * + *

    Example :
    + *
    +     *                 JSONObject params = new JSONObject();
    +     *                 params.put("include_snippet_schema", true);
    +     *                 params.put("limit", 3);
    +     *                 stack.getContentTypes(params, new ContentTypesCallback() {
    +     *                 @Override
    +     *                 public void onCompletion(ContentTypesModel contentTypesModel, Error error) {
    +     *                 if (error == null){
    +     *                   // do your stuff.
    +     *                  }
    +     *
    +     *                 }
    +     *                 });
    +     *                 
    + */ + + public void getContentTypes(JSONObject params, final ContentTypesCallback callback) { + + try { + String URL = "/" + this.VERSION + "/content_types"; + ArrayMap headers = getHeader(localHeader); + if (params == null) { + params = new JSONObject(); + } + Iterator keys = params.keys(); + while (keys.hasNext()) { + // loop to get the dynamic key + String key = (String) keys.next(); + // get the value of the dynamic key + Object value = params.opt(key); + // do something here with the value... + params.put(key, value); + } + + if (headers.containsKey("environment")) { + params.put("environment", headers.get("environment")); + params.put("include_count", true); + } + + fetchContentTypes(URL, params, headers, null, callback); + + } catch (Exception e) { + + Error error = new Error(); + error.setErrorMessage(CSAppConstants.ErrorMessage_JsonNotProper); + callback.onRequestFail(ResponseType.UNKNOWN, error); + } + } + + + private void fetchContentTypes(String urlString, JSONObject urlQueries, ArrayMap headers, String cacheFilePath, ContentTypesCallback callback) { + + if (callback != null) { + + HashMap urlParams = getUrlParams(urlQueries); + new CSBackgroundTask(this, this, CSController.FETCHCONTENTTYPES, urlString, headers, urlParams, new JSONObject(), cacheFilePath, CSAppConstants.callController.CONTENTTYPES.toString(), false, CSAppConstants.RequestMethod.GET, callback); + } + } + + + /** + * @param syncCallBack returns callback for sync result. + *

    + *

    + * The Sync request performs a complete sync of your app data. + * It returns all the published entries and assets of the specified stack in response. + * The response also contains a sync token, which you need to store, + * since this token is used to get subsequent delta updates later. + * + *

    Example :
    + *

    +     *
    +     *                     stack.sync(SyncResultCallBack syncCallBack){  }
    +     *
    +     *                     
    + */ + + public void sync(SyncResultCallBack syncCallBack) { + + if (syncParams == null) { + syncParams = new JSONObject(); + } + try { + syncParams.put("init", true); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + + this.requestSync(syncCallBack); + + } + + + /** + * @param pagination_token If the response is paginated, use the pagination token under this parameter. + * @param syncCallBack returns callback for sync result + *

    + * If the result of the initial sync (or subsequent sync) contains more than 100 records, + * the response would be paginated. It provides pagination token in the response. However, + * you do not have to use the pagination token manually to get the next batch, + * the SDK does that automatically until the sync is complete. + * Pagination token can be used in case you want to fetch only selected batches. + * It is especially useful if the sync process is interrupted midway (due to network issues, etc.). + * In such cases, this token can be used to restart the sync process from where it was interrupted. + * + *

    Example :
    + *

    +     *                         stack.syncPaginationToken(pagination_token, new SyncResultCallBack()) {}
    +     *                         
    + */ + public void syncPaginationToken(String pagination_token, SyncResultCallBack syncCallBack) { + this.pagination_token = pagination_token; + if (syncParams == null) { + syncParams = new JSONObject(); + } + + try { + syncParams.put("init", true); + syncParams.put("pagination_token", pagination_token); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + + this.requestSync(syncCallBack); + } + + + /** + * @param sync_token Use the sync token that you received in the previous/initial sync under this parameter. + * @param syncCallBack returns callback for sync result + *

    + * You can use the sync token (that you receive after initial sync) to get the updated content next time. + * The sync token fetches only the content that was added after your last sync, + * and the details of the content that was deleted or updated. + *

    Example :
    + *

    +     *                      stack.syncToken(sync_token, new SyncResultCallBack() ){ }
    +     *
    +     *                     
    + */ + public void syncToken(String sync_token, SyncResultCallBack syncCallBack) { + + this.sync_token = sync_token; + if (syncParams == null) { + syncParams = new JSONObject(); + } + try { + syncParams.put("init", true); + syncParams.put("sync_token", sync_token); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + + this.requestSync(syncCallBack); + } + + + /** + * @param from_date Enter the start date for initial sync. + * @param syncCallBack Returns callback for sync result. + *

    + * You can also initialize sync with entries published after a specific date. To do this, use syncWithDate + * and specify the start date as its value. + * + *

    Example :
    + *

    +     *                    stack.syncFromDate(start_date, new SyncResultCallBack()) { }
    +     *                      
    + */ + public void syncFromDate(Date from_date, SyncResultCallBack syncCallBack) { + start_from_date = convertUTCToISO(from_date); + if (syncParams == null) { + syncParams = new JSONObject(); + } + + try { + syncParams.put("init", true); + syncParams.put("start_from", start_from_date); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + this.requestSync(syncCallBack); + } + + + private String convertUTCToISO(Date date) { + + TimeZone tz = TimeZone.getTimeZone("UTC"); + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + dateFormat.setTimeZone(tz); + return dateFormat.format(date); + } + + + /** + * @param content_type Provide uid of your content_type + * @param syncCallBack Returns callback for sync result. + *

    + * You can also initialize sync with entries of only specific content_type. + * To do this, use syncContentType and specify the content type uid as its value. + * However, if you do this, the subsequent syncs will only include the entries of the specified content_type. + * + *

    Example :
    + *

    +     *
    +     *                     // dummy content_type like "session"
    +     *                     stack.syncContentType(String content_type, new SyncResultCallBack()){  }
    +     *
    +     *                      
    + */ + public void syncContentType(String content_type, SyncResultCallBack syncCallBack) { + + this.contentType = content_type; + if (syncParams == null) { + syncParams = new JSONObject(); + } + try { + syncParams.put("init", true); + syncParams.put("content_type_uid", contentType); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + + this.requestSync(syncCallBack); + } + + + /** + * @param language Select the required locale from the Language class. + * @param syncCallBack Returns callback for sync result. + *

    + * You can also initialize sync with entries of only specific locales. + * To do this, use syncLocale and specify the locale code as its value. + * However, if you do this, the subsequent syncs will only include the entries of the specified locales. + * + *

    Example :
    + *

    +     *
    +     *                     // dummy language- Language.ENGLISH_UNITED_STATES
    +     *                     stackInstance.syncLocale(Language.ENGLISH_UNITED_STATES, new SyncResultCallBack() ) { }
    +     *
    +     *                     
    + */ + public void syncLocale(Language language, SyncResultCallBack syncCallBack) { + this.localeCode = getLanguageCode(language); + + if (syncParams == null) { + syncParams = new JSONObject(); + } + try { + syncParams.put("init", true); + syncParams.put("locale", localeCode); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + + + this.requestSync(syncCallBack); + } + + + private String getLanguageCode(Language language) { + + String localeCode = null; + if (language != null) { + + Language languageName = Language.valueOf(language.name()); + int localeValue = languageName.ordinal(); + LanguageCode[] languageCodeValues = LanguageCode.values(); + localeCode = languageCodeValues[localeValue].name(); + localeCode = localeCode.replace("_", "-"); + } + + return localeCode; + } + + + /** + * @param type - Use the type parameter to get a specific type of content + * like ( asset_published, entry_published, asset_unpublished, asset_deleted, entry_unpublished, entry_deleted, content_type_deleted.) + * @param syncCallBack returns callback for sync result. + *

    + * Use the type parameter to get a specific type of content. You can pass one of the following values: + * asset_published, entry_published, asset_unpublished, asset_deleted, entry_unpublished, entry_deleted, content_type_deleted. + * If you do not specify any value, it will bring all published entries and published assets. + * + *

    Example :
    + *

    +     *
    +     *                       stackInstance.syncPublishType(Stack.PublishType.entry_published, new SyncResultCallBack()) { }
    +     *
    +     *                      
    + */ + + public void syncPublishType(PublishType type, SyncResultCallBack syncCallBack) { + this.publishType = type; + if (syncParams == null) { + syncParams = new JSONObject(); + } + + try { + syncParams.put("init", true); + syncParams.put("type", publishType); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + + this.requestSync(syncCallBack); + } + + + /** + * @param contentType + * @param from_date + * @param language + * @param type + * @param syncCallBack You can also initialize sync with entries that satisfy multiple parameters. + * To do this, use syncWith and specify the parameters. + * However, if you do this, the subsequent syncs will only include the entries of the specified parameters + * + *

    Example :
    + *
    +     *
    +     *                      stackInstance.sync(String contentType, Date from_date, Language language, PublishType type,  SyncResultCallBack syncCallBack) { }
    +     *
    +     *
    +     *                     
    + */ + + public void sync(String contentType, Date from_date, Language language, PublishType type, SyncResultCallBack syncCallBack) { + start_from_date = convertUTCToISO(from_date); + this.contentType = contentType; + this.publishType = type; + this.localeCode = getLanguageCode(language); + + if (syncParams == null) { + syncParams = new JSONObject(); + } + try { + syncParams.put("init", true); + syncParams.put("start_from", this.start_from_date); + syncParams.put("content_type_uid", this.contentType); + syncParams.put("type", publishType); + syncParams.put("locale", this.localeCode); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + + this.requestSync(syncCallBack); + } + + + private void requestSync(final SyncResultCallBack callback) { + + try { + + String URL = "/" + this.VERSION + "/stacks/" + SYNC_KEY; + ArrayMap headers = getHeader(localHeader); + + JSONObject urlQueries = new JSONObject(); + if (headers.containsKey("environment")) { + syncParams.put("environment", headers.get("environment")); + } + + urlQueries = syncParams; + fetchFromNetwork(URL, urlQueries, headers, null, new SyncResultCallBack() { + @Override + public void onCompletion(SyncStack syncStack, Error error) { + + if (error == null) { + String paginationToken = syncStack.getPaginationToken(); + if (paginationToken != null) { + syncPaginationToken(paginationToken, callback); + } + } + + callback.onCompletion(syncStack, error); + } + }); + + + } catch (Exception e) { + + Error error = new Error(); + error.setErrorMessage(CSAppConstants.ErrorMessage_JsonNotProper); + callback.onRequestFail(ResponseType.UNKNOWN, error); + } + + } + + + private void fetchFromNetwork(String urlString, JSONObject urlQueries, ArrayMap headers, String cacheFilePath, SyncResultCallBack callback) { + if (callback != null) { + HashMap urlParams = getUrlParams(urlQueries); + new CSBackgroundTask(this, this, CSController.FETCHSYNC, urlString, headers, urlParams, new JSONObject(), cacheFilePath, CSAppConstants.callController.SYNC.toString(), false, CSAppConstants.RequestMethod.GET, callback); + } + } + + + private HashMap getUrlParams(JSONObject urlQueriesJSON) { + + HashMap hashMap = new HashMap<>(); + + if (urlQueriesJSON != null && urlQueriesJSON.length() > 0) { + Iterator iter = urlQueriesJSON.keys(); + while (iter.hasNext()) { + String key = iter.next(); + try { + Object value = urlQueriesJSON.opt(key); + hashMap.put(key, value); + } catch (Exception e) { + CSAppUtils.showLog(TAG, e.getLocalizedMessage()); + } + } + + return hashMap; + } + + return null; + } + + + private ArrayMap getHeader(ArrayMap localHeader) { + ArrayMap mainHeader = headerGroup_app; + ArrayMap classHeaders = new ArrayMap<>(); + if (localHeader != null && localHeader.size() > 0) { + if (mainHeader != null && mainHeader.size() > 0) { + for (Map.Entry entry : localHeader.entrySet()) { + String key = entry.getKey(); + classHeaders.put(key, entry.getValue()); + } + for (Map.Entry entry : mainHeader.entrySet()) { + String key = entry.getKey(); + if (!classHeaders.containsKey(key)) { + classHeaders.put(key, entry.getValue()); + } + } + return classHeaders; + } else { + return localHeader; + } + } else { + return headerGroup_app; + } + + } + + + @Override + public void getResult(Object object, String controller) { + } + + + @Override + public void getResultObject(List object, JSONObject jsonObject, boolean isSingleEntry) { + SyncStack syncStackObject = new SyncStack(); + syncStackObject.setJSON(jsonObject); + if (syncCallBack != null) { + syncCallBack.onRequestFinish(syncStackObject); + } + } + + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/SyncResultCallBack.java b/contentstack/src/main/java/com/contentstack/sdk/SyncResultCallBack.java new file mode 100755 index 00000000..a1e95eb0 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/SyncResultCallBack.java @@ -0,0 +1,25 @@ +package com.contentstack.sdk; + +/** + * @author Contentstack.com, Inc callback. + */ +public abstract class SyncResultCallBack extends ResultCallBack { + + + public abstract void onCompletion(SyncStack syncStack, Error error); + + void onRequestFinish(SyncStack syncStack) { + onCompletion(syncStack, null); + } + + @Override + void onRequestFail(ResponseType responseType, Error error) { + onCompletion(null, error); + } + + @Override + void always() { + + } + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/SyncStack.java b/contentstack/src/main/java/com/contentstack/sdk/SyncStack.java new file mode 100755 index 00000000..b115f900 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/SyncStack.java @@ -0,0 +1,103 @@ +package com.contentstack.sdk; + +import com.contentstack.sdk.utilities.CSAppUtils; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; + + +public class SyncStack { + + private static final String TAG = SyncStack.class.getSimpleName(); + private JSONObject receiveJson; + private int skip; + private int limit; + private int count; + private String URL; + private String pagination_token; + private String sync_token; + private ArrayList syncItems; + + + public String getURL() { + return this.URL; + } + + public JSONObject getJSONResponse() { + return this.receiveJson; + } + + public int getCount() { + return this.count; + } + + public int getLimit() { + return this.limit; + } + + public int getSkip() { + return this.skip; + } + + public String getPaginationToken() { + return this.pagination_token; + } + + public String getSyncToken() { + return this.sync_token; + } + + public ArrayList getItems() { + return this.syncItems; + } + + protected void setJSON(JSONObject jsonobject) { + + if (jsonobject != null) { + receiveJson = jsonobject; + try { + if (receiveJson != null) { + + URL = ""; + + if (receiveJson.has("items")) { + JSONArray jsonarray = receiveJson.getJSONArray("items"); + if (jsonarray != null) { + syncItems = new ArrayList<>(); + for (int position = 0; position < jsonarray.length(); position++) { + syncItems.add(jsonarray.optJSONObject(position)); + } + } + } + + if (receiveJson.has("skip")) { + this.skip = receiveJson.optInt("skip"); + } + if (receiveJson.has("total_count")) { + this.count = receiveJson.optInt("total_count"); + } + if (receiveJson.has("limit")) { + this.limit = receiveJson.optInt("limit"); + } + if (receiveJson.has("pagination_token")) { + this.pagination_token = receiveJson.optString("pagination_token"); + } else { + this.sync_token = null; + } + if (receiveJson.has("sync_token")) { + this.sync_token = receiveJson.optString("sync_token"); + } else { + this.sync_token = null; + } + } + } catch (Exception e) { + CSAppUtils.showLog(TAG, e.getLocalizedMessage()); + } + + } + } + + +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/TestActivity.java b/contentstack/src/main/java/com/contentstack/sdk/TestActivity.java new file mode 100755 index 00000000..7225266d --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/TestActivity.java @@ -0,0 +1,15 @@ +package com.contentstack.sdk; + +import android.app.Application; + +/** + * @author Contentstack.com, Inc + */ + +public class TestActivity extends Application { + + @Override + public void onCreate() { + super.onCreate(); + } +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/utilities/CSAppConstants.java b/contentstack/src/main/java/com/contentstack/sdk/utilities/CSAppConstants.java new file mode 100755 index 00000000..c300d29b --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/utilities/CSAppConstants.java @@ -0,0 +1,65 @@ +package com.contentstack.sdk.utilities; + +import com.contentstack.okhttp.Protocol; + +import java.util.ArrayList; + +/** + * @author contentstack.com, Inc + */ +public class CSAppConstants { + + public static final boolean debug = false; + public static boolean isNetworkAvailable = true; + public static String URLSCHEMA_HTTPS = "https://"; + public static String SDK_VERSION = "3.10.1"; + public final static int NONETWORKCONNECTION = 408; + public final static int TimeOutDuration = 30000; // timeout in millisecond + public final static int NumRetry = 0; + public final static int BackOFMultiplier = 0; + + //Implemented for single network call cancellation. for class-level network call cancellation. + public static ArrayList cancelledCallController = new ArrayList(); + + /** + * Directory path to store offline data. + * used to saved cached network calls with response. + */ + public static String cacheFolderName; + + public static enum RequestMethod { + + GET, POST, PUT, DELETE + } + + public static enum callController { + + QUERY, + ENTRY, + STACK, + ASSET, + SYNC, + CONTENTTYPES, + ASSETLIBRARY; + } + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + public final static String ErrorMessage_JsonNotProper = "Please provide valid JSON."; + public final static String ErrorMessage_StackContextIsNull = "Context can not be null."; + public final static String ErrorMessage_StackApiKeyIsNull = "Stack api key can not be null."; + public final static String ErrorMessage_FormName = "Please set contentType name."; + public final static String ErrorMessage_EntryUID = "Please set entry uid."; + public final static String ErrorMessage_Stack_AccessToken_IsNull = "Access token can not be null."; + public final static String ErrorMessage_Stack_Environment_IsNull = "Environment can not be null."; + public final static String ErrorMessage_VolleyNoConnectionError = "Connection error"; + public final static String ErrorMessage_VolleyAuthFailureError = "Authentication Not present."; + public final static String ErrorMessage_VolleyParseError = "Parsing Error."; + public final static String ErrorMessage_VolleyServerError = "Server interaction went wrong, Please try again."; + public final static String ErrorMessage_Default = "Oops! Something went wrong. Please try again."; + public final static String ErrorMessage_NoNetwork = "Network not available."; + public final static String ErrorMessage_CalledDefaultMethod = "You must called Contentstack.stack() first"; + public final static String ErrorMessage_QueryFilterException = "Please provide valid params."; + public final static String ErrorMessage_EntryNotFoundInCache = "ENTRY is not present in cache"; + public final static String ErrorMessage_SavingNetworkCallResponseForCache = "Error while saving network call response."; +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/utilities/CSAppUtils.java b/contentstack/src/main/java/com/contentstack/sdk/utilities/CSAppUtils.java new file mode 100755 index 00000000..abd27727 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/utilities/CSAppUtils.java @@ -0,0 +1,240 @@ +package com.contentstack.sdk.utilities; + +import android.annotation.SuppressLint; +import android.util.Log; + +import org.json.JSONObject; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.security.MessageDigest; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +/** + * @author contentstack.com + */ +public class CSAppUtils { + + public CSAppUtils() { + } + + public static void showLog(String tag, String message) { + if (CSAppConstants.debug) { + Log.i(tag, message); + } + } + + + /** + * To check if required response within given time window available in cache + * + * @param file cache file. + * @param time time + * @return true if cache data available which satisfy given time condition. + */ + public boolean getResponseTimeFromCacheFile(File file, long time) { + try { + JSONObject jsonObj = getJsonFromCacheFile(file); + long responseDate = Long.parseLong(jsonObj.optString("timestamp")); + + Calendar responseCalendar = Calendar.getInstance(); + + responseCalendar.add(Calendar.MINUTE, 0); + responseCalendar.set(Calendar.SECOND, 0); + responseCalendar.set(Calendar.MILLISECOND, 0); + responseCalendar.setTimeInMillis(responseDate); + responseCalendar.getTimeInMillis(); + + + Calendar currentCalendar = Calendar.getInstance(); + currentCalendar.setTime(new Date()); + currentCalendar.getTimeInMillis(); + + long dateDiff = (currentCalendar.getTimeInMillis() - responseCalendar.getTimeInMillis()); + long dateDiffInMin = dateDiff / (60 * 1000); + + + if (dateDiffInMin > (time / 60000)) { + return true;// need to send call. + } else { + return false;// no need to send call. + } + + } catch (Exception e) { + showLog("appUtils", e.getLocalizedMessage()); + return false; + } + } + + /** + * To retrieve data from cache. + * + * @param file cache file. + * @return cache data in JSON. + */ + public static JSONObject getJsonFromCacheFile(File file) { + + JSONObject json = null; + InputStream input = null; + ByteArrayOutputStream buffer = null; + try { + + input = new BufferedInputStream(new FileInputStream(file)); + buffer = new ByteArrayOutputStream(); + byte[] temp = new byte[1024]; + int read; + while ((read = input.read(temp)) > 0) { + buffer.write(temp, 0, read); + } + json = new JSONObject(buffer.toString("UTF-8")); + buffer.flush(); + buffer.close(); + input.close(); + return json; + } catch (Exception e) { + showLog("appUtils", "------------getJsonFromFilec catch-|" + e.toString()); + return null; + } + } + + /** + * To encrypt given value. + * + * @param value string + * @return MD5 value + */ + public String getMD5FromString(String value) { + String output; + output = value.toString().trim(); + if (value.length() > 0) { + try { + // Create MD5 Hash + MessageDigest digest = java.security.MessageDigest.getInstance("MD5"); + digest.reset(); + digest.update(output.getBytes()); + byte messageDigest[] = digest.digest(); + + // Create Hex String + // deepcode ignore ApiMigration: + StringBuffer hexString = new StringBuffer(); + for (int i = 0; i < messageDigest.length; i++) { + String hex = Integer.toHexString(0xFF & messageDigest[i]); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + + return hexString.toString(); + + } catch (Exception e) { + showLog("appUtils", e.getLocalizedMessage()); + return null; + } + } else { + return null; + } + } + + + /** + * Converts the given date to user's timezone. + * + * @param date date in ISO format. + * @return {@link Calendar} object. + * @throws ParseException

    Example :
    + *
    +     *                          Util.parseDate(dateString, TimeZone.getDefault());
    +     *                        
    + */ + public static Calendar parseDate(String date, TimeZone timeZone) { + ArrayList knownPatterns = new ArrayList<>(); + knownPatterns.add("yyyy-MM-dd'T'HH:mm:ssZ"); + knownPatterns.add("yyyy-MM-dd'T'HH:mm:ss'Z'"); + knownPatterns.add("yyyy-MM-dd'T'HH:mm.ss'Z'"); + knownPatterns.add("yyyy-MM-dd'T'HH:mmZ"); + knownPatterns.add("yyyy-MM-dd'T'HH:mm'Z'"); + knownPatterns.add("yyyy-MM-dd'T'HH:mm'Z'"); + knownPatterns.add("yyyy-MM-dd'T'HH:mm:ss"); + knownPatterns.add("yyyy-MM-dd' 'HH:mm:ss"); + knownPatterns.add("yyyy-MM-dd"); + knownPatterns.add("HH:mm:ssZ"); + knownPatterns.add("HH:mm:ss'Z'"); + + for (String formatString : knownPatterns) { + try { + return parseDate(date, formatString, timeZone); + } catch (ParseException e) { + Log.e("CSAppUtils", e.getLocalizedMessage()); + } + } + return null; + } + + /** + * Converts the given date to the user's timezone. + * + * @param date date in string format. + * @param dateFormat date format. + * @return {@link Calendar} object. + * @throws ParseException

    Example :
    + *
    +     *                          BuiltUtil.parseDate(dateString, "yyyy-MM-dd'T'HH:mm:ssZ", TimeZone.getTimeZone("GMT"));
    +     *                        
    + */ + @SuppressLint("SimpleDateFormat") + public static Calendar parseDate(String date, String dateFormat, TimeZone timeZone) throws ParseException { + Date dateObject = null; + String month = ""; + String day = ""; + String year = ""; + String hourOfDay = ""; + String min = ""; + String sec = ""; + Calendar cal = Calendar.getInstance(); + + SimpleDateFormat dateFormatter = new SimpleDateFormat(dateFormat); + dateObject = dateFormatter.parse(date); + + month = new SimpleDateFormat("MM").format(dateObject); + day = new SimpleDateFormat("dd").format(dateObject); + year = new SimpleDateFormat("yyyy").format(dateObject); + hourOfDay = new SimpleDateFormat("HH").format(dateObject); + min = new SimpleDateFormat("mm").format(dateObject); + sec = new SimpleDateFormat("ss").format(dateObject); + + if (timeZone != null) { + cal.setTimeZone(timeZone); + } else { + cal.setTimeZone(TimeZone.getDefault()); + } + + cal.set(Integer.valueOf(year), Integer.valueOf(month) - 1, Integer.valueOf(day), Integer.valueOf(hourOfDay), Integer.valueOf(min), Integer.valueOf(sec)); + + month = null; + day = null; + year = null; + hourOfDay = null; + min = null; + sec = null; + dateObject = null; + + return cal; + } + + /** + * Type to compare dates. + * + */ + public static enum DateComapareType { + WEEK, DAY, HOURS, MINUTES, SECONDS + } +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/utilities/CSController.java b/contentstack/src/main/java/com/contentstack/sdk/utilities/CSController.java new file mode 100755 index 00000000..e0250ba6 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/utilities/CSController.java @@ -0,0 +1,21 @@ +package com.contentstack.sdk.utilities; + +/** + * @author contentstack.com, Inc + */ +public class CSController { + + public static final String QUERYOBJECT = "getQueryEntries"; + + public static final String SINGLEQUERYOBJECT = "getSingleQueryEntries"; + + public static final String FETCHENTRY = "getEntry"; + + public static final String FETCHALLASSETS = "getAllAssets"; + + public static final String FETCHASSETS = "getAssets"; + + public static final String FETCHSYNC = "getSync"; + + public static final String FETCHCONTENTTYPES = "getContentTypes"; +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/utilities/ContentstackUtil.java b/contentstack/src/main/java/com/contentstack/sdk/utilities/ContentstackUtil.java new file mode 100755 index 00000000..3f1c0a91 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/utilities/ContentstackUtil.java @@ -0,0 +1,117 @@ +package com.contentstack.sdk.utilities; + +import android.annotation.SuppressLint; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +/** + * Helper class of utilities. + * + * @author contentstack.com, Inc + */ +public class ContentstackUtil { + + /** + * Converts the given date to user's timezone. + * + * @param date date in ISO format. + * @return {@link Calendar} object. + * @throws ParseException

    Example :
    + *
    +     *  Util.parseDate(dateString, TimeZone.getDefault());
    +     *  
    + */ + public static Calendar parseDate(String date, TimeZone timeZone) throws ParseException { + ArrayList knownPatterns = new ArrayList(); + knownPatterns.add("yyyy-MM-dd'T'HH:mm:ssZ"); + knownPatterns.add("yyyy-MM-dd'T'HH:mm:ss'Z'"); + knownPatterns.add("yyyy-MM-dd'T'HH:mm.ss'Z'"); + knownPatterns.add("yyyy-MM-dd'T'HH:mmZ"); + knownPatterns.add("yyyy-MM-dd'T'HH:mm'Z'"); + knownPatterns.add("yyyy-MM-dd'T'HH:mm'Z'"); + knownPatterns.add("yyyy-MM-dd'T'HH:mm:ss"); + knownPatterns.add("yyyy-MM-dd' 'HH:mm:ss"); + knownPatterns.add("yyyy-MM-dd"); + knownPatterns.add("HH:mm:ssZ"); + knownPatterns.add("HH:mm:ss'Z'"); + + for (String formatString : knownPatterns) { + try { + + return parseDate(date, formatString, timeZone); + + } catch (ParseException e) { + } + } + + return null; + } + + /** + * Converts the given date to the user's timezone. + * + * @param date date in string format. + * @param dateFormat date format. + * @return {@link Calendar} object. + * @throws ParseException

    Example :
    + *
    +     *                          Util.parseDate(dateString, "yyyy-MM-dd'T'HH:mm:ssZ", TimeZone.getTimeZone("GMT"));
    +     *                        
    + */ + @SuppressLint("SimpleDateFormat") + public static Calendar parseDate(String date, String dateFormat, TimeZone timeZone) throws ParseException { + Date dateObject = null; + String month = ""; + String day = ""; + String year = ""; + String hourOfDay = ""; + String min = ""; + String sec = ""; + Calendar cal = Calendar.getInstance(); + + SimpleDateFormat dateFormatter = new SimpleDateFormat(dateFormat); + dateObject = dateFormatter.parse(date); + + month = new SimpleDateFormat("MM").format(dateObject); + day = new SimpleDateFormat("dd").format(dateObject); + year = new SimpleDateFormat("yyyy").format(dateObject); + hourOfDay = new SimpleDateFormat("HH").format(dateObject); + min = new SimpleDateFormat("mm").format(dateObject); + sec = new SimpleDateFormat("ss").format(dateObject); + + if (timeZone != null) { + cal.setTimeZone(timeZone); + } else { + cal.setTimeZone(TimeZone.getDefault()); + } + + cal.set(Integer.valueOf(year), Integer.valueOf(month) - 1, Integer.valueOf(day), Integer.valueOf(hourOfDay), Integer.valueOf(min), Integer.valueOf(sec)); + + month = null; + day = null; + year = null; + hourOfDay = null; + min = null; + sec = null; + dateObject = null; + + return cal; + } + + /** + * Type to compare dates. + * + */ + public static enum DateComapareType { + + WEEK, DAY, HOURS, MINUTES, SECONDS + + } + + ; +} diff --git a/contentstack/src/main/java/com/contentstack/txtmark/Block.java b/contentstack/src/main/java/com/contentstack/txtmark/Block.java new file mode 100755 index 00000000..6666cbae --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/txtmark/Block.java @@ -0,0 +1,336 @@ +/* + * Copyright (C) 2011-2015 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.txtmark; + +/** + * This class represents a block of lines. + * + * @author René Jeschke + */ +class Block +{ + /** This block's type. */ + public BlockType type = BlockType.NONE; + /** Head and tail of linked lines. */ + public Line lines = null, lineTail = null; + /** Head and tail of child blocks. */ + public Block blocks = null, blockTail = null; + /** Next block. */ + public Block next = null; + /** Depth of headline BlockType. */ + public int hlDepth = 0; + /** ID for headlines and list items */ + public String id = null; + /** Block meta information */ + public String meta = ""; + + /** Constructor. */ + public Block() + { + // + } + + /** + * @return true if this block contains lines. + */ + public boolean hasLines() + { + return this.lines != null; + } + + /** + * Removes leading and trailing empty lines. + */ + public void removeSurroundingEmptyLines() + { + if (this.lines != null) + { + this.removeTrailingEmptyLines(); + this.removeLeadingEmptyLines(); + } + } + + /** + * Sets hlDepth and takes care of '#' chars. + */ + public void transfromHeadline() + { + if (this.hlDepth > 0) + { + return; + } + int level = 0; + final Line line = this.lines; + if (line.isEmpty) + { + return; + } + int start = line.leading; + while (start < line.value.length() && line.value.charAt(start) == '#') + { + level++; + start++; + } + while (start < line.value.length() && line.value.charAt(start) == ' ') + { + start++; + } + if (start >= line.value.length()) + { + line.setEmpty(); + } + else + { + int end = line.value.length() - line.trailing - 1; + while (line.value.charAt(end) == '#') + { + end--; + } + while (line.value.charAt(end) == ' ') + { + end--; + } + line.value = line.value.substring(start, end + 1); + line.leading = line.trailing = 0; + } + this.hlDepth = Math.min(level, 6); + } + + /** + * Used for nested lists. Removes list markers and up to 4 leading spaces. + * + * @param configuration + * txtmark configuration + * + */ + public void removeListIndent(final Configuration configuration) + { + Line line = this.lines; + while (line != null) + { + if (!line.isEmpty) + { + switch (line.getLineType(configuration)) + { + case ULIST: + line.value = line.value.substring(line.leading + 2); + break; + case OLIST: + line.value = line.value.substring(line.value.indexOf('.') + 2); + break; + default: + line.value = line.value.substring(Math.min(line.leading, 4)); + break; + } + line.initLeading(); + } + line = line.next; + } + } + + /** + * Used for nested block quotes. Removes '>' char. + */ + public void removeBlockQuotePrefix() + { + Line line = this.lines; + while (line != null) + { + if (!line.isEmpty) + { + if (line.value.charAt(line.leading) == '>') + { + int rem = line.leading + 1; + if (line.leading + 1 < line.value.length() && line.value.charAt(line.leading + 1) == ' ') + { + rem++; + } + line.value = line.value.substring(rem); + line.initLeading(); + } + } + line = line.next; + } + } + + /** + * Removes leading empty lines. + * + * @return true if an empty line was removed. + */ + public boolean removeLeadingEmptyLines() + { + boolean wasEmpty = false; + Line line = this.lines; + while (line != null && line.isEmpty) + { + this.removeLine(line); + line = this.lines; + wasEmpty = true; + } + return wasEmpty; + } + + /** + * Removes trailing empty lines. + */ + public void removeTrailingEmptyLines() + { + Line line = this.lineTail; + while (line != null && line.isEmpty) + { + this.removeLine(line); + line = this.lineTail; + } + } + + /** + * Splits this block's lines, creating a new child block having 'line' as + * it's lineTail. + * + * @param line + * The line to split from. + * @return The newly created Block. + */ + public Block split(final Line line) + { + final Block block = new Block(); + + block.lines = this.lines; + block.lineTail = line; + this.lines = line.next; + line.next = null; + if (this.lines == null) + { + this.lineTail = null; + } + else + { + this.lines.previous = null; + } + + if (this.blocks == null) + { + this.blocks = this.blockTail = block; + } + else + { + this.blockTail.next = block; + this.blockTail = block; + } + + return block; + } + + /** + * Removes the given line from this block. + * + * @param line + * Line to remove. + */ + public void removeLine(final Line line) + { + if (line.previous == null) + { + this.lines = line.next; + } + else + { + line.previous.next = line.next; + } + if (line.next == null) + { + this.lineTail = line.previous; + } + else + { + line.next.previous = line.previous; + } + line.previous = line.next = null; + } + + /** + * Appends the given line to this block. + * + * @param line + * Line to append. + */ + public void appendLine(final Line line) + { + if (this.lineTail == null) + { + this.lines = this.lineTail = line; + } + else + { + this.lineTail.nextEmpty = line.isEmpty; + line.prevEmpty = this.lineTail.isEmpty; + line.previous = this.lineTail; + this.lineTail.next = line; + this.lineTail = line; + } + } + + /** + * Changes all Blocks of type NONE to PARAGRAPH if + * this Block is a List and any of the ListItems contains a paragraph. + */ + public void expandListParagraphs() + { + if (this.type != BlockType.ORDERED_LIST && this.type != BlockType.UNORDERED_LIST) + { + return; + } + Block outer = this.blocks, inner; + boolean hasParagraph = false; + while (outer != null && !hasParagraph) + { + if (outer.type == BlockType.LIST_ITEM) + { + inner = outer.blocks; + while (inner != null && !hasParagraph) + { + if (inner.type == BlockType.PARAGRAPH) + { + hasParagraph = true; + } + inner = inner.next; + } + } + outer = outer.next; + } + if (hasParagraph) + { + outer = this.blocks; + while (outer != null) + { + if (outer.type == BlockType.LIST_ITEM) + { + inner = outer.blocks; + while (inner != null) + { + if (inner.type == BlockType.NONE) + { + inner.type = BlockType.PARAGRAPH; + } + inner = inner.next; + } + } + outer = outer.next; + } + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/txtmark/BlockEmitter.java b/contentstack/src/main/java/com/contentstack/txtmark/BlockEmitter.java new file mode 100755 index 00000000..509162a6 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/txtmark/BlockEmitter.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2011-2015 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.txtmark; + +import java.util.List; + +/** + * Block emitter interface. An example for a code block emitter is given below: + * + *
    + * public void emitBlock(StringBuilder out, List<String> lines, String meta)
    + * {
    + *     out.append("<pre><code>");
    + *     for(final String s : lines)
    + *     {
    + *         for(int i = 0; i < s.length(); i++)
    + *         {
    + *             final char c = s.charAt(i);
    + *             switch(c)
    + *             {
    + *             case '&':
    + *                 out.append("&amp;");
    + *                 break;
    + *             case '<':
    + *                 out.append("&lt;");
    + *                 break;
    + *             case '>':
    + *                 out.append("&gt;");
    + *                 break;
    + *             default:
    + *                 out.append(c);
    + *                 break;
    + *             }
    + *         }
    + *         out.append('\n');
    + *     }
    + *     out.append("</code></pre>\n");
    + * }
    + * 
    + * 
    + * + * + * @author René Jeschke <, rene_jeschke@yahoo.de>, + * @since 0.7 + */ +public interface BlockEmitter +{ + /** + * This method is responsible for outputting a markdown block and for any + * needed pre-processing like escaping HTML special characters. + * + * @param out + * The StringBuilder to append to + * @param lines + * List of lines + * @param meta + * Meta information as a single String (if any) or empty String + */ + public void emitBlock(StringBuilder out, List lines, String meta); +} diff --git a/contentstack/src/main/java/com/contentstack/txtmark/BlockType.java b/contentstack/src/main/java/com/contentstack/txtmark/BlockType.java new file mode 100755 index 00000000..ebe7849f --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/txtmark/BlockType.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2011-2015 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.txtmark; + +/** + * Block type enum. + * + * @author René Jeschke + */ +enum BlockType +{ + /** Unspecified. Used for root block and list items without paragraphs. */ + NONE, + /** A block quote. */ + BLOCKQUOTE, + /** A code block. */ + CODE, + /** A fenced code block. */ + FENCED_CODE, + /** A headline. */ + HEADLINE, + /** A list item. */ + LIST_ITEM, + /** An ordered list. */ + ORDERED_LIST, + /** A paragraph. */ + PARAGRAPH, + /** A horizontal ruler. */ + RULER, + /** An unordered list. */ + UNORDERED_LIST, + /** A XML block. */ + XML, + /** A GFM table */ + TABLE +} diff --git a/contentstack/src/main/java/com/contentstack/txtmark/Configuration.java b/contentstack/src/main/java/com/contentstack/txtmark/Configuration.java new file mode 100755 index 00000000..4f18063e --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/txtmark/Configuration.java @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2011-2015 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.txtmark; + +/** + * Txtmark configuration. + * + * @author René Jeschke <, rene_jeschke@yahoo.de>, + * @since 0.7 + */ +public class Configuration +{ + final boolean safeMode; + final boolean panicMode; + final String encoding; + final Decorator decorator; + final BlockEmitter codeBlockEmitter; + final boolean forceExtendedProfile; + final boolean allowSpacesInFencedDelimiters; + final SpanEmitter specialLinkEmitter; + + /** + *

    + * This is the default configuration for txtmark's process + * methods + *

    + * + *
      + *
    • safeMode = false
    • + *
    • encoding = UTF-8
    • + *
    • decorator = DefaultDecorator
    • + *
    • codeBlockEmitter = null
    • + *
    + */ + public final static Configuration DEFAULT = Configuration.builder().build(); + + /** + *

    + * Default safe configuration + *

    + * + *
      + *
    • safeMode = true
    • + *
    • encoding = UTF-8
    • + *
    • decorator = DefaultDecorator
    • + *
    • codeBlockEmitter = null
    • + *
    + */ + public final static Configuration DEFAULT_SAFE = Configuration.builder().enableSafeMode().build(); + + /** + * Constructor. + */ + Configuration(final boolean safeMode, final String encoding, final Decorator decorator, + final BlockEmitter codeBlockEmitter, + final boolean forceExtendedProfile, final SpanEmitter specialLinkEmitter, + final boolean allowSpacesInFencedDelimiters, final boolean panicMode) + { + this.safeMode = safeMode; + this.encoding = encoding; + this.decorator = decorator; + this.codeBlockEmitter = codeBlockEmitter; + this.forceExtendedProfile = forceExtendedProfile; + this.specialLinkEmitter = specialLinkEmitter; + this.allowSpacesInFencedDelimiters = allowSpacesInFencedDelimiters; + this.panicMode = panicMode; + } + + /** + * Creates a new Builder instance. + * + * @return A new Builder instance. + */ + public static Builder builder() + { + return new Builder(); + } + + /** + * Configuration builder. + * + * @author René Jeschke <, rene_jeschke@yahoo.de>, + * @since 0.7 + */ + public static class Builder + { + private boolean safeMode = false; + private boolean panicMode = false; + private boolean forceExtendedProfile = false; + private boolean allowSpacesInFencedDelimiters = true; + private String encoding = "UTF-8"; + private Decorator decorator = new DefaultDecorator(); + private BlockEmitter codeBlockEmitter = null; + private SpanEmitter specialLinkEmitter = null; + + /** + * Constructor. + * + */ + Builder() + { + // empty + } + + /** + * Enables HTML safe mode. + * + * Default: false + * + * @return This builder + * @since 0.7 + */ + public Builder enableSafeMode() + { + this.safeMode = true; + return this; + } + + /** + * Forces extened profile to be enabled by default. + * + * @return This builder. + * @since 0.7 + */ + public Builder forceExtentedProfile() + { + this.forceExtendedProfile = true; + return this; + } + + /** + * Sets the HTML safe mode flag. + * + * Default: false + * + * @param flag + * true to enable safe mode + * @return This builder + * @since 0.7 + */ + public Builder setSafeMode(final boolean flag) + { + this.safeMode = flag; + return this; + } + + /** + * Sets the character encoding for txtmark. + * + * Default: "UTF-8" + * + * @param encoding + * The encoding + * @return This builder + * @since 0.7 + */ + public Builder setEncoding(final String encoding) + { + this.encoding = encoding; + return this; + } + + /** + * Sets the decorator for txtmark. + * + * Default: DefaultDecorator() + * + * @param decorator + * The decorator + * @return This builder + * @see DefaultDecorator + * @since 0.7 + */ + public Builder setDecorator(final Decorator decorator) + { + this.decorator = decorator; + return this; + } + + /** + * Sets the code block emitter. + * + * Default: null + * + * @param emitter + * The BlockEmitter + * @return This builder + * @see BlockEmitter + * @since 0.7 + */ + public Builder setCodeBlockEmitter(final BlockEmitter emitter) + { + this.codeBlockEmitter = emitter; + return this; + } + + /** + * Sets the emitter for special link spans ([[ ... ]]). + * + * @param emitter + * The emitter. + * @return This builder. + * @since 0.7 + */ + public Builder setSpecialLinkEmitter(final SpanEmitter emitter) + { + this.specialLinkEmitter = emitter; + return this; + } + + /** + * (Dis-)Allows spaces in fenced code block delimiter lines. + * + * @param allow + * whether to allow or not + * @return This builder. + * @since 0.12 + */ + public Builder setAllowSpacesInFencedCodeBlockDelimiters(final boolean allow) + { + this.allowSpacesInFencedDelimiters = allow; + return this; + } + + /** + * This allows you to enable 'panicMode'. When 'panicMode' is enabled, + * every {@code <} encountered will then be translated into {@code <} + * + * @param panic + * whether to enable or not + * @return This builder. + * @since 0.12 + */ + public Builder setEnablePanicMode(final boolean panic) + { + this.panicMode = panic; + return this; + } + + /** + * This allows you to enable 'panicMode'. When 'panicMode' is enabled, + * every {@code <} encountered will then be translated into {@code <} + * + * @return This builder. + * @since 0.12 + */ + public Builder enablePanicMode() + { + this.panicMode = true; + return this; + } + + /** + * Builds a configuration instance. + * + * @return a Configuration instance + * @since 0.7 + */ + public Configuration build() + { + return new Configuration(this.safeMode, this.encoding, this.decorator, this.codeBlockEmitter, + this.forceExtendedProfile, this.specialLinkEmitter, this.allowSpacesInFencedDelimiters, + this.panicMode); + } + } +} diff --git a/contentstack/src/main/java/com/contentstack/txtmark/Decorator.java b/contentstack/src/main/java/com/contentstack/txtmark/Decorator.java new file mode 100755 index 00000000..ca8617a9 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/txtmark/Decorator.java @@ -0,0 +1,701 @@ +/* + * Copyright (C) 2011-2015 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.txtmark; + +/** + * Decorator interface. + * + * @author René Jeschke <, rene_jeschke@yahoo.de>, + */ +public interface Decorator +{ + /** + * Called when a paragraph is opened. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("<p>");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void openParagraph(final StringBuilder out); + + /** + * Called when a paragraph is closed. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("</p>\n");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void closeParagraph(final StringBuilder out); + + /** + * Called when a blockquote is opened. + * + * Default implementation is: + * + *
    +     * out.append("<blockquote>");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void openBlockquote(final StringBuilder out); + + /** + * Called when a blockquote is closed. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("</blockquote>\n");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void closeBlockquote(final StringBuilder out); + + /** + * Called when a code block is opened. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("<pre><code>");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void openCodeBlock(final StringBuilder out); + + /** + * Called when a code block is closed. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("</code></pre>\n");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void closeCodeBlock(final StringBuilder out); + + /** + * Called when a code span is opened. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("<code>");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void openCodeSpan(final StringBuilder out); + + /** + * Called when a code span is closed. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("</code>");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void closeCodeSpan(final StringBuilder out); + + /** + * Called when a headline is opened. + * + *

    + * Note: Don't close the HTML tag! + *

    + *

    + * Default implementation is: + *

    + * + *
    +     *  out.append("<h");
    +     * out.append(level);
    +     * 
    + * + * @param out + * The StringBuilder to write to. + * @param level + * The level to use. + */ + public void openHeadline(final StringBuilder out, int level); + + /** + * Called when a headline is closed. + * + *

    + * Default implementation is: + *

    + * + *
    +     *  out.append("</h");
    +     * out.append(level);
    +     * out.append(">\n");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + * @param level + * The level to use. + */ + public void closeHeadline(final StringBuilder out, int level); + + /** + * Called when a strong span is opened. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("<strong>");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void openStrong(final StringBuilder out); + + /** + * Called when a strong span is closed. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("</strong>");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void closeStrong(final StringBuilder out); + + /** + * Called when an emphasis span is opened. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("<em>");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void openEmphasis(final StringBuilder out); + + /** + * Called when an emphasis span is closed. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("</em>");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void closeEmphasis(final StringBuilder out); + + /** + * Called when a strikeout span is opened. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("<del>");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void openStrikeout(final StringBuilder out); + + /** + * Called when a strikeout span is closed. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("</del>");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void closeStrikeout(final StringBuilder out); + + /** + * Called when a superscript span is opened. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("<sup>");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void openSuper(final StringBuilder out); + + /** + * Called when a superscript span is closed. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("</sup>");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void closeSuper(final StringBuilder out); + + /** + * Called when an ordered list is opened. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("<ol>\n");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void openOrderedList(final StringBuilder out); + + /** + * Called when an ordered list is closed. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("</ol>\n");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void closeOrderedList(final StringBuilder out); + + /** + * Called when an unordered list is opened. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("<ul>\n");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void openUnorderedList(final StringBuilder out); + + /** + * Called when an unordered list is closed. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("</ul>\n");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void closeUnorderedList(final StringBuilder out); + + /** + * Called when a list item is opened. + * + *

    + * Note: Don't close the HTML tag! + *

    + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("<li");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void openListItem(final StringBuilder out); + + /** + * Called when a list item is closed. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("</li>\n");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void closeListItem(final StringBuilder out); + + /** + * Called when a horizontal ruler is encountered. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("<hr />\n");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void horizontalRuler(final StringBuilder out); + + /** + * Called when a link is opened. + * + *

    + * Note: Don't close the HTML tag! + *

    + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("<a");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void openLink(final StringBuilder out); + + /** + * Called when a link is closed + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("</a>");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void closeLink(final StringBuilder out); + + /** + * Called when an image is opened. + * + *

    + * Note: Don't close the HTML tag! + *

    + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("<img");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void openImage(final StringBuilder out); + + /** + * Called when an image is closed. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append(" />");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void closeImage(final StringBuilder out); + + /** + * Called when a table is opened. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("");
    +     * 
    +     *
    +     * @param out
    +     *            The StringBuilder to write to.
    +     */
    +    public void openTable(final StringBuilder out);
    +
    +    /**
    +     * Called when a table is closed.
    +     *
    +     * 

    + * Default implementation is: + *

    + * + *
    +     * out.append("
    "); + *
    + * + * @param out + * The StringBuilder to write to. + */ + public void closeTable(final StringBuilder out); + + /** + * Called when a table body is opened. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void openTableBody(final StringBuilder out); + + /** + * Called when a table body is closed. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void closeTableBody(final StringBuilder out); + + /** + * Called when a table head is opened. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void openTableHead(final StringBuilder out); + + /** + * Called when a table head is closed. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void closeTableHead(final StringBuilder out); + + /** + * Called when a table row is opened. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void openTableRow(final StringBuilder out); + + /** + * Called when a table row is closed. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void closeTableRow(final StringBuilder out); + + /** + * Called when a table data is opened. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + * @param align one of null | left | center | right. + * If null no align is set + */ + public void openTableData(final StringBuilder out, final String align); + + /** + * Called when a table data is closed. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void closeTableData(final StringBuilder out); + + /** + * Called when a table header is opened. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + * @param align one of null | left | center | right. + * If null no align is set + */ + public void openTableHeader(final StringBuilder out, final String align); + + /** + * Called when a table header is closed. + * + *

    + * Default implementation is: + *

    + * + *
    +     * out.append("");
    +     * 
    + * + * @param out + * The StringBuilder to write to. + */ + public void closeTableHeader(final StringBuilder out); +} diff --git a/contentstack/src/main/java/com/contentstack/txtmark/DefaultDecorator.java b/contentstack/src/main/java/com/contentstack/txtmark/DefaultDecorator.java new file mode 100755 index 00000000..c04a034e --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/txtmark/DefaultDecorator.java @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2011-2015 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.txtmark; + +/** + * Default Decorator implementation. + * + *

    + * Example for a user Decorator having a class attribute on <p> tags. + *

    + * + *
    + * public class MyDecorator extends DefaultDecorator
    + * {
    + *     @Override
    + *     public void openParagraph(StringBuilder out)
    + *     {
    + *         out.append("<p class=\"myclass\">");
    + *     }
    + * }
    + * 
    + * 
    + * + * @author René Jeschke <rene_jeschke@yahoo.de> + */ +public class DefaultDecorator implements Decorator +{ + /** Constructor. */ + public DefaultDecorator() + { + // empty + } + + /** @see Decorator#openParagraph(StringBuilder) */ + @Override + public void openParagraph(final StringBuilder out) + { + out.append("

    "); + } + + /** @see Decorator#closeParagraph(StringBuilder) */ + @Override + public void closeParagraph(final StringBuilder out) + { + out.append("

    \n"); + } + + /** @see Decorator#openBlockquote(StringBuilder) */ + @Override + public void openBlockquote(final StringBuilder out) + { + out.append("
    "); + } + + /** @see Decorator#closeBlockquote(StringBuilder) */ + @Override + public void closeBlockquote(final StringBuilder out) + { + out.append("
    \n"); + } + + /** @see Decorator#openCodeBlock(StringBuilder) */ + @Override + public void openCodeBlock(final StringBuilder out) + { + out.append("
    ");
    +    }
    +
    +    /** @see Decorator#closeCodeBlock(StringBuilder) */
    +    @Override
    +    public void closeCodeBlock(final StringBuilder out)
    +    {
    +        out.append("
    \n"); + } + + /** @see Decorator#openCodeSpan(StringBuilder) */ + @Override + public void openCodeSpan(final StringBuilder out) + { + out.append(""); + } + + /** @see Decorator#closeCodeSpan(StringBuilder) */ + @Override + public void closeCodeSpan(final StringBuilder out) + { + out.append(""); + } + + /** + * @see Decorator#openHeadline(StringBuilder, + * int) + */ + @Override + public void openHeadline(final StringBuilder out, final int level) + { + out.append("\n"); + } + + /** @see Decorator#openStrong(StringBuilder) */ + @Override + public void openStrong(final StringBuilder out) + { + out.append(""); + } + + /** @see Decorator#closeStrong(StringBuilder) */ + @Override + public void closeStrong(final StringBuilder out) + { + out.append(""); + } + + /** @see Decorator#openEmphasis(StringBuilder) */ + @Override + public void openEmphasis(final StringBuilder out) + { + out.append(""); + } + + /** @see Decorator#closeEmphasis(StringBuilder) */ + @Override + public void closeEmphasis(final StringBuilder out) + { + out.append(""); + } + + /** @see Decorator#openStrikeout(StringBuilder) */ + @Override + public void openStrikeout(final StringBuilder out) + { + out.append(""); + } + + /** @see Decorator#closeStrikeout(StringBuilder) */ + @Override + public void closeStrikeout(final StringBuilder out) + { + out.append(""); + } + + /** @see Decorator#openSuper(StringBuilder) */ + @Override + public void openSuper(final StringBuilder out) + { + out.append(""); + } + + /** @see Decorator#closeSuper(StringBuilder) */ + @Override + public void closeSuper(final StringBuilder out) + { + out.append(""); + } + + /** @see Decorator#openOrderedList(StringBuilder) */ + @Override + public void openOrderedList(final StringBuilder out) + { + out.append("
      \n"); + } + + /** @see Decorator#closeOrderedList(StringBuilder) */ + @Override + public void closeOrderedList(final StringBuilder out) + { + out.append("
    \n"); + } + + /** @see Decorator#openUnorderedList(StringBuilder) */ + @Override + public void openUnorderedList(final StringBuilder out) + { + out.append("
      \n"); + } + + /** @see Decorator#closeUnorderedList(StringBuilder) */ + @Override + public void closeUnorderedList(final StringBuilder out) + { + out.append("
    \n"); + } + + /** @see Decorator#openListItem(StringBuilder) */ + @Override + public void openListItem(final StringBuilder out) + { + out.append("\n"); + } + + /** @see Decorator#horizontalRuler(StringBuilder) */ + @Override + public void horizontalRuler(final StringBuilder out) + { + out.append("
    \n"); + } + + /** @see Decorator#openLink(StringBuilder) */ + @Override + public void openLink(final StringBuilder out) + { + out.append(""); + } + + /** @see Decorator#openImage(StringBuilder) */ + @Override + public void openImage(final StringBuilder out) + { + out.append(""); + } + + /** @see Decorator#openTable(java.lang.StringBuilder) */ + @Override + public void openTable(final StringBuilder out) + { + out.append("\n"); + } + + /** @see Decorator#closeTable(java.lang.StringBuilder) */ + @Override + public void closeTable(final StringBuilder out) + { + out.append("
    \n"); + } + + /** @see Decorator#openTableHead(java.lang.StringBuilder) */ + @Override + public void openTableHead(final StringBuilder out) + { + out.append("\n"); + } + + /** @see Decorator#closeTableHead(java.lang.StringBuilder) */ + @Override + public void closeTableHead(final StringBuilder out) + { + out.append("\n"); + } + + /** @see Decorator#openTableBody(java.lang.StringBuilder) */ + @Override + public void openTableBody(final StringBuilder out) + { + out.append("\n"); + } + + /** @see Decorator#closeTableBody(java.lang.StringBuilder) */ + @Override + public void closeTableBody(final StringBuilder out) + { + out.append("\n"); + } + + /** @see Decorator#openTableRow(java.lang.StringBuilder) */ + @Override + public void openTableRow(final StringBuilder out) + { + out.append("\n"); + } + + /** @see Decorator#closeTableRow(java.lang.StringBuilder) */ + @Override + public void closeTableRow(final StringBuilder out) + { + out.append("\n"); + } + + /** @see Decorator#openTableData(java.lang.StringBuilder,java.lang.String) */ + @Override + public void openTableData(final StringBuilder out, final String align) + { + if (align == null) { + out.append(""); + } else { + out.append(""); + } + } + + /** @see Decorator#closeTableData(java.lang.StringBuilder) */ + @Override + public void closeTableData(final StringBuilder out) + { + out.append(""); + } + + /** @see Decorator#openTableHeader(java.lang.StringBuilder,java.lang.String) */ + @Override + public void openTableHeader(final StringBuilder out, final String align) + { + if (align == null) { + out.append(""); + } else { + out.append(""); + } + } + + /** @see Decorator#closeTableHeader(java.lang.StringBuilder) */ + @Override + public void closeTableHeader(final StringBuilder out) + { + out.append(""); + } +} diff --git a/contentstack/src/main/java/com/contentstack/txtmark/Emitter.java b/contentstack/src/main/java/com/contentstack/txtmark/Emitter.java new file mode 100755 index 00000000..70f681fa --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/txtmark/Emitter.java @@ -0,0 +1,1200 @@ +/* + * Copyright (C) 2011-2015 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.txtmark; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; + +/** + * Emitter class responsible for generating HTML output. + * + * @author René Jeschke + */ +class Emitter +{ + /** Link references. */ + private final HashMap linkRefs = new HashMap(); + /** The configuration. */ + private final Configuration config; + /** Extension flag. */ + public boolean useExtensions = false; + + /** Constructor. */ + public Emitter(final Configuration config) + { + this.config = config; + this.useExtensions = config.forceExtendedProfile; + } + + /** + * Adds a LinkRef to this set of LinkRefs. + * + * @param key + * The key/id. + * @param linkRef + * The LinkRef. + */ + public void addLinkRef(final String key, final LinkRef linkRef) + { + this.linkRefs.put(key.toLowerCase(), linkRef); + } + + /** + * Transforms the given block recursively into HTML. + * + * @param out + * The StringBuilder to write to. + * @param root + * The Block to process. + */ + public void emit(final StringBuilder out, final Block root) + { + root.removeSurroundingEmptyLines(); + + switch (root.type) + { + case RULER: + this.config.decorator.horizontalRuler(out); + return; + case NONE: + case XML: + break; + case HEADLINE: + this.config.decorator.openHeadline(out, root.hlDepth); + if (this.useExtensions && root.id != null) + { + out.append(" id=\""); + Utils.appendCode(out, root.id, 0, root.id.length()); + out.append('"'); + } + out.append('>'); + break; + case PARAGRAPH: + this.config.decorator.openParagraph(out); + break; + case CODE: + case FENCED_CODE: + if (this.config.codeBlockEmitter == null) + { + this.config.decorator.openCodeBlock(out); + } + break; + case BLOCKQUOTE: + this.config.decorator.openBlockquote(out); + break; + case UNORDERED_LIST: + this.config.decorator.openUnorderedList(out); + break; + case ORDERED_LIST: + this.config.decorator.openOrderedList(out); + break; + case LIST_ITEM: + this.config.decorator.openListItem(out); + if (this.useExtensions && root.id != null) + { + out.append(" id=\""); + Utils.appendCode(out, root.id, 0, root.id.length()); + out.append('"'); + } + out.append('>'); + break; + case TABLE: + this.config.decorator.openTable(out); + break; + } + + if (root.hasLines()) + { + this.emitLines(out, root); + } + else + { + Block block = root.blocks; + while (block != null) + { + this.emit(out, block); + block = block.next; + } + } + + switch (root.type) + { + case RULER: + case NONE: + case XML: + break; + case HEADLINE: + this.config.decorator.closeHeadline(out, root.hlDepth); + break; + case PARAGRAPH: + this.config.decorator.closeParagraph(out); + break; + case CODE: + case FENCED_CODE: + if (this.config.codeBlockEmitter == null) + { + this.config.decorator.closeCodeBlock(out); + } + break; + case BLOCKQUOTE: + this.config.decorator.closeBlockquote(out); + break; + case UNORDERED_LIST: + this.config.decorator.closeUnorderedList(out); + break; + case ORDERED_LIST: + this.config.decorator.closeOrderedList(out); + break; + case LIST_ITEM: + this.config.decorator.closeListItem(out); + break; + case TABLE: + this.config.decorator.closeTable(out); + break; + } + } + + /** + * Transforms lines into HTML. + * + * @param out + * The StringBuilder to write to. + * @param block + * The Block to process. + */ + private void emitLines(final StringBuilder out, final Block block) + { + switch (block.type) + { + case CODE: + this.emitCodeLines(out, block.lines, block.meta, true); + break; + case FENCED_CODE: + this.emitCodeLines(out, block.lines, block.meta, false); + break; + case XML: + this.emitRawLines(out, block.lines); + break; + case TABLE: + this.emitTableLines(out, block.lines); + break; + default: + this.emitMarkedLines(out, block.lines); + break; + } + } + + /** + * Finds the position of the given Token in the given String. + * + * @param in + * The String to search on. + * @param start + * The starting character position. + * @param token + * The token to find. + * @return The position of the token or -1 if none could be found. + */ + private int findToken(final String in, final int start, final MarkToken token) + { + int pos = start; + while (pos < in.length()) + { + if (this.getToken(in, pos) == token) + { + return pos; + } + pos++; + } + return -1; + } + + /** + * Checks if there is a valid markdown link definition. + * + * @param out + * The StringBuilder containing the generated output. + * @param in + * Input String. + * @param start + * Starting position. + * @param token + * Either LINK or IMAGE. + * @return The new position or -1 if there is no valid markdown link. + */ + private int checkLink(final StringBuilder out, final String in, final int start, final MarkToken token) + { + boolean isAbbrev = false; + int pos = start + (token == MarkToken.LINK ? 1 : 2); + final StringBuilder temp = new StringBuilder(); + + temp.setLength(0); + pos = Utils.readMdLinkId(temp, in, pos); + if (pos < start) + { + return -1; + } + + final String name = temp.toString(); + String link = null, comment = null; + final int oldPos = pos++; + pos = Utils.skipSpaces(in, pos); + if (pos < start) + { + final LinkRef lr = this.linkRefs.get(name.toLowerCase()); + if (lr != null) + { + isAbbrev = lr.isAbbrev; + link = lr.link; + comment = lr.title; + pos = oldPos; + } + else + { + return -1; + } + } + else if (in.charAt(pos) == '(') + { + pos++; + pos = Utils.skipSpaces(in, pos); + if (pos < start) + { + return -1; + } + temp.setLength(0); + final boolean useLt = in.charAt(pos) == '<'; + pos = useLt ? Utils.readUntil(temp, in, pos + 1, '>') : Utils.readMdLink(temp, in, pos); + if (pos < start) + { + return -1; + } + if (useLt) + { + pos++; + } + link = temp.toString(); + + if (in.charAt(pos) == ' ') + { + pos = Utils.skipSpaces(in, pos); + if (pos > start && in.charAt(pos) == '"') + { + pos++; + temp.setLength(0); + pos = Utils.readUntil(temp, in, pos, '"'); + if (pos < start) + { + return -1; + } + comment = temp.toString(); + pos++; + pos = Utils.skipSpaces(in, pos); + if (pos == -1) + { + return -1; + } + } + } + if (in.charAt(pos) != ')') + { + return -1; + } + } + else if (in.charAt(pos) == '[') + { + pos++; + temp.setLength(0); + pos = Utils.readRawUntil(temp, in, pos, ']'); + if (pos < start) + { + return -1; + } + final String id = temp.length() > 0 ? temp.toString() : name; + final LinkRef lr = this.linkRefs.get(id.toLowerCase()); + if (lr != null) + { + link = lr.link; + comment = lr.title; + } + } + else + { + final LinkRef lr = this.linkRefs.get(name.toLowerCase()); + if (lr != null) + { + isAbbrev = lr.isAbbrev; + link = lr.link; + comment = lr.title; + pos = oldPos; + } + else + { + return -1; + } + } + + if (link == null) + { + return -1; + } + + if (token == MarkToken.LINK) + { + if (isAbbrev && comment != null) + { + if (!this.useExtensions) + { + return -1; + } + out.append(""); + this.recursiveEmitLine(out, name, 0, MarkToken.NONE); + out.append(""); + } + else + { + this.config.decorator.openLink(out); + out.append(" href=\""); + Utils.appendValue(out, link, 0, link.length()); + out.append('"'); + if (comment != null) + { + out.append(" title=\""); + Utils.appendValue(out, comment, 0, comment.length()); + out.append('"'); + } + out.append('>'); + this.recursiveEmitLine(out, name, 0, MarkToken.LINK); + this.config.decorator.closeLink(out); + } + } + else + { + this.config.decorator.openImage(out); + out.append(" src=\""); + Utils.appendValue(out, link, 0, link.length()); + out.append("\" alt=\""); + Utils.appendValue(out, name, 0, name.length()); + out.append('"'); + if (comment != null) + { + out.append(" title=\""); + Utils.appendValue(out, comment, 0, comment.length()); + out.append('"'); + } + this.config.decorator.closeImage(out); + } + + return pos; + } + + /** + * Check if there is a valid HTML tag here. This method also transforms auto + * links and mailto auto links. + * + * @param out + * The StringBuilder to write to. + * @param in + * Input String. + * @param start + * Starting position. + * @return The new position or -1 if nothing valid has been found. + */ + private int checkHtml(final StringBuilder out, final String in, final int start) + { + final StringBuilder temp = new StringBuilder(); + int pos; + + // Check for auto links + temp.setLength(0); + pos = Utils.readUntil(temp, in, start + 1, ':', ' ', '>', '\n'); + if (pos != -1 && in.charAt(pos) == ':' && HTML.isLinkPrefix(temp.toString())) + { + pos = Utils.readUntil(temp, in, pos, '>'); + if (pos != -1) + { + final String link = temp.toString(); + this.config.decorator.openLink(out); + out.append(" href=\""); + Utils.appendValue(out, link, 0, link.length()); + out.append("\">"); + Utils.appendValue(out, link, 0, link.length()); + this.config.decorator.closeLink(out); + return pos; + } + } + + // Check for mailto auto link + temp.setLength(0); + pos = Utils.readUntil(temp, in, start + 1, '@', ' ', '>', '\n'); + if (pos != -1 && in.charAt(pos) == '@') + { + pos = Utils.readUntil(temp, in, pos, '>'); + if (pos != -1) + { + final String link = temp.toString(); + this.config.decorator.openLink(out); + out.append(" href=\""); + Utils.appendMailto(out, "mailto:", 0, 7); + Utils.appendMailto(out, link, 0, link.length()); + out.append("\">"); + Utils.appendMailto(out, link, 0, link.length()); + this.config.decorator.closeLink(out); + return pos; + } + } + + // Check for inline html + if (start + 2 < in.length()) + { + temp.setLength(0); + return Utils.readXML(out, in, start, this.config.safeMode); + } + + return -1; + } + + /** + * Check if this is a valid XML/HTML entity. + * + * @param out + * The StringBuilder to write to. + * @param in + * Input String. + * @param start + * Starting position + * @return The new position or -1 if this entity in invalid. + */ + private static int checkEntity(final StringBuilder out, final String in, final int start) + { + final int pos = Utils.readUntil(out, in, start, ';'); + if (pos < 0 || out.length() < 3) + { + return -1; + } + if (out.charAt(1) == '#') + { + if (out.charAt(2) == 'x' || out.charAt(2) == 'X') + { + if (out.length() < 4) + { + return -1; + } + for (int i = 3; i < out.length(); i++) + { + final char c = out.charAt(i); + if ((c < '0' || c > '9') && ((c < 'a' || c > 'f') && (c < 'A' || c > 'F'))) + { + return -1; + } + } + } + else + { + for (int i = 2; i < out.length(); i++) + { + final char c = out.charAt(i); + if (c < '0' || c > '9') + { + return -1; + } + } + } + out.append(';'); + } + else + { + for (int i = 1; i < out.length(); i++) + { + final char c = out.charAt(i); + if (!Character.isLetterOrDigit(c)) + { + return -1; + } + } + out.append(';'); + return HTML.isEntity(out.toString()) ? pos : -1; + } + + return pos; + } + + /** + * Recursively scans through the given line, taking care of any markdown + * stuff. + * + * @param out + * The StringBuilder to write to. + * @param in + * Input String. + * @param start + * Start position. + * @param token + * The matching Token (for e.g. '*') + * @return The position of the matching Token or -1 if token was NONE or no + * Token could be found. + */ + private int recursiveEmitLine(final StringBuilder out, final String in, final int start, final MarkToken token) + { + int pos = start, a, b; + final StringBuilder temp = new StringBuilder(); + while (pos < in.length()) + { + final MarkToken mt = this.getToken(in, pos); + if (token != MarkToken.NONE && token != MarkToken.LINK + && (mt == token || token == MarkToken.EM_STAR && mt == MarkToken.STRONG_STAR || token == MarkToken.EM_UNDERSCORE + && mt == MarkToken.STRONG_UNDERSCORE)) + { + return pos; + } + + switch (mt) + { + case IMAGE: + case LINK: + temp.setLength(0); + b = this.checkLink(temp, in, pos, mt); + if (b > 0) + { + out.append(temp); + pos = b; + } + else + { + out.append(in.charAt(pos)); + } + break; + case EM_STAR: + case EM_UNDERSCORE: + temp.setLength(0); + b = this.recursiveEmitLine(temp, in, pos + 1, mt); + if (b > 0) + { + this.config.decorator.openEmphasis(out); + out.append(temp); + this.config.decorator.closeEmphasis(out); + pos = b; + } + else + { + out.append(in.charAt(pos)); + } + break; + case STRONG_STAR: + case STRONG_UNDERSCORE: + temp.setLength(0); + b = this.recursiveEmitLine(temp, in, pos + 2, mt); + if (b > 0) + { + this.config.decorator.openStrong(out); + out.append(temp); + this.config.decorator.closeStrong(out); + pos = b + 1; + } + else + { + out.append(in.charAt(pos)); + } + break; + case STRIKEOUT: + temp.setLength(0); + b = this.recursiveEmitLine(temp, in, pos + 2, mt); + if (b > 0) + { + this.config.decorator.openStrikeout(out); + out.append(temp); + this.config.decorator.closeStrikeout(out); + pos = b + 1; + } + else + { + out.append(in.charAt(pos)); + } + break; + case SUPER: + temp.setLength(0); + b = this.recursiveEmitLine(temp, in, pos + 1, mt); + if (b > 0) + { + this.config.decorator.openSuper(out); + out.append(temp); + this.config.decorator.closeSuper(out); + pos = b; + } + else + { + out.append(in.charAt(pos)); + } + break; + case CODE_SINGLE: + case CODE_DOUBLE: + a = pos + (mt == MarkToken.CODE_DOUBLE ? 2 : 1); + b = this.findToken(in, a, mt); + if (b > 0) + { + pos = b + (mt == MarkToken.CODE_DOUBLE ? 1 : 0); + while (a < b && in.charAt(a) == ' ') + { + a++; + } + if (a < b) + { + while (in.charAt(b - 1) == ' ') + { + b--; + } + this.config.decorator.openCodeSpan(out); + Utils.appendCode(out, in, a, b); + this.config.decorator.closeCodeSpan(out); + } + } + else + { + out.append(in.charAt(pos)); + } + break; + case HTML: + temp.setLength(0); + b = this.checkHtml(temp, in, pos); + if (b > 0) + { + out.append(temp); + pos = b; + } + else + { + out.append("<"); + } + break; + case ENTITY: + temp.setLength(0); + b = checkEntity(temp, in, pos); + if (b > 0) + { + out.append(temp); + pos = b; + } + else + { + out.append("&"); + } + break; + case GFM_AUTOLINK: + if (token == MarkToken.LINK) { + out.append(in.charAt(pos)); + break; + } + temp.setLength(0); + b = checkGFMAutolink(temp, in, pos); + if (b > 0) + { + String url = temp.toString(); + this.config.decorator.openLink(out); + out.append(" href=\""); + Utils.appendValue(out, url, 0, url.length()); + out.append("\">"); + Utils.appendCode(out, url, 0, url.length()); + this.config.decorator.closeLink(out); + pos += url.length()-1; + } + else + { + out.append(in.charAt(pos)); + } + break; + case X_LINK_OPEN: + temp.setLength(0); + b = this.recursiveEmitLine(temp, in, pos + 2, MarkToken.X_LINK_CLOSE); + if (b > 0 && this.config.specialLinkEmitter != null) + { + this.config.specialLinkEmitter.emitSpan(out, temp.toString()); + pos = b + 1; + } + else + { + out.append(in.charAt(pos)); + } + break; + case X_COPY: + out.append("©"); + pos += 2; + break; + case X_REG: + out.append("®"); + pos += 2; + break; + case X_TRADE: + out.append("™"); + pos += 3; + break; + case X_NDASH: + out.append("–"); + pos++; + break; + case X_MDASH: + out.append("—"); + pos += 2; + break; + case X_HELLIP: + out.append("…"); + pos += 2; + break; + case X_LAQUO: + out.append("«"); + pos++; + break; + case X_RAQUO: + out.append("»"); + pos++; + break; + case X_RDQUO: + out.append("”"); + break; + case X_LDQUO: + out.append("“"); + break; + case ESCAPE: + pos++; + //$FALL-THROUGH$ + default: + out.append(in.charAt(pos)); + break; + } + pos++; + } + return -1; + } + + /** + * Turns every whitespace character into a space character. + * + * @param c + * Character to check + * @return 32 is c was a whitespace, c otherwise + */ + private static char whitespaceToSpace(final char c) + { + return Character.isWhitespace(c) ? ' ' : c; + } + + /** + * Check if there is any markdown Token. + * + * @param in + * Input String. + * @param pos + * Starting position. + * @return The Token. + */ + private MarkToken getToken(final String in, final int pos) + { + final char c0 = pos > 0 ? whitespaceToSpace(in.charAt(pos - 1)) : ' '; + final char c = whitespaceToSpace(in.charAt(pos)); + final char c1 = pos + 1 < in.length() ? whitespaceToSpace(in.charAt(pos + 1)) : ' '; + final char c2 = pos + 2 < in.length() ? whitespaceToSpace(in.charAt(pos + 2)) : ' '; + final char c3 = pos + 3 < in.length() ? whitespaceToSpace(in.charAt(pos + 3)) : ' '; + + switch (c) + { + case '*': + if (c1 == '*') + { + return c0 != ' ' || c2 != ' ' ? MarkToken.STRONG_STAR : MarkToken.EM_STAR; + } + return c0 != ' ' || c1 != ' ' ? MarkToken.EM_STAR : MarkToken.NONE; + case '_': + if (c1 == '_') + { + return c0 != ' ' || c2 != ' ' ? MarkToken.STRONG_UNDERSCORE : MarkToken.EM_UNDERSCORE; + } + if (this.useExtensions) + { + return Character.isLetterOrDigit(c0) && c0 != '_' && Character.isLetterOrDigit(c1) ? MarkToken.NONE + : MarkToken.EM_UNDERSCORE; + } + return c0 != ' ' || c1 != ' ' ? MarkToken.EM_UNDERSCORE : MarkToken.NONE; + case '~': + if (this.useExtensions && c1 == '~') { + return MarkToken.STRIKEOUT; + } + return MarkToken.NONE; + case '!': + if (c1 == '[') + { + return MarkToken.IMAGE; + } + return MarkToken.NONE; + case '[': + if (this.useExtensions && c1 == '[') + { + return MarkToken.X_LINK_OPEN; + } + return MarkToken.LINK; + case ']': + if (this.useExtensions && c1 == ']') + { + return MarkToken.X_LINK_CLOSE; + } + return MarkToken.NONE; + case '`': + return c1 == '`' ? MarkToken.CODE_DOUBLE : MarkToken.CODE_SINGLE; + case '\\': + switch (c1) + { + case '\\': + case '[': + case ']': + case '(': + case ')': + case '{': + case '}': + case '#': + case '"': + case '\'': + case '.': + case '>': + case '<': + case '*': + case '+': + case '-': + case '_': + case '!': + case '`': + case '~': + case '^': + return MarkToken.ESCAPE; + default: + return MarkToken.NONE; + } + case '<': + if (this.useExtensions && c1 == '<') + { + return MarkToken.X_LAQUO; + } + return MarkToken.HTML; + case '&': + return MarkToken.ENTITY; + default: + if (this.useExtensions) + { + if (c0 == ' ' && c == 'h' && c1 == 't' && c2 == 't' && c3 == 'p' + && in.startsWith("://", (pos + 4 < in.length() && in.charAt(pos + 4) == 's') ? pos+5 : pos+4)) { + return MarkToken.GFM_AUTOLINK; + } + switch (c) + { + case '-': + if (c1 == '-') + { + return c2 == '-' ? MarkToken.X_MDASH : MarkToken.X_NDASH; + } + break; + case '^': + return c0 == '^' || c1 == '^' ? MarkToken.NONE : MarkToken.SUPER; + case '>': + if (c1 == '>') + { + return MarkToken.X_RAQUO; + } + break; + case '.': + if (c1 == '.' && c2 == '.') + { + return MarkToken.X_HELLIP; + } + break; + case '(': + if (c1 == 'C' && c2 == ')') + { + return MarkToken.X_COPY; + } + if (c1 == 'R' && c2 == ')') + { + return MarkToken.X_REG; + } + if (c1 == 'T' & c2 == 'M' & c3 == ')') + { + return MarkToken.X_TRADE; + } + break; + case '"': + if (!Character.isLetterOrDigit(c0) && c1 != ' ') + { + return MarkToken.X_LDQUO; + } + if (c0 != ' ' && !Character.isLetterOrDigit(c1)) + { + return MarkToken.X_RDQUO; + } + break; + } + } + return MarkToken.NONE; + } + } + + /** + * Writes a set of markdown lines into the StringBuilder. + * + * @param out + * The StringBuilder to write to. + * @param lines + * The lines to write. + */ + private void emitMarkedLines(final StringBuilder out, final Line lines) + { + final StringBuilder in = new StringBuilder(); + Line line = lines; + while (line != null) + { + if (!line.isEmpty) + { + in.append(line.value.substring(line.leading, line.value.length() - line.trailing)); + if (line.trailing >= 2) + { + in.append("
    "); + } + } + if (line.next != null) + { + in.append('\n'); + } + line = line.next; + } + + this.recursiveEmitLine(out, in.toString(), 0, MarkToken.NONE); + } + + /** + * Writes a set of raw lines into the StringBuilder. + * + * @param out + * The StringBuilder to write to. + * @param lines + * The lines to write. + */ + private void emitRawLines(final StringBuilder out, final Line lines) + { + Line line = lines; + if (this.config.safeMode) + { + final StringBuilder temp = new StringBuilder(); + while (line != null) + { + if (!line.isEmpty) + { + temp.append(line.value); + } + temp.append('\n'); + line = line.next; + } + final String in = temp.toString(); + for (int pos = 0; pos < in.length(); pos++) + { + if (in.charAt(pos) == '<') + { + temp.setLength(0); + final int t = Utils.readXML(temp, in, pos, this.config.safeMode); + if (t != -1) + { + out.append(temp); + pos = t; + } + else + { + out.append(in.charAt(pos)); + } + } + else + { + out.append(in.charAt(pos)); + } + } + } + else + { + while (line != null) + { + if (!line.isEmpty) + { + out.append(line.value); + } + out.append('\n'); + line = line.next; + } + } + } + + /** + * Writes a code block into the StringBuilder. + * + * @param out + * The StringBuilder to write to. + * @param lines + * The lines to write. + * @param meta + * Meta information. + */ + private void emitCodeLines(final StringBuilder out, final Line lines, final String meta, final boolean removeIndent) + { + Line line = lines; + if (this.config.codeBlockEmitter != null) + { + final ArrayList list = new ArrayList(); + while (line != null) + { + if (line.isEmpty) + { + list.add(""); + } + else + { + list.add(removeIndent ? line.value.substring(4) : line.value); + } + line = line.next; + } + this.config.codeBlockEmitter.emitBlock(out, list, meta); + } + else + { + while (line != null) + { + if (!line.isEmpty) + { + for (int i = removeIndent ? 4 : 0; i < line.value.length(); i++) + { + final char c; + switch (c = line.value.charAt(i)) + { + case '&': + out.append("&"); + break; + case '<': + out.append("<"); + break; + case '>': + out.append(">"); + break; + default: + out.append(c); + break; + } + } + } + out.append('\n'); + line = line.next; + } + } + } + + private void emitTableLines(final StringBuilder out, final Line lines) { + Line line = lines; + if (line == null) { + return; + } + TableDef table = (TableDef)line.data; + Decorator decorator = this.config.decorator; + // emit header row + decorator.openTableHead(out); + decorator.openTableRow(out); + int i = 0; + for (String cellText : table.header) { + decorator.openTableHeader(out, table.getAlign(i++)); + recursiveEmitLine(out, cellText, 0, MarkToken.NONE); + decorator.closeTableHeader(out); + } + decorator.closeTableRow(out); + decorator.closeTableHead(out); + // emit rows + decorator.openTableBody(out); + for (LinkedList row : table.rows) { + decorator.openTableRow(out); + i = 0; + for (String cellText : row) { + decorator.openTableData(out, table.getAlign(i++)); + recursiveEmitLine(out, cellText, 0, MarkToken.NONE); + decorator.closeTableData(out); + } + decorator.closeTableRow(out); + } + decorator.closeTableBody(out); + } + + private static int checkGFMAutolink(final StringBuilder out, final String in, final int start) { + int s = in.indexOf("://", start) + 3; + if (s == -1) { + return -1; + } + + // find first non space preceding char + int i=start-1; + char c = 0; + while (i>-1) { + c = in.charAt(i); + if (!Character.isWhitespace(c)) { + break; + } + i--; + } + // the following characters are not allowed to precede the url + if (c == '>' || c == '<' || c == '(' ||c == '[' || c == '"' || c == '\'') { + return -1; + } + + // The links cannot contains: ", ', ), <, > or spaces. I they end in \s*", \s*', \s*), >\s* or\s*< they are not treated as a link + // Also a link must start with a space (or at beginning of the line) + // to avoid conflicting with real markdown link definitions: [..](url "title") + + int len = in.length(); + c = 0; + i = s; + out.append(in, start, s); + while (i < len) { + c = in.charAt(i); + if (c == '>' || c == '<' || c=='"' || c=='\'' || c==')') { + return -1; // not an GFM auto link + } + if (Character.isWhitespace(c)) { + // get the first non space char + int k = Utils.skipSpaces(in, i+1); + if (k > -1) { + c = in.charAt(i); + if (c == '>' || c == '<' || c=='"' || c=='\'' || c==')') { + return -1; // not an GFM auto link + } + } + break; + } + out.append(c); + i++; + } + + // remove any ending punctuation marks if needed + if (c == '.' || c == ',' || c == ';' || c == '?' || c == '!' || c == ':') { + out.setLength(out.length()-1); + i--; + } + if (i <= s) { + // invalid url + return -1; + } + return i; + } + +} diff --git a/contentstack/src/main/java/com/contentstack/txtmark/HTML.java b/contentstack/src/main/java/com/contentstack/txtmark/HTML.java new file mode 100755 index 00000000..ac0fb2ac --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/txtmark/HTML.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2011-2015 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.txtmark; + +import java.util.HashMap; +import java.util.HashSet; + +/** + * HTML utility class. + * + * @author René Jeschke + */ +class HTML +{ + /** List of valid HTML/XML entity names. */ + private final static String[] ENTITY_NAMES = { + "Â", "â", "´", "Æ", "æ", "À", "à", "ℵ", + "Α", "α", "&", "∧", "∠", "'", "Å", "å", + "≈", "Ã", "ã", "Ä", "ä", "„", "Β", "β", + "¦", "•", "∩", "Ç", "ç", "¸", "¢", "Χ", + "χ", "ˆ", "♣", "≅", "©", "↵", "∪", "¤", + "‡", "†", "⇓", "↓", "°", "Δ", "δ", "♦", + "÷", "É", "é", "Ê", "ê", "È", "è", "∅", + " ", " ", "Ε", "ε", "≡", "Η", "η", "Ð", + "ð", "Ë", "ë", "€", "∃", "ƒ", "∀", "½", + "¼", "¾", "⁄", "Γ", "γ", "≥", ">", "⇔", + "↔", "♥", "…", "Í", "í", "Î", "î", "¡", + "Ì", "ì", "ℑ", "∞", "∫", "Ι", "ι", "¿", + "∈", "Ï", "ï", "Κ", "κ", "Λ", "λ", "⟨", + "«", "⇐", "←", "⌈", "“", "≤", "⌊", "∗", + "◊", "‎", "‹", "‘", "<", "¯", "—", "µ", + "·", "−", "Μ", "μ", "∇", " ", "–", "≠", + "∋", "¬", "∉", "⊄", "Ñ", "ñ", "Ν", "ν", + "Ó", "ó", "Ô", "ô", "Œ", "œ", "Ò", "ò", + "‾", "Ω", "ω", "Ο", "ο", "⊕", "∨", "ª", + "º", "Ø", "ø", "Õ", "õ", "⊗", "Ö", "ö", + "¶", "∂", "‰", "⊥", "Φ", "φ", "Π", "π", + "ϖ", "±", "£", "″", "′", "∏", "∝", "Ψ", + "ψ", """, "√", "⟩", "»", "⇒", "→", "⌉", + "”", "ℜ", "®", "⌋", "Ρ", "ρ", "‏", "›", + "’", "‚", "Š", "š", "⋅", "§", "­", "Σ", + "σ", "ς", "∼", "♠", "⊂", "⊆", "∑", "⊃", + "¹", "²", "³", "⊇", "ß", "Τ", "τ", "∴", + "Θ", "θ", "ϑ", " ", "þ", "˜", "×", "™", + "Ú", "ú", "⇑", "↑", "Û", "û", "Ù", "ù", + "¨", "ϒ", "Υ", "υ", "Ü", "ü", "℘", "Ξ", + "ξ", "Ý", "ý", "¥", "Ÿ", "ÿ", "Ζ", "ζ", + "‍", "‌" + }; + /** Characters corresponding to ENTITY_NAMES. */ + private final static char[] ENTITY_CHARS = { + '\u00C2', '\u00E2', '\u00B4', '\u00C6', '\u00E6', '\u00C0', '\u00E0', '\u2135', + '\u0391', '\u03B1', '\u0026', '\u2227', '\u2220', '\'', '\u00C5', '\u00E5', + '\u2248', '\u00C3', '\u00E3', '\u00C4', '\u00E4', '\u201E', '\u0392', '\u03B2', + '\u00A6', '\u2022', '\u2229', '\u00C7', '\u00E7', '\u00B8', '\u00A2', '\u03A7', + '\u03C7', '\u02C6', '\u2663', '\u2245', '\u00A9', '\u21B5', '\u222A', '\u00A4', + '\u2021', '\u2020', '\u21D3', '\u2193', '\u00B0', '\u0394', '\u03B4', '\u2666', + '\u00F7', '\u00C9', '\u00E9', '\u00CA', '\u00EA', '\u00C8', '\u00E8', '\u2205', + '\u2003', '\u2002', '\u0395', '\u03B5', '\u2261', '\u0397', '\u03B7', '\u00D0', + '\u00F0', '\u00CB', '\u00EB', '\u20AC', '\u2203', '\u0192', '\u2200', '\u00BD', + '\u00BC', '\u00BE', '\u2044', '\u0393', '\u03B3', '\u2265', '\u003E', '\u21D4', + '\u2194', '\u2665', '\u2026', '\u00CD', '\u00ED', '\u00CE', '\u00EE', '\u00A1', + '\u00CC', '\u00EC', '\u2111', '\u221E', '\u222B', '\u0399', '\u03B9', '\u00BF', + '\u2208', '\u00CF', '\u00EF', '\u039A', '\u03BA', '\u039B', '\u03BB', '\u2329', + '\u00AB', '\u21D0', '\u2190', '\u2308', '\u201C', '\u2264', '\u230A', '\u2217', + '\u25CA', '\u200E', '\u2039', '\u2018', '\u003C', '\u00AF', '\u2014', '\u00B5', + '\u00B7', '\u2212', '\u039C', '\u03BC', '\u2207', '\u00A0', '\u2013', '\u2260', + '\u220B', '\u00AC', '\u2209', '\u2284', '\u00D1', '\u00F1', '\u039D', '\u03BD', + '\u00D3', '\u00F3', '\u00D4', '\u00F4', '\u0152', '\u0153', '\u00D2', '\u00F2', + '\u203E', '\u03A9', '\u03C9', '\u039F', '\u03BF', '\u2295', '\u2228', '\u00AA', + '\u00BA', '\u00D8', '\u00F8', '\u00D5', '\u00F5', '\u2297', '\u00D6', '\u00F6', + '\u00B6', '\u2202', '\u2030', '\u22A5', '\u03A6', '\u03C6', '\u03A0', '\u03C0', + '\u03D6', '\u00B1', '\u00A3', '\u2033', '\u2032', '\u220F', '\u221D', '\u03A8', + '\u03C8', '\u0022', '\u221A', '\u232A', '\u00BB', '\u21D2', '\u2192', '\u2309', + '\u201D', '\u211C', '\u00AE', '\u230B', '\u03A1', '\u03C1', '\u200F', '\u203A', + '\u2019', '\u201A', '\u0160', '\u0161', '\u22C5', '\u00A7', '\u00AD', '\u03A3', + '\u03C3', '\u03C2', '\u223C', '\u2660', '\u2282', '\u2286', '\u2211', '\u2283', + '\u00B9', '\u00B2', '\u00B3', '\u2287', '\u00DF', '\u03A4', '\u03C4', '\u2***REMOVED***', + '\u0398', '\u03B8', '\u03D1', '\u00DE', '\u00FE', '\u02DC', '\u00D7', '\u2122', + '\u00DA', '\u00FA', '\u21D1', '\u2191', '\u00DB', '\u00FB', '\u00D9', '\u00F9', + '\u00A8', '\u03D2', '\u03A5', '\u03C5', '\u00DC', '\u00FC', '\u2118', '\u039E', + '\u03BE', '\u00DD', '\u00FD', '\u00A5', '\u0178', '\u00FF', '\u0396', '\u03B6', + '\u200D', '\u200C' + }; + /** Valid markdown link prefixes for auto links. */ + private final static String[] LINK_PREFIXES = { + "http", "https", + "ftp", "ftps" + }; + + /** HTML block level elements. */ + private final static HTMLElement[] BLOCK_ELEMENTS = { + HTMLElement.address, + HTMLElement.blockquote, + HTMLElement.del, HTMLElement.div, HTMLElement.dl, + HTMLElement.fieldset, HTMLElement.form, + HTMLElement.h1, HTMLElement.h2, HTMLElement.h3, HTMLElement.h4, HTMLElement.h5, HTMLElement.h6, HTMLElement.hr, + HTMLElement.ins, + HTMLElement.noscript, + HTMLElement.ol, + HTMLElement.p, HTMLElement.pre, + HTMLElement.table, + HTMLElement.ul + }; + + /** HTML unsafe elements. */ + private final static HTMLElement[] UNSAFE_ELEMENTS = { + HTMLElement.applet, + HTMLElement.head, + HTMLElement.html, + HTMLElement.body, + HTMLElement.frame, + HTMLElement.frameset, + HTMLElement.iframe, + HTMLElement.script, + HTMLElement.object, + }; + + /** Character to entity encoding map. */ + private final static HashMap encodeMap = new HashMap(); + /** Entity to character decoding map. */ + private final static HashMap decodeMap = new HashMap(); + /** Set of valid HTML tags. */ + private final static HashSet HTML_ELEMENTS = new HashSet(); + /** Set of unsafe HTML tags. */ + private final static HashSet HTML_UNSAFE = new HashSet(); + /** Set of HTML block level tags. */ + private final static HashSet HTML_BLOCK_ELEMENTS = new HashSet(); + /** Set of valid markdown link prefixes. */ + private final static HashSet LINK_PREFIX = new HashSet(); + + static + { + for (final HTMLElement h : HTMLElement.values()) + { + HTML_ELEMENTS.add(h.toString()); + } + for (final HTMLElement h : UNSAFE_ELEMENTS) + { + HTML_UNSAFE.add(h.toString()); + } + for (final HTMLElement h : BLOCK_ELEMENTS) + { + HTML_BLOCK_ELEMENTS.add(h.toString()); + } + for (int i = 0; i < ENTITY_NAMES.length; i++) + { + encodeMap.put(ENTITY_CHARS[i], ENTITY_NAMES[i]); + decodeMap.put(ENTITY_NAMES[i], ENTITY_CHARS[i]); + } + for (int i = 0; i < LINK_PREFIXES.length; i++) + { + LINK_PREFIX.add(LINK_PREFIXES[i]); + } + } + + /** Constructor. (Singleton) */ + private HTML() + { + // + } + + /** + * @param value + * String to check. + * @return Returns true if the given String is a link prefix. + */ + public final static boolean isLinkPrefix(final String value) + { + return LINK_PREFIX.contains(value); + } + + /** + * @param value + * String to check. + * @return Returns true if the given String is an entity. + */ + public final static boolean isEntity(final String value) + { + return decodeMap.containsKey(value); + } + + /** + * @param value + * String to check. + * @return Returns true if the given String is a HTML tag. + */ + public final static boolean isHtmlElement(final String value) + { + return HTML_ELEMENTS.contains(value); + } + + /** + * @param value + * String to check. + * @return Returns true if the given String is a HTML block + * level tag. + */ + public final static boolean isHtmlBlockElement(final String value) + { + return HTML_BLOCK_ELEMENTS.contains(value); + } + + /** + * @param value + * String to check. + * @return Returns true if the given String is an unsafe HTML + * tag. + */ + public final static boolean isUnsafeHtmlElement(final String value) + { + return HTML_UNSAFE.contains(value); + } +} diff --git a/contentstack/src/main/java/com/contentstack/txtmark/HTMLElement.java b/contentstack/src/main/java/com/contentstack/txtmark/HTMLElement.java new file mode 100755 index 00000000..da17eca9 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/txtmark/HTMLElement.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2011-2015 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.txtmark; + +/** + * Enum of HTML tags. + * + * @author Shailesh Mishra + */ +enum HTMLElement +{ + NONE, + a, abbr, acronym, address, applet, area, + b, base, basefont, bdo, big, blockquote, body, br, button, + caption, cite, code, col, colgroup, + dd, del, dfn, div, dl, dt, + em, + fieldset, font, form, frame, frameset, + h1, h2, h3, h4, h5, h6, head, hr, html, + i, iframe, img, input, ins, + kbd, + label, legend, li, link, + map, meta, + noscript, + object, ol, optgroup, option, + p, param, pre, + q, + s, samp, script, select, small, span, strike, strong, style, sub, sup, + table, tbody, td, textarea, tfoot, th, thead, title, tr, tt, + u, ul, + var +} diff --git a/contentstack/src/main/java/com/contentstack/txtmark/Line.java b/contentstack/src/main/java/com/contentstack/txtmark/Line.java new file mode 100755 index 00000000..045a78c0 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/txtmark/Line.java @@ -0,0 +1,607 @@ +/* + * Copyright (C) 2011-2015 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.txtmark; + +import java.util.LinkedList; + +/** + * This class represents a text line. + * + *

    + * It also provides methods for processing and analyzing a line. + *

    + * + * @author René Jeschke + */ +class Line +{ + /** Current cursor position. */ + public int pos; + /** Leading and trailing spaces. */ + public int leading = 0, trailing = 0; + /** Is this line empty? */ + public boolean isEmpty = true; + /** This line's value. */ + public String value = null; + /** Previous and next line. */ + public Line previous = null, next = null; + /** Is previous/next line empty? */ + public boolean prevEmpty, nextEmpty; + /** Final line of a XML block. */ + public Line xmlEndLine; + /** additional data associated with that line if any */ + public Object data = null; + + /** Constructor. */ + public Line() + { + // + } + + /** + * Calculates leading and trailing spaces. Also sets empty if needed. + */ + public void init() + { + this.leading = 0; + while (this.leading < this.value.length() && this.value.charAt(this.leading) == ' ') + { + this.leading++; + } + + if (this.leading == this.value.length()) + { + this.setEmpty(); + } + else + { + this.isEmpty = false; + this.trailing = 0; + while (this.value.charAt(this.value.length() - this.trailing - 1) == ' ') + { + this.trailing++; + } + } + } + + /** + * Recalculate leading spaces. + */ + public void initLeading() + { + this.leading = 0; + while (this.leading < this.value.length() && this.value.charAt(this.leading) == ' ') + { + this.leading++; + } + + if (this.leading == this.value.length()) + { + this.setEmpty(); + } + } + + /** + * Skips spaces. + * + * @return false if end of line is reached + */ + public boolean skipSpaces() + { + while (this.pos < this.value.length() && this.value.charAt(this.pos) == ' ') + { + this.pos++; + } + return this.pos < this.value.length(); + } + + /** + * Reads chars from this line until any 'end' char is reached. + * + * @param end + * Delimiting character(s) + * @return The read String or null if no 'end' char was + * reached. + */ + public String readUntil(final char... end) + { + final StringBuilder sb = new StringBuilder(); + int pos = this.pos; + while (pos < this.value.length()) + { + final char ch = this.value.charAt(pos); + if (ch == '\\' && pos + 1 < this.value.length()) + { + final char c; + switch (c = this.value.charAt(pos + 1)) + { + case '\\': + case '[': + case ']': + case '(': + case ')': + case '{': + case '}': + case '#': + case '"': + case '\'': + case '.': + case '>': + case '*': + case '+': + case '-': + case '_': + case '!': + case '`': + case '~': + sb.append(c); + pos++; + break; + default: + sb.append(ch); + break; + } + } + else + { + boolean endReached = false; + for (int n = 0; n < end.length; n++) + { + if (ch == end[n]) + { + endReached = true; + break; + } + } + if (endReached) + { + break; + } + sb.append(ch); + } + pos++; + } + + final char ch = pos < this.value.length() ? this.value.charAt(pos) : '\n'; + for (int n = 0; n < end.length; n++) + { + if (ch == end[n]) + { + this.pos = pos; + return sb.toString(); + } + } + return null; + } + + /** + * Marks this line empty. Also sets previous/next line's empty attributes. + */ + public void setEmpty() + { + this.value = ""; + this.leading = this.trailing = 0; + this.isEmpty = true; + if (this.previous != null) + { + this.previous.nextEmpty = true; + } + if (this.next != null) + { + this.next.prevEmpty = true; + } + } + + /** + * Counts the amount of 'ch' in this line. + * + * @param ch + * The char to count. + * @return A value > 0 if this line only consists of 'ch' end spaces. + */ + private int countChars(final char ch) + { + int count = 0; + for (int i = 0; i < this.value.length(); i++) + { + final char c = this.value.charAt(i); + if (c == ' ') + { + continue; + } + if (c == ch) + { + count++; + continue; + } + count = 0; + break; + } + return count; + } + + /** + * Counts the amount of 'ch' at the start of this line optionally ignoring + * spaces. + * + * @param ch + * The char to count. + * @param allowSpaces + * Whether to allow spaces or not + * @return Number of characters found. + * @since 0.12 + */ + private int countCharsStart(final char ch, final boolean allowSpaces) + { + int count = 0; + for (int i = 0; i < this.value.length(); i++) + { + final char c = this.value.charAt(i); + if (c == ' ' && allowSpaces) + { + continue; + } + if (c == ch) + { + count++; + } + else + { + break; + } + } + return count; + } + + /** + * Gets this line's type. + * + * @param configuration + * txtmark configuration + * + * @return The LineType. + */ + public LineType getLineType(final Configuration configuration) + { + if (this.isEmpty) + { + return LineType.EMPTY; + } + + if (this.leading > 3) + { + return LineType.CODE; + } + + if (this.value.charAt(this.leading) == '#') + { + return LineType.HEADLINE; + } + + if (this.value.charAt(this.leading) == '>') + { + return LineType.BQUOTE; + } + + if (configuration.forceExtendedProfile) + { + if (this.value.length() - this.leading - this.trailing > 2) + { + if (this.value.charAt(this.leading) == '`' + && this.countCharsStart('`', configuration.allowSpacesInFencedDelimiters) >= 3) + { + return LineType.FENCED_CODE; + } + if (this.value.charAt(this.leading) == '~' + && this.countCharsStart('~', configuration.allowSpacesInFencedDelimiters) >= 3) + { + return LineType.FENCED_CODE; + } + } + } + + if (this.value.length() - this.leading - this.trailing > 2 + && (this.value.charAt(this.leading) == '*' || this.value.charAt(this.leading) == '-' || this.value + .charAt(this.leading) == '_')) + { + if (this.countChars(this.value.charAt(this.leading)) >= 3) + { + return LineType.HR; + } + } + + if (this.value.length() - this.leading >= 2 && this.value.charAt(this.leading + 1) == ' ') + { + switch (this.value.charAt(this.leading)) + { + case '*': + case '-': + case '+': + return LineType.ULIST; + } + } + + if (this.value.length() - this.leading >= 3 && Character.isDigit(this.value.charAt(this.leading))) + { + int i = this.leading + 1; + while (i < this.value.length() && Character.isDigit(this.value.charAt(i))) + { + i++; + } + if (i + 1 < this.value.length() && this.value.charAt(i) == '.' && this.value.charAt(i + 1) == ' ') + { + return LineType.OLIST; + } + } + + if (this.value.charAt(this.leading) == '<') + { + if (this.checkHTML()) + { + return LineType.XML; + } + } + + if (this.next != null && !this.next.isEmpty) + { + if ((this.next.value.charAt(0) == '-') && (this.next.countChars('-') > 0)) + { + return LineType.HEADLINE2; + } + if ((this.next.value.charAt(0) == '=') && (this.next.countChars('=') > 0)) + { + return LineType.HEADLINE1; + } + if (configuration.forceExtendedProfile && (this.previous == null || this.previous.isEmpty)) + { + TableDef table = TableDef.parse(this.value, this.next.value); + if (table != null) { + this.data = table; // attach the table definition to be used later + return LineType.TABLE; + } + } + } + + return LineType.OTHER; + } + + /** + * Reads an XML comment. Sets xmlEndLine. + * + * @param firstLine + * The Line to start reading from. + * @param start + * The starting position. + * @return The new position or -1 if it is no valid comment. + */ + private int readXMLComment(final Line firstLine, final int start) + { + Line line = firstLine; + if (start + 3 < line.value.length()) + { + if (line.value.charAt(2) == '-' && line.value.charAt(3) == '-') + { + int pos = start + 4; + while (line != null) + { + while (pos < line.value.length() && line.value.charAt(pos) != '-') + { + pos++; + } + if (pos == line.value.length()) + { + line = line.next; + pos = 0; + } + else + { + if (pos + 2 < line.value.length()) + { + if (line.value.charAt(pos + 1) == '-' && line.value.charAt(pos + 2) == '>') + { + this.xmlEndLine = line; + return pos + 3; + } + } + pos++; + } + } + } + } + return -1; + } + + /** + * Checks if this line contains an ID at it's end and removes it from the + * line. + * + * @return The ID or null if no valid ID exists. + */ + public String stripID() + { + if (this.isEmpty || this.value.charAt(this.value.length() - this.trailing - 1) != '}') + { + return null; + } + int p = this.leading; + boolean found = false; + while (p < this.value.length() && !found) + { + switch (this.value.charAt(p)) + { + case '\\': + if (p + 1 < this.value.length()) + { + switch (this.value.charAt(p + 1)) + { + case '{': + p++; + break; + } + } + p++; + break; + case '{': + found = true; + break; + default: + p++; + break; + } + } + + if (found) + { + if (p + 1 < this.value.length() && this.value.charAt(p + 1) == '#') + { + final int start = p + 2; + p = start; + found = false; + while (p < this.value.length() && !found) + { + switch (this.value.charAt(p)) + { + case '\\': + if (p + 1 < this.value.length()) + { + switch (this.value.charAt(p + 1)) + { + case '}': + p++; + break; + } + } + p++; + break; + case '}': + found = true; + break; + default: + p++; + break; + } + } + if (found) + { + final String id = this.value.substring(start, p).trim(); + if (this.leading != 0) + { + this.value = this.value.substring(0, this.leading) + + this.value.substring(this.leading, start - 2).trim(); + } + else + { + this.value = this.value.substring(this.leading, start - 2).trim(); + } + this.trailing = 0; + return id.length() > 0 ? id : null; + } + } + } + return null; + } + + /** + * Checks for a valid HTML block. Sets xmlEndLine. + * + * @return true if it is a valid block. + */ + private boolean checkHTML() + { + final LinkedList tags = new LinkedList(); + final StringBuilder temp = new StringBuilder(); + int pos = this.leading; + if (this.leading + 1 < this.value.length() && this.value.charAt(this.leading + 1) == '!') + { + if (this.readXMLComment(this, this.leading) > 0) + { + return true; + } + } + pos = Utils.readXML(temp, this.value, this.leading, false); + String element, tag; + if (pos > -1) + { + element = temp.toString(); + temp.setLength(0); + Utils.getXMLTag(temp, element); + tag = temp.toString().toLowerCase(); + if (!HTML.isHtmlBlockElement(tag)) + { + return false; + } + if (tag.equals("hr") || element.endsWith("/>")) + { + this.xmlEndLine = this; + return true; + } + tags.add(tag); + + Line line = this; + while (line != null) + { + while (pos < line.value.length() && line.value.charAt(pos) != '<') + { + pos++; + } + if (pos >= line.value.length()) + { + line = line.next; + pos = 0; + } + else + { + temp.setLength(0); + final int newPos = Utils.readXML(temp, line.value, pos, false); + if (newPos > 0) + { + element = temp.toString(); + temp.setLength(0); + Utils.getXMLTag(temp, element); + tag = temp.toString().toLowerCase(); + if (HTML.isHtmlBlockElement(tag) && !tag.equals("hr") && !element.endsWith("/>")) + { + if (element.charAt(1) == '/') + { + if (!tags.getLast().equals(tag)) + { + return false; + } + tags.removeLast(); + } + else + { + tags.addLast(tag); + } + } + if (tags.size() == 0) + { + this.xmlEndLine = line; + break; + } + pos = newPos; + } + else + { + pos++; + } + } + } + return tags.size() == 0; + } + return false; + } +} diff --git a/contentstack/src/main/java/com/contentstack/txtmark/LineType.java b/contentstack/src/main/java/com/contentstack/txtmark/LineType.java new file mode 100755 index 00000000..223b3b17 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/txtmark/LineType.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2011-2015 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.txtmark; + +/** + * Line type enumeration. + * + * @author René Jeschke + */ +enum LineType +{ + /** Empty line. */ + EMPTY, + /** Undefined content. */ + OTHER, + /** A markdown headline. */ + HEADLINE, HEADLINE1, HEADLINE2, + /** A code block line. */ + CODE, + /** A list. */ + ULIST, OLIST, + /** A block quote. */ + BQUOTE, + /** A horizontal ruler. */ + HR, + /** Start of a XML block. */ + XML, + /** Fenced code block start/end */ + FENCED_CODE, + /** GFM table **/ + TABLE +} diff --git a/contentstack/src/main/java/com/contentstack/txtmark/LinkRef.java b/contentstack/src/main/java/com/contentstack/txtmark/LinkRef.java new file mode 100755 index 00000000..f15af418 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/txtmark/LinkRef.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2011-2015 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.txtmark; + +/** + * A markdown link reference. + * + * @author René Jeschke + */ +class LinkRef +{ + /** The link. */ + public final String link; + /** The optional comment/title. */ + public String title; + /** Flag indicating that this is an abbreviation. */ + public final boolean isAbbrev; + + /** + * Constructor. + * + * @param link + * The link. + * @param title + * The title (may be null). + */ + public LinkRef(final String link, final String title, final boolean isAbbrev) + { + this.link = link; + this.title = title; + this.isAbbrev = isAbbrev; + } + + /** @see java.lang.Object#toString() */ + @Override + public String toString() + { + return this.link + " \"" + this.title + "\""; + } +} diff --git a/contentstack/src/main/java/com/contentstack/txtmark/MarkToken.java b/contentstack/src/main/java/com/contentstack/txtmark/MarkToken.java new file mode 100755 index 00000000..a855bbf4 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/txtmark/MarkToken.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2011-2015 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.txtmark; + +/** + * Markdown token enumeration. + * + * @author René Jeschke + */ +enum MarkToken +{ + /** No token. */ + NONE, + /** * */ + EM_STAR, // x*x + /** _ */ + EM_UNDERSCORE, // x_x + /** ** */ + STRONG_STAR, // x**x + /** __ */ + STRONG_UNDERSCORE, // x__x + /** ~~ */ + STRIKEOUT, // ~~ + /** ` */ + CODE_SINGLE, // ` + /** `` */ + CODE_DOUBLE, // `` + /** [ */ + LINK, // [ + /** < */ + HTML, // < + /** ![ */ + IMAGE, // ![ + /** & */ + ENTITY, // & + /** https?://domain[/path][?query][#hash] */ + GFM_AUTOLINK, // https?://domain[/path][?query][#hash] + /** \ */ + ESCAPE, // \x + /** Extended: ^ */ + SUPER, // ^ + /** Extended: (C) */ + X_COPY, // (C) + /** Extended: (R) */ + X_REG, // (R) + /** Extended: (TM) */ + X_TRADE, // (TM) + /** Extended: << */ + X_LAQUO, // << + /** Extended: >> */ + X_RAQUO, // >> + /** Extended: -- */ + X_NDASH, // -- + /** Extended: --- */ + X_MDASH, // --- + /** Extended: ... */ + X_HELLIP, // ... + /** Extended: "x */ + X_RDQUO, // " + /** Extended: x" */ + X_LDQUO, // " + /** [[ */ + X_LINK_OPEN, // [[ + /** ]] */ + X_LINK_CLOSE, // ]] +} diff --git a/contentstack/src/main/java/com/contentstack/txtmark/Processor.java b/contentstack/src/main/java/com/contentstack/txtmark/Processor.java new file mode 100755 index 00000000..2fe80853 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/txtmark/Processor.java @@ -0,0 +1,1033 @@ +/* + * Copyright (C) 2011-2015 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.txtmark; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; + +/** + * Markdown processor class. + * + *

    + * Example usage: + *

    + * + *
    + * String result = Processor.process("This is ***TXTMARK***");
    + * 
    + * 
    + * + * @author René Jeschke <, rene_jeschke@yahoo.de>, + */ +public class Processor +{ + /** The reader. */ + private final Reader reader; + /** The emitter. */ + private final Emitter emitter; + /** The Configuration. */ + final Configuration config; + /** Extension flag. */ + private boolean useExtensions = false; + + /** + * Constructor. + * + * @param reader + * The input reader. + */ + private Processor(final Reader reader, final Configuration config) + { + this.reader = reader; + this.config = config; + this.useExtensions = config.forceExtendedProfile; + this.emitter = new Emitter(this.config); + } + + /** + * Transforms an input stream into HTML using the given Configuration. + * + * @param reader + * The Reader to process. + * @param configuration + * The Configuration. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @since 0.7 + * @see Configuration + */ + public final static String process(final Reader reader, final Configuration configuration) throws IOException + { + final Processor p = new Processor(!(reader instanceof BufferedReader) ? new BufferedReader(reader) : reader, + configuration); + return p.process(); + } + + /** + * Transforms an input String into HTML using the given Configuration. + * + * @param input + * The String to process. + * @param configuration + * The Configuration. + * @return The processed String. + * @since 0.7 + * @see Configuration + */ + public final static String process(final String input, final Configuration configuration) + { + try + { + return process(new StringReader(input), configuration); + } + catch (final IOException e) + { + // This _can never_ happen + return null; + } + } + + /** + * Transforms an input file into HTML using the given Configuration. + * + * @param file + * The File to process. + * @param configuration + * the Configuration + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @since 0.7 + * @see Configuration + */ + public final static String process(final File file, final Configuration configuration) throws IOException + { + final FileInputStream input = new FileInputStream(file); + final String ret = process(input, configuration); + input.close(); + return ret; + } + + /** + * Transforms an input stream into HTML using the given Configuration. + * + * @param input + * The InputStream to process. + * @param configuration + * The Configuration. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @since 0.7 + * @see Configuration + */ + public final static String process(final InputStream input, final Configuration configuration) throws IOException + { + final Processor p = new Processor(new BufferedReader(new InputStreamReader(input, configuration.encoding)), + configuration); + return p.process(); + } + + /** + * Transforms an input String into HTML using the default Configuration. + * + * @param input + * The String to process. + * @return The processed String. + * @see Configuration#DEFAULT + */ + public final static String process(final String input) + { + return process(input, Configuration.DEFAULT); + } + + /** + * Transforms an input String into HTML. + * + * @param input + * The String to process. + * @param safeMode + * Set to true to escape unsafe HTML tags. + * @return The processed String. + * @see Configuration#DEFAULT + */ + public final static String process(final String input, final boolean safeMode) + { + return process(input, Configuration.builder().setSafeMode(safeMode).build()); + } + + /** + * Transforms an input String into HTML. + * + * @param input + * The String to process. + * @param decorator + * The decorator to use. + * @return The processed String. + * @see Configuration#DEFAULT + */ + public final static String process(final String input, final Decorator decorator) + { + return process(input, Configuration.builder().setDecorator(decorator).build()); + } + + /** + * Transforms an input String into HTML. + * + * @param input + * The String to process. + * @param decorator + * The decorator to use. + * @param safeMode + * Set to true to escape unsafe HTML tags. + * @return The processed String. + * @see Configuration#DEFAULT + */ + public final static String process(final String input, final Decorator decorator, final boolean safeMode) + { + return process(input, Configuration.builder().setDecorator(decorator).setSafeMode(safeMode).build()); + } + + /** + * Transforms an input file into HTML using the default Configuration. + * + * @param file + * The File to process. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @see Configuration#DEFAULT + */ + public final static String process(final File file) throws IOException + { + return process(file, Configuration.DEFAULT); + } + + /** + * Transforms an input file into HTML. + * + * @param file + * The File to process. + * @param safeMode + * Set to true to escape unsafe HTML tags. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @see Configuration#DEFAULT + */ + public final static String process(final File file, final boolean safeMode) throws IOException + { + return process(file, Configuration.builder().setSafeMode(safeMode).build()); + } + + /** + * Transforms an input file into HTML. + * + * @param file + * The File to process. + * @param decorator + * The decorator to use. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @see Configuration#DEFAULT + */ + public final static String process(final File file, final Decorator decorator) throws IOException + { + return process(file, Configuration.builder().setDecorator(decorator).build()); + } + + /** + * Transforms an input file into HTML. + * + * @param file + * The File to process. + * @param decorator + * The decorator to use. + * @param safeMode + * Set to true to escape unsafe HTML tags. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @see Configuration#DEFAULT + */ + public final static String process(final File file, final Decorator decorator, final boolean safeMode) + throws IOException + { + return process(file, Configuration.builder().setDecorator(decorator).setSafeMode(safeMode).build()); + } + + /** + * Transforms an input file into HTML. + * + * @param file + * The File to process. + * @param encoding + * The encoding to use. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @see Configuration#DEFAULT + */ + public final static String process(final File file, final String encoding) throws IOException + { + return process(file, Configuration.builder().setEncoding(encoding).build()); + } + + /** + * Transforms an input file into HTML. + * + * @param file + * The File to process. + * @param encoding + * The encoding to use. + * @param safeMode + * Set to true to escape unsafe HTML tags. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @see Configuration#DEFAULT + */ + public final static String process(final File file, final String encoding, final boolean safeMode) + throws IOException + { + return process(file, Configuration.builder().setEncoding(encoding).setSafeMode(safeMode).build()); + } + + /** + * Transforms an input file into HTML. + * + * @param file + * The File to process. + * @param encoding + * The encoding to use. + * @param decorator + * The decorator to use. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @see Configuration#DEFAULT + */ + public final static String process(final File file, final String encoding, final Decorator decorator) + throws IOException + { + return process(file, Configuration.builder().setEncoding(encoding).setDecorator(decorator).build()); + } + + /** + * Transforms an input file into HTML. + * + * @param file + * The File to process. + * @param encoding + * The encoding to use. + * @param decorator + * The decorator to use. + * @param safeMode + * Set to true to escape unsafe HTML tags. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @see Configuration#DEFAULT + */ + public final static String process(final File file, final String encoding, final Decorator decorator, + final boolean safeMode) throws IOException + { + return process(file, Configuration.builder().setEncoding(encoding).setSafeMode(safeMode) + .setDecorator(decorator).build()); + } + + /** + * Transforms an input stream into HTML. + * + * @param input + * The InputStream to process. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @see Configuration#DEFAULT + */ + public final static String process(final InputStream input) throws IOException + { + return process(input, Configuration.DEFAULT); + } + + /** + * Transforms an input stream into HTML. + * + * @param input + * The InputStream to process. + * @param safeMode + * Set to true to escape unsafe HTML tags. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @see Configuration#DEFAULT + */ + public final static String process(final InputStream input, final boolean safeMode) throws IOException + { + return process(input, Configuration.builder().setSafeMode(safeMode).build()); + } + + /** + * Transforms an input stream into HTML. + * + * @param input + * The InputStream to process. + * @param decorator + * The decorator to use. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @see Configuration#DEFAULT + */ + public final static String process(final InputStream input, final Decorator decorator) throws IOException + { + return process(input, Configuration.builder().setDecorator(decorator).build()); + } + + /** + * Transforms an input stream into HTML. + * + * @param input + * The InputStream to process. + * @param decorator + * The decorator to use. + * @param safeMode + * Set to true to escape unsafe HTML tags. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @see Configuration#DEFAULT + */ + public final static String process(final InputStream input, final Decorator decorator, final boolean safeMode) + throws IOException + { + return process(input, Configuration.builder().setDecorator(decorator).setSafeMode(safeMode).build()); + } + + /** + * Transforms an input stream into HTML. + * + * @param input + * The InputStream to process. + * @param encoding + * The encoding to use. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @see Configuration#DEFAULT + */ + public final static String process(final InputStream input, final String encoding) throws IOException + { + return process(input, Configuration.builder().setEncoding(encoding).build()); + } + + /** + * Transforms an input stream into HTML. + * + * @param input + * The InputStream to process. + * @param encoding + * The encoding to use. + * @param safeMode + * Set to true to escape unsafe HTML tags. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @see Configuration#DEFAULT + */ + public final static String process(final InputStream input, final String encoding, final boolean safeMode) + throws IOException + { + return process(input, Configuration.builder().setEncoding(encoding).setSafeMode(safeMode).build()); + } + + /** + * Transforms an input stream into HTML. + * + * @param input + * The InputStream to process. + * @param encoding + * The encoding to use. + * @param decorator + * The decorator to use. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @see Configuration#DEFAULT + */ + public final static String process(final InputStream input, final String encoding, final Decorator decorator) + throws IOException + { + return process(input, Configuration.builder().setEncoding(encoding).setDecorator(decorator).build()); + } + + /** + * Transforms an input stream into HTML. + * + * @param input + * The InputStream to process. + * @param encoding + * The encoding to use. + * @param decorator + * The decorator to use. + * @param safeMode + * Set to true to escape unsafe HTML tags. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @see Configuration#DEFAULT + */ + public final static String process(final InputStream input, final String encoding, final Decorator decorator, + final boolean safeMode) throws IOException + { + return process(input, + Configuration.builder().setEncoding(encoding).setDecorator(decorator).setSafeMode(safeMode).build()); + } + + /** + * Transforms an input stream into HTML using the default Configuration. + * + * @param reader + * The Reader to process. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @see Configuration#DEFAULT + */ + public final static String process(final Reader reader) throws IOException + { + return process(reader, Configuration.DEFAULT); + } + + /** + * Transforms an input stream into HTML. + * + * @param reader + * The Reader to process. + * @param safeMode + * Set to true to escape unsafe HTML tags. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @see Configuration#DEFAULT + */ + public final static String process(final Reader reader, final boolean safeMode) throws IOException + { + return process(reader, Configuration.builder().setSafeMode(safeMode).build()); + } + + /** + * Transforms an input stream into HTML. + * + * @param reader + * The Reader to process. + * @param decorator + * The decorator to use. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @see Configuration#DEFAULT + */ + public final static String process(final Reader reader, final Decorator decorator) throws IOException + { + return process(reader, Configuration.builder().setDecorator(decorator).build()); + } + + /** + * Transforms an input stream into HTML. + * + * @param reader + * The Reader to process. + * @param decorator + * The decorator to use. + * @param safeMode + * Set to true to escape unsafe HTML tags. + * @return The processed String. + * @throws IOException + * if an IO error occurs + * @see Configuration#DEFAULT + */ + public final static String process(final Reader reader, final Decorator decorator, final boolean safeMode) + throws IOException + { + return process(reader, Configuration.builder().setDecorator(decorator).setSafeMode(safeMode).build()); + } + + /** + * Reads all lines from our reader. + *

    + * Takes care of markdown link references. + *

    + * + * @return A Block containing all lines. + * @throws IOException + * If an IO error occurred. + */ + private Block readLines() throws IOException + { + final Block block = new Block(); + final StringBuilder sb = new StringBuilder(80); + int c = this.reader.read(); + LinkRef lastLinkRef = null; + while (c != -1) + { + sb.setLength(0); + int pos = 0; + boolean eol = false; + while (!eol) + { + switch (c) + { + case -1: + eol = true; + break; + case '\n': + c = this.reader.read(); + if (c == '\r') + { + c = this.reader.read(); + } + eol = true; + break; + case '\r': + c = this.reader.read(); + if (c == '\n') + { + c = this.reader.read(); + } + eol = true; + break; + case '\t': + { + final int np = pos + (4 - (pos & 3)); + while (pos < np) + { + sb.append(' '); + pos++; + } + c = this.reader.read(); + break; + } + default: + if (c != '<' || !this.config.panicMode) + { + pos++; + sb.append((char)c); + } + else + { + pos += 4; + sb.append("<"); + } + c = this.reader.read(); + break; + } + } + + final Line line = new Line(); + line.value = sb.toString(); + line.init(); + + // Check for link definitions + boolean isLinkRef = false; + String id = null, link = null, comment = null; + if (!line.isEmpty && line.leading < 4 && line.value.charAt(line.leading) == '[') + { + line.pos = line.leading + 1; + // Read ID up to ']' + id = line.readUntil(']'); + // Is ID valid and are there any more characters? + if (id != null && line.pos + 2 < line.value.length()) + { + // Check for ':' ([...]:...) + if (line.value.charAt(line.pos + 1) == ':') + { + line.pos += 2; + if (!line.skipSpaces()) + { + isLinkRef = false; + } + // Check for link syntax + else if (line.value.charAt(line.pos) == '<') + { + line.pos++; + link = line.readUntil('>'); + line.pos++; + } + else + { + link = line.readUntil(' ', '\n'); + } + + // Is link valid? + if (link != null) + { + // Any non-whitespace characters following? + if (line.skipSpaces()) + { + final char ch = line.value.charAt(line.pos); + // Read comment + if (ch == '\"' || ch == '\'' || ch == '(') + { + line.pos++; + comment = line.readUntil(ch == '(' ? ')' : ch); + // Valid linkRef only if comment is valid + if (comment != null) + { + isLinkRef = true; + } + } + } + else + { + isLinkRef = true; + } + } + } + } + } + + // To make compiler happy: add != null checks + if (isLinkRef && id != null && link != null) + { + if (id.toLowerCase().equals("$profile$")) + { + this.emitter.useExtensions = this.useExtensions = link.toLowerCase().equals("extended"); + lastLinkRef = null; + } + else + { + // Store linkRef and skip line + final LinkRef lr = new LinkRef(link, comment, comment != null + && (link.length() == 1 && link.charAt(0) == '*')); + this.emitter.addLinkRef(id, lr); + if (comment == null) + { + lastLinkRef = lr; + } + } + } + else + { + comment = null; + // Check for multi-line linkRef + if (!line.isEmpty && lastLinkRef != null) + { + line.pos = line.leading; + final char ch = line.value.charAt(line.pos); + if (ch == '\"' || ch == '\'' || ch == '(') + { + line.pos++; + comment = line.readUntil(ch == '(' ? ')' : ch); + } + if (comment != null) + { + lastLinkRef.title = comment; + } + + lastLinkRef = null; + } + + // No multi-line linkRef, store line + if (comment == null) + { + line.pos = 0; + block.appendLine(line); + } + } + } + + return block; + } + + /** + * Initializes a list block by separating it into list item blocks. + * + * @param root + * The Block to process. + */ + private void initListBlock(final Block root) + { + Line line = root.lines; + line = line.next; + while (line != null) + { + final LineType t = line.getLineType(this.config); + if ((t == LineType.OLIST || t == LineType.ULIST) + || (!line.isEmpty && (line.prevEmpty && line.leading == 0 && !(t == LineType.OLIST || t == LineType.ULIST)))) + { + root.split(line.previous).type = BlockType.LIST_ITEM; + } + line = line.next; + } + root.split(root.lineTail).type = BlockType.LIST_ITEM; + } + + /** + * Recursively process the given Block. + * + * @param root + * The Block to process. + * @param listMode + * Flag indicating that we're in a list item block. + */ + private void recurse(final Block root, final boolean listMode) + { + Block block, list; + Line line = root.lines; + + if (listMode) + { + root.removeListIndent(this.config); + if (this.useExtensions && root.lines != null && root.lines.getLineType(this.config) != LineType.CODE) + { + root.id = root.lines.stripID(); + } + } + + while (line != null && line.isEmpty) + { + line = line.next; + } + if (line == null) + { + return; + } + + while (line != null) + { + final LineType type = line.getLineType(this.config); + switch (type) + { + case OTHER: + { + final boolean wasEmpty = line.prevEmpty; + while (line != null && !line.isEmpty) + { + final LineType t = line.getLineType(this.config); + if ((listMode || this.useExtensions) && (t == LineType.OLIST || t == LineType.ULIST)) + { + break; + } + if (this.useExtensions && (t == LineType.CODE || t == LineType.FENCED_CODE)) + { + break; + } + if (t == LineType.HEADLINE || t == LineType.HEADLINE1 || t == LineType.HEADLINE2 + || t == LineType.HR + || t == LineType.BQUOTE || t == LineType.XML) + { + break; + } + line = line.next; + } + final BlockType bt; + if (line != null && !line.isEmpty) + { + bt = (listMode && !wasEmpty) ? BlockType.NONE : BlockType.PARAGRAPH; + root.split(line.previous).type = bt; + root.removeLeadingEmptyLines(); + } + else + { + bt = (listMode && (line == null || !line.isEmpty) && !wasEmpty) ? BlockType.NONE + : BlockType.PARAGRAPH; + root.split(line == null ? root.lineTail : line).type = bt; + root.removeLeadingEmptyLines(); + } + line = root.lines; + break; + } + case CODE: + while (line != null && (line.isEmpty || line.leading > 3)) + { + line = line.next; + } + block = root.split(line != null ? line.previous : root.lineTail); + block.type = BlockType.CODE; + block.removeSurroundingEmptyLines(); + break; + case XML: + if (line.previous != null) + { + root.split(line.previous); + } + root.split(line.xmlEndLine).type = BlockType.XML; + root.removeLeadingEmptyLines(); + line = root.lines; + break; + case BQUOTE: + while (line != null) + { + if (!line.isEmpty + && (line.prevEmpty && line.leading == 0 && line.getLineType(this.config) != LineType.BQUOTE)) + { + break; + } + line = line.next; + } + block = root.split(line != null ? line.previous : root.lineTail); + block.type = BlockType.BLOCKQUOTE; + block.removeSurroundingEmptyLines(); + block.removeBlockQuotePrefix(); + this.recurse(block, false); + line = root.lines; + break; + case HR: + if (line.previous != null) + { + root.split(line.previous); + } + root.split(line).type = BlockType.RULER; + root.removeLeadingEmptyLines(); + line = root.lines; + break; + case FENCED_CODE: + line = line.next; + while (line != null) + { + if (line.getLineType(this.config) == LineType.FENCED_CODE) + { + break; + } + // flag? + line = line.next; + } + if (line != null) + { + line = line.next; + } + block = root.split(line != null ? line.previous : root.lineTail); + block.type = BlockType.FENCED_CODE; + block.meta = Utils.getMetaFromFence(block.lines.value); + block.lines.setEmpty(); + if (block.lineTail.getLineType(this.config) == LineType.FENCED_CODE) + { + block.lineTail.setEmpty(); + } + block.removeSurroundingEmptyLines(); + break; + case HEADLINE: + case HEADLINE1: + case HEADLINE2: + if (line.previous != null) + { + root.split(line.previous); + } + if (type != LineType.HEADLINE) + { + line.next.setEmpty(); + } + block = root.split(line); + block.type = BlockType.HEADLINE; + if (type != LineType.HEADLINE) + { + block.hlDepth = type == LineType.HEADLINE1 ? 1 : 2; + } + if (this.useExtensions) + { + block.id = block.lines.stripID(); + } + block.transfromHeadline(); + root.removeLeadingEmptyLines(); + line = root.lines; + break; + case OLIST: + case ULIST: + while (line != null) + { + final LineType t = line.getLineType(this.config); + if (!line.isEmpty + && (line.prevEmpty && line.leading == 0 && !(t == LineType.OLIST || t == LineType.ULIST))) + { + break; + } + line = line.next; + } + list = root.split(line != null ? line.previous : root.lineTail); + list.type = type == LineType.OLIST ? BlockType.ORDERED_LIST : BlockType.UNORDERED_LIST; + list.lines.prevEmpty = false; + list.lineTail.nextEmpty = false; + list.removeSurroundingEmptyLines(); + list.lines.prevEmpty = list.lineTail.nextEmpty = false; + this.initListBlock(list); + block = list.blocks; + while (block != null) + { + this.recurse(block, true); + block = block.next; + } + list.expandListParagraphs(); + break; + case TABLE: + TableDef table = (TableDef)line.data; + // skip the next line - which is the the table divider line + line = line.next.next; + while (line != null) + { + if (line.isEmpty) + { + break; + } + if (!table.addRow(line.value)) { + break; + } + line = line.next; + } + block = root.split(line != null ? line.previous : root.lineTail); + block.type = BlockType.TABLE; + line = root.lines; + break; + default: + line = line.next; + break; + } + } + } + + /** + * Does all the processing. + * + * @return The processed String. + * @throws IOException + * If an IO error occurred. + */ + private String process() throws IOException + { + final StringBuilder out = new StringBuilder(); + final Block parent = this.readLines(); + parent.removeSurroundingEmptyLines(); + + this.recurse(parent, false); + Block block = parent.blocks; + while (block != null) + { + this.emitter.emit(out, block); + block = block.next; + } + + return out.toString(); + } +} diff --git a/contentstack/src/main/java/com/contentstack/txtmark/Run.java b/contentstack/src/main/java/com/contentstack/txtmark/Run.java new file mode 100755 index 00000000..d58df957 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/txtmark/Run.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2011-2015 René Jeschke + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.contentstack.txtmark; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * Simple class for processing markdown files on the command line. + * + *

    + * Usage: + *

    + * + *
    + * java -cp txtmark.jar txtmark.Run filename [header_footer_file]
    + * 
    + * 
    + * + *

    + * The header_footer_file is an optional UTF-8 encoded file + * containing a header and a footer to output around the generated HTML code. + *

    + * + *

    + * Example: + *

    + * + *
    + * <?xml version="1.0" encoding="UTF-8"?>
    + * <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    + *                       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    + * <html xmlns="http://www.w3.org/1999/xhtml">
    + * <head>
    + * <title>markdown</title>
    + * <link type="text/css" href="style.css" rel="stylesheet"/>
    + * <meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
    + * </head>
    + * <body>
    + * <!-- the following line separates header from footer -->
    + * <!-- ### -->
    + * </body>
    + * </html>
    + * 
    + * 
    + * + * @author René Jeschke <rene_jeschke@yahoo.de> + */ +public class Run +{ + /** + * Static main. + * + * @param args + * Program arguments. + * @throws IOException + * If an IO error occurred. + */ + public static void main(final String[] args) throws IOException + { + // This is just a _hack_ ... + BufferedReader reader = null; + if (args.length == 0) + { + System.err.println("No input file specified."); + System.exit(-1); + } + if (args.length > 1) + { + reader = new BufferedReader(new InputStreamReader(new FileInputStream(args[1]), "UTF-8")); + String line = reader.readLine(); + while (line != null && !line.startsWith(" + + +