├── .editorconfig
├── .gitignore
├── .travis.yml
├── LICENSE
├── Readme.md
├── app
├── .gitignore
├── appTest.jks
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── epam
│ │ └── example
│ │ └── coroutinescache
│ │ └── ExampleInstrumentedTest.kt
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── epam
│ │ └── example
│ │ └── coroutinescache
│ │ ├── App.kt
│ │ ├── CacheProviders.kt
│ │ ├── Data.kt
│ │ ├── MainActivity.kt
│ │ ├── Repository.kt
│ │ └── RestApi.kt
│ └── res
│ ├── drawable-v24
│ └── ic_launcher_foreground.xml
│ ├── drawable
│ └── ic_launcher_background.xml
│ ├── layout
│ └── activity_main.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ └── values
│ ├── colors.xml
│ ├── strings.xml
│ └── styles.xml
├── build.gradle
├── bumpVersion.gradle
├── core
├── .gitignore
├── build.gradle
└── src
│ ├── main
│ └── java
│ │ └── com
│ │ └── epam
│ │ └── coroutinecache
│ │ ├── annotations
│ │ ├── Expirable.kt
│ │ ├── LifeTime.kt
│ │ ├── ProviderKey.kt
│ │ └── UseIfExpired.kt
│ │ ├── api
│ │ ├── CacheParams.kt
│ │ ├── CoroutinesCache.kt
│ │ └── ParameterizedDataProvider.kt
│ │ ├── core
│ │ ├── DiskCache.kt
│ │ ├── Memory.kt
│ │ ├── MemoryCache.kt
│ │ ├── Persistence.kt
│ │ ├── Record.kt
│ │ ├── Source.kt
│ │ └── actions
│ │ │ ├── DeleteExpirableRecordsAction.kt
│ │ │ ├── DeleteExpiredRecordsAction.kt
│ │ │ ├── DeleteRecordAction.kt
│ │ │ ├── GetRecordAction.kt
│ │ │ └── SaveRecordAction.kt
│ │ ├── di
│ │ ├── ActionsModule.kt
│ │ └── CacheModule.kt
│ │ ├── internal
│ │ ├── CacheObjectParams.kt
│ │ ├── ProcessorProvider.kt
│ │ ├── ProcessorProviderImpl.kt
│ │ ├── ProxyProvider.kt
│ │ ├── ProxyTranslator.kt
│ │ └── RecordExpiredChecker.kt
│ │ ├── mappers
│ │ ├── GsonMapper.kt
│ │ ├── JacksonMapper.kt
│ │ ├── JsonMapper.kt
│ │ └── MoshiMapper.kt
│ │ └── utils
│ │ ├── CacheLog.kt
│ │ ├── NoParamsDataProvider.kt
│ │ ├── Types.kt
│ │ └── Utils.kt
│ └── test
│ └── java
│ └── com
│ └── epam
│ └── coroutinecache
│ ├── BaseTest.kt
│ ├── DiskTest.kt
│ ├── MemoryTest.kt
│ ├── actions
│ ├── DeleteRecordActionTest.kt
│ ├── GetRecordActionTest.kt
│ └── SaveRecordActionTest.kt
│ └── utils
│ ├── JsonFactoryChooser.kt
│ ├── MapperProvider.kt
│ └── MockDataString.kt
├── detekt-config.yml
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── ktlint.gradle
├── settings.gradle
└── updateReadme.sh
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{kt,kts}]
2 | indent_size=unset
3 | max_line_length=150
4 | insert_final_newline=false
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/*
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: android
2 | dist: trusty
3 | jdk: oraclejdk8
4 |
5 | env:
6 | global:
7 | - ANDROID_API_LEVEL=28
8 | - ANDROID_BUILD_TOOLS_VERSION=28.0.3
9 |
10 | android:
11 | components:
12 | # The BuildTools version used by your project
13 | - build-tools-$ANDROID_BUILD_TOOLS_VERSION
14 | # The SDK version used to compile your project
15 | - android-$ANDROID_API_LEVEL
16 |
17 | before_cache:
18 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
19 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/
20 |
21 | cache:
22 | directories:
23 | - $HOME/.gradle/caches/
24 | - $HOME/.gradle/wrapper/
25 | - $HOME/.android/build-cache
26 |
27 | licenses:
28 | - 'android-sdk-preview-license-.+'
29 | - 'android-sdk-license-.+'
30 | - 'google-gdk-license-.+'
31 |
32 | script:
33 | - ./gradlew ktlint detekt build check
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # CoroutinesCache
2 |
3 | [](https://travis-ci.org/epam/CoroutinesCache)
4 |
5 | ## Preview
6 | The goal of this library is simple: caching your data models like **Picasso** caches your images, with no effort at all.
7 |
8 | Every Android application is a client application, which means it does not make sense to create and maintain a database just for caching data.
9 |
10 | Plus, the fact that you have some sort of legendary database for persisting your data does not solves by itself the real challenge: to be able to configure your caching needs in a flexible and simple way.
11 |
12 | Inspired by Retrofit api, CoroutinesCache is a reactive caching library for Android and Kotlin which turns your caching needs.
13 |
14 | To get full information about how library works follow next article:
15 |
16 | https://proandroiddev.com/caching-with-kotlin-coroutines-7d819276c820
17 |
18 | #### Note
19 |
20 | Coroutines Cache works only on `kotlin-coroutines:0.26.1` and above.
21 |
22 | ## Getting started
23 |
24 | Library is located in jcenter, add jcenter repository in main gradle file
25 |
26 | ```kotlin
27 | allprojects {
28 | repositories {
29 | ...
30 | jcenter()
31 | }
32 | }
33 | ```
34 |
35 | Grab via Gradle:
36 |
37 | ```kotlin
38 | implementation 'com.epam.coroutinecache:coroutinecache:0.9.6'
39 | ```
40 | or Maven:
41 |
42 | ```kotlin
43 |
44 | com.epam.coroutinecache
45 | coroutinecache
46 | 0.9.6
47 | pom
48 |
49 | ```
50 |
51 |
52 |
53 | ## Usage
54 |
55 | To start use cache you need to create CoroutinesCache object:
56 |
57 | ```kotlin
58 | val coroutinesCache = CoroutinesCache(cacheParams: CacheParams, scope: CoroutineScope)
59 | ```
60 |
61 | `CacheParams` defines cache location, json mapper and persistence size in Mb.
62 |
63 | First param of `CacheParams` is max size that could be saved in persistence in Mb.
64 |
65 | Second param is `JsonMapper` interface that provides one of several implementation: `GsonMapper`, `JacksonMapper`, `MoshiMapper`. For each of those mappers you need to add appropriate dependency in `build.gradle` file.
66 |
67 | Last param is directory where cache files will be stored. Be sure that directory exists and you has write permission there, otherwise you will get `IllegalStateException`
68 |
69 | ```kotlin
70 | class CacheParams (maxPersistenceCacheMB: Int, mapper: JsonMapper, directory: File)
71 | ```
72 |
73 | Next step you need to create an interface with functions that will describe data, which should be stored. For each function you can add params by next annotations:
74 |
75 | 1. `@ProviderKey(key: String, entryClass: EntryClass)` - **key** that will be used for saving data, **entryClass** it is an another annotation `@EntryClass(rawType: KClass<*>, vararg typeParams: EntryClass = [])` that used to specify value's type needed for serialization/deserialization (to avoid obtaining type through reflection). **typeParams** used when you need parameterized type, such annotation for list should looks like: `EntryClass(List::class, EntryClass(Data::class))`
76 | 2. `@LifeTime(value: Long, timeUnit: TimeUnit)` - **value** describes how long record should be stored in memory or persistence. If this annotation isn't set, record will be stored without time limit
77 | 3. `@Expirable` - Set **expirable** param to true. If this annotation isn't set it means that record could be deleted from persistence, even it hasn't reached life limit in persistence low memory case.
78 | 4. `@UseIfExpired` - If this annotation is set it means that data will be retrieved from cache even if record reached its lifetime. Could be used only once, after getting, record will be deleted from cache.
79 |
80 | **Note:** In cases when data can be requested without any parameters you may use a method that should be a suspend function containing only one param that is also a suspend function without params that returns type **T**. It's result will be stored in the cache. And function's return value also should be ****
81 | In case, when you data request call requires some parameters and also you want to store data with different keys that depend on call parameters, then you should use a suspend method that takes an implementation of `ParameterizedDataProvider` interface. In that case data will be stored under modified key that is controlled by your implementation of `parameterizeKey` method.
82 |
83 | To connect interface and CoroutinesCache and your interface just call `CoroutinesCache.using(YourInterface::class.java)`. This method will return interface instance. To save and get data from cache just call methods from returned instance of your interface.
84 | ## Example 1 (no parameters)
85 |
86 | **CacheProviders**
87 | ```kotlin
88 | interface CacheProviders {
89 | @ProviderKey("TestKey", EntryClass(Data::class))
90 | @LifeTime(value = 1L, unit = TimeUnit.MINUTES)
91 | @Expirable
92 | @UseIfExpired
93 | suspend fun getData(dataProvider: suspend () -> Data): Data
94 | }
95 | ```
96 |
97 | **Repository**
98 | ```kotlin
99 | class Repository (private val cacheDirectory: File) {
100 | private val coroutinesCache = CoroutinesCache(CacheParams(10, GsonMapper(), cacheDirectory))
101 | private val restApi = Retrofit...create(RestApi::class.java)
102 | private val cacheProviders = coroutinesCache.using(CacheProviders::class.java)
103 |
104 | suspend fun getData(): Data = cacheProviders.getData(restApi::getData)
105 | }
106 | ```
107 | **MainActivity**
108 | ```kotlin
109 | class MainActivity : AppCompatActivity() {
110 | private val persistence by lazy { Repository(cacheDir) }
111 | override fun onCreate(savedInstanceState: Bundle?) {
112 | super.onCreate(savedInstanceState)
113 | setContentView(R.layout.activity_main)
114 | GlobalScope.launch (Dispatchers.Main) {
115 | val data = persistence.getData()
116 | messageView.text = data.toString()
117 | }
118 | }
119 | }
120 | ````
121 |
122 | ## Example 2 (with parameters)
123 |
124 | **CacheProviders**
125 | ```kotlin
126 | interface CacheProviders {
127 | @ProviderKey("BaseKey", EntryClass(Data::class))
128 | @LifeTime(value = 1L, unit = TimeUnit.MINUTES)
129 | @Expirable
130 | @UseIfExpired
131 | suspend fun getParameterizedData(provider: ParameterizedDataProvider): Data
132 | }
133 | ```
134 |
135 | **Repository**
136 | ```kotlin
137 | class Repository (private val cacheDirectory: File) {
138 | private val coroutinesCache = CoroutinesCache(CacheParams(10, GsonMapper(), cacheDirectory))
139 | private val restApi = Retrofit...create(RestApi::class.java)
140 | private val cacheProviders = coroutinesCache.using(CacheProviders::class.java)
141 |
142 | suspend fun getParametrizedData(search: String): Data = cacheProviders.getParameterizedData(DataProviderImpl())
143 |
144 | private inner class DataProviderImpl(private val search: String) : ParameterizedDataProvider {
145 |
146 | override suspend fun getData(): Data = restApi.getParameterizedData(search)
147 |
148 | override fun parameterizeKey(baseKey: String): String = "${baseKey}_$search"
149 | }
150 | }
151 | ```
152 | **MainActivity**
153 | ```kotlin
154 | class MainActivity : AppCompatActivity() {
155 | private val persistence by lazy { Repository(cacheDir) }
156 | override fun onCreate(savedInstanceState: Bundle?) {
157 | super.onCreate(savedInstanceState)
158 | setContentView(R.layout.activity_main)
159 | GlobalScope.launch (Dispatchers.Main) {
160 | val data = persistence.getParameterizedData("Hello world!")
161 | messageView.text = data.toString()
162 | }
163 | }
164 | }
165 | ````
166 |
167 | To see full example, check app module in this repository.
168 |
169 | ## ProGuard
170 | Do not forget to keep your own data classes in proguard rules. For example, add this line for 'Data' class that is being cached with this library:
171 | ```
172 | # Keep all of your data classes that are stored in CoroutineCache
173 | -keep class com.epam.example.coroutinescache.Data { *; }
174 | ```
175 |
176 | Also, please add following instructions so that CoroutineCache may work properly:
177 | ```
178 | # ProGuard instructions that are required for CoroutineCache to work properly
179 | -keepclassmembers enum com.epam.coroutinecache.core.Source {
180 | public *;
181 | }
182 | ```
183 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/appTest.jks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/epam/CoroutinesCache/ad111bb1d1a6a90849c28e39259f4d564f1aa171/app/appTest.jks
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | apply plugin: 'kotlin-android'
4 |
5 | apply plugin: 'kotlin-android-extensions'
6 |
7 | android {
8 | compileSdkVersion 28
9 | defaultConfig {
10 | applicationId "com.epam.example.coroutinescache"
11 | minSdkVersion 15
12 | targetSdkVersion 28
13 | versionCode appVersionCode.toInteger()
14 | versionName appVersionName
15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
16 | }
17 |
18 | signingConfigs {
19 | releaseConfig {
20 | storeFile file('appTest.jks')
21 | storePassword 'apptest'
22 | keyAlias = 'apptest'
23 | keyPassword 'apptest'
24 | }
25 | }
26 |
27 | buildTypes {
28 | debug {
29 | minifyEnabled false
30 | debuggable true
31 | }
32 |
33 | release {
34 | debuggable false
35 | minifyEnabled true
36 | signingConfig signingConfigs.releaseConfig
37 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
38 | }
39 | }
40 | }
41 |
42 | dependencies {
43 | implementation fileTree(dir: 'libs', include: ['*.jar'])
44 | implementation project(":core")
45 |
46 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
47 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
48 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
49 |
50 | implementation "org.koin:koin-android:${koinVersion}"
51 |
52 | implementation 'com.android.support:appcompat-v7:28.0.0'
53 | implementation 'com.android.support.constraint:constraint-layout:1.1.3'
54 |
55 | implementation 'com.google.code.gson:gson:2.8.5'
56 | implementation 'com.squareup.moshi:moshi:1.6.0'
57 | implementation 'com.fasterxml.jackson.core:jackson-core:2.9.6'
58 |
59 | implementation 'com.squareup.retrofit2:retrofit:2.6.0'
60 | implementation "com.squareup.retrofit2:converter-gson:2.6.0"
61 |
62 | implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'
63 |
64 | testImplementation 'junit:junit:4.12'
65 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
66 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
67 | }
68 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Don't forget to keep your data classes that are stored in CoroutineCache
2 | -keep class com.epam.example.coroutinescache.Data { *; }
3 |
4 |
5 | # ProGuard instructions that are required for CoroutineCache to work properly
6 | -keepclassmembers enum com.epam.coroutinecache.core.Source {
7 | public *;
8 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/epam/example/coroutinescache/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.epam.example.coroutinescache
2 |
3 | import android.support.test.InstrumentationRegistry
4 | import android.support.test.runner.AndroidJUnit4
5 | import org.junit.Assert.assertEquals
6 |
7 | import org.junit.Test
8 | import org.junit.runner.RunWith
9 |
10 | /**
11 | * Instrumented test, which will execute on an Android device.
12 | *
13 | * See [testing documentation](http://d.android.com/tools/testing).
14 | */
15 | @RunWith(AndroidJUnit4::class)
16 | class ExampleInstrumentedTest {
17 | @Test
18 | fun useAppContext() {
19 | // Context of the app under test.
20 | val appContext = InstrumentationRegistry.getTargetContext()
21 | assertEquals("com.epam.example.coroutinescache", appContext.packageName)
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/epam/example/coroutinescache/App.kt:
--------------------------------------------------------------------------------
1 | package com.epam.example.coroutinescache
2 |
3 | import android.app.Application
4 | import org.koin.android.ext.koin.androidContext
5 | import org.koin.core.context.startKoin
6 |
7 | class App : Application() {
8 |
9 | override fun onCreate() {
10 | super.onCreate()
11 | startKoin {
12 | androidContext(this@App)
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/epam/example/coroutinescache/CacheProviders.kt:
--------------------------------------------------------------------------------
1 | package com.epam.example.coroutinescache
2 |
3 | import com.epam.coroutinecache.annotations.ProviderKey
4 | import com.epam.coroutinecache.annotations.LifeTime
5 | import com.epam.coroutinecache.annotations.Expirable
6 | import com.epam.coroutinecache.annotations.EntryClass
7 | import com.epam.coroutinecache.annotations.UseIfExpired
8 | import com.epam.coroutinecache.api.ParameterizedDataProvider
9 | import java.util.concurrent.TimeUnit
10 |
11 | interface CacheProviders {
12 |
13 | @ProviderKey("TestKey", EntryClass(Data::class))
14 | @LifeTime(value = 1L, unit = TimeUnit.MINUTES)
15 | @Expirable
16 | @UseIfExpired
17 | suspend fun getData(dataProvider: suspend () -> Data): Data
18 |
19 | @ProviderKey("AnotherKey", EntryClass(Data::class))
20 | @LifeTime(value = 1L, unit = TimeUnit.MINUTES)
21 | @Expirable
22 | @UseIfExpired
23 | suspend fun getParameterizedData(provider: ParameterizedDataProvider): Data
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/epam/example/coroutinescache/Data.kt:
--------------------------------------------------------------------------------
1 | package com.epam.example.coroutinescache
2 |
3 | data class Data(
4 | val userId: String,
5 | val id: String,
6 | val title: String,
7 | val completed: Boolean
8 | ) {
9 | override fun toString(): String {
10 | return "UserId: $userId, id: $id, title: $title, completed: $completed"
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/epam/example/coroutinescache/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.epam.example.coroutinescache
2 |
3 | import android.os.Bundle
4 | import android.support.v7.app.AppCompatActivity
5 | import kotlinx.android.synthetic.main.activity_main.*
6 | import kotlinx.coroutines.CoroutineScope
7 | import kotlinx.coroutines.Dispatchers.IO
8 | import kotlinx.coroutines.Dispatchers.Main
9 | import kotlinx.coroutines.SupervisorJob
10 | import kotlinx.coroutines.launch
11 | import kotlinx.coroutines.withContext
12 | import kotlin.coroutines.CoroutineContext
13 |
14 | class MainActivity : AppCompatActivity(), CoroutineScope {
15 |
16 | override val coroutineContext: CoroutineContext
17 | get() = SupervisorJob()
18 |
19 | private val persistence by lazy { Repository(cacheDir) }
20 |
21 | override fun onCreate(savedInstanceState: Bundle?) {
22 | super.onCreate(savedInstanceState)
23 | setContentView(R.layout.activity_main)
24 | pressMe.setOnClickListener { onClick() }
25 | }
26 |
27 | private fun onClick() {
28 | launch(Main) {
29 | val data = withContext(IO) { persistence.getParameterizedData("1234") }
30 | val result = data.toString()
31 | messageView.text = result
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/epam/example/coroutinescache/Repository.kt:
--------------------------------------------------------------------------------
1 | package com.epam.example.coroutinescache
2 |
3 | import com.epam.coroutinecache.api.CacheParams
4 | import com.epam.coroutinecache.api.CoroutinesCache
5 | import com.epam.coroutinecache.api.ParameterizedDataProvider
6 | import com.epam.coroutinecache.mappers.GsonMapper
7 | import retrofit2.Retrofit
8 | import retrofit2.converter.gson.GsonConverterFactory
9 | import java.io.File
10 |
11 | class Repository(
12 | cacheDirectory: File
13 | ) {
14 |
15 | private val coroutinesCache: CoroutinesCache = CoroutinesCache(CacheParams(MAX_CACHE_SIZE_MB, GsonMapper(), cacheDirectory))
16 |
17 | private val restApi: RestApi = Retrofit.Builder()
18 | .baseUrl("https://jsonplaceholder.typicode.com")
19 | .addConverterFactory(GsonConverterFactory.create())
20 | .build().create(RestApi::class.java)
21 |
22 | private val cacheProviders: CacheProviders = coroutinesCache.using(CacheProviders::class.java)
23 |
24 | suspend fun getData(): Data = cacheProviders.getData(restApi::getData)
25 |
26 | suspend fun getParameterizedData(search: String): Data = cacheProviders.getParameterizedData(DataProviderImpl(search))
27 |
28 | private inner class DataProviderImpl(private val search: String) : ParameterizedDataProvider {
29 |
30 | override suspend fun getData(): Data = restApi.getParameterizedData(search)
31 |
32 | override fun parameterizeKey(baseKey: String): String = "${baseKey}_$search"
33 | }
34 |
35 | companion object {
36 | private const val MAX_CACHE_SIZE_MB = 10
37 | }
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/epam/example/coroutinescache/RestApi.kt:
--------------------------------------------------------------------------------
1 | package com.epam.example.coroutinescache
2 |
3 | import retrofit2.http.GET
4 | import retrofit2.http.Query
5 |
6 | interface RestApi {
7 |
8 | @GET("todos/1")
9 | suspend fun getData(): Data
10 |
11 | @GET("todos/1")
12 | suspend fun getParameterizedData(@Query("search") search: String): Data
13 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
11 |
16 |
21 |
26 |
31 |
36 |
41 |
46 |
51 |
56 |
61 |
66 |
71 |
76 |
81 |
86 |
91 |
96 |
101 |
106 |
111 |
116 |
121 |
126 |
131 |
136 |
141 |
146 |
151 |
156 |
161 |
166 |
171 |
172 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
20 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/epam/CoroutinesCache/ad111bb1d1a6a90849c28e39259f4d564f1aa171/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/epam/CoroutinesCache/ad111bb1d1a6a90849c28e39259f4d564f1aa171/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/epam/CoroutinesCache/ad111bb1d1a6a90849c28e39259f4d564f1aa171/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/epam/CoroutinesCache/ad111bb1d1a6a90849c28e39259f4d564f1aa171/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/epam/CoroutinesCache/ad111bb1d1a6a90849c28e39259f4d564f1aa171/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/epam/CoroutinesCache/ad111bb1d1a6a90849c28e39259f4d564f1aa171/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/epam/CoroutinesCache/ad111bb1d1a6a90849c28e39259f4d564f1aa171/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/epam/CoroutinesCache/ad111bb1d1a6a90849c28e39259f4d564f1aa171/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/epam/CoroutinesCache/ad111bb1d1a6a90849c28e39259f4d564f1aa171/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/epam/CoroutinesCache/ad111bb1d1a6a90849c28e39259f4d564f1aa171/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | CoroutinesCache
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | apply from: 'ktlint.gradle'
4 | apply from: 'bumpVersion.gradle'
5 |
6 | buildscript {
7 | ext.kotlin_version = '1.3.41'
8 | ext.dokka_version = '0.9.18' // https://plugins.gradle.org/plugin/org.jetbrains.dokka
9 | ext.bintray_version = '0.9.1' // https://github.com/novoda/bintray-release
10 | ext.detektVersion = '1.0.0-RC16' // https://plugins.gradle.org/plugin/io.gitlab.arturbosch.detekt
11 |
12 | ext.koinVersion = '2.0.1' // https://github.com/InsertKoinIO/koin
13 | ext.coroutines_version = '1.2.2'
14 |
15 | repositories {
16 | google()
17 | jcenter()
18 | maven {
19 | url "https://plugins.gradle.org/m2/"
20 | }
21 | }
22 | dependencies {
23 | classpath 'com.android.tools.build:gradle:3.4.2'
24 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
25 | classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}"
26 | classpath "com.novoda:bintray-release:${bintray_version}"
27 | classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$detektVersion"
28 |
29 | // NOTE: Do not place your application dependencies here; they belong
30 | // in the individual module build.gradle files
31 | }
32 | }
33 |
34 | allprojects {
35 | repositories {
36 | google()
37 | jcenter()
38 |
39 | maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
40 | }
41 | }
42 |
43 | subprojects {
44 | apply plugin: 'io.gitlab.arturbosch.detekt'
45 |
46 | detekt {
47 | toolVersion = rootProject.ext.detektVersion
48 | input = files("$projectDir")
49 | filters = ".*/resources/.*,.*/build/.*,.*Test.kt"
50 | config = files("$rootDir/detekt-config.yml")
51 | reports {
52 | xml.enabled = false
53 | html.enabled = true
54 | html.destination = file("$buildDir/reports/detekt.html")
55 | }
56 | }
57 | }
58 |
59 | task clean(type: Delete) {
60 | delete rootProject.buildDir
61 | }
62 |
--------------------------------------------------------------------------------
/bumpVersion.gradle:
--------------------------------------------------------------------------------
1 | // Code by @IlyaEremin
2 | //
3 | // See description at https://medium.com/@IlyaEremin/npm-version-for-gradle-android-9137a7dc273c
4 |
5 | def getVersionName = { getVersionProps()['appVersionName'] }
6 |
7 | def getAppVersionCode() { getVersionProps()['appVersionCode'].toInteger() }
8 |
9 | def getVersionProps() {
10 | def versionPropsFile = file('gradle.properties')
11 | if (!versionPropsFile.exists()) {
12 | versionPropsFile.createNewFile()
13 | }
14 | def versionProps = new Properties()
15 | versionProps.load(new FileInputStream(versionPropsFile))
16 | return versionProps
17 | }
18 |
19 | def getVersionNamePatch = { (getVersionName() =~ /\d+/)[2].toInteger() }
20 |
21 | def getVersionNameMinor = { (getVersionName() =~ /\d+/)[1].toInteger() }
22 |
23 | def getVersionNameMajor = { (getVersionName() =~ /\d+/)[0].toInteger() }
24 |
25 | private void commitAndSetTag(versionName) {
26 | Process createCommit = ['git', 'commit', "-m Release ${versionName}"].execute(null, project.rootDir)
27 | createCommit.waitForProcessOutput(System.out, System.err)
28 |
29 | Process createTag = ['git', 'tag', "v${versionName}".toString()].execute(null, project.rootDir)
30 | createTag.waitForProcessOutput(System.out, System.err)
31 | }
32 |
33 | private void save(major, minor, patch, versionCode) {
34 | save("${major}.${minor}.${patch}".toString(), versionCode.toString())
35 | }
36 |
37 | private void save(versionName, versionCode) {
38 | def versionProps = getVersionProps()
39 | versionProps['appVersionName'] = versionName
40 | versionProps['appVersionCode'] = versionCode
41 | versionProps.store(file('gradle.properties').newWriter(), null)
42 | commitAndSetTag(versionName)
43 | }
44 |
45 | task bumperVersionPatch () {
46 | group = 'bumper'
47 | doLast {
48 | save(getVersionNameMajor(), getVersionNameMinor(), getVersionNamePatch() + 1, getAppVersionCode() + 1)
49 | }
50 | }
51 |
52 | task bumperVersionMinor () {
53 | group = 'bumper'
54 | doLast {
55 | save(getVersionNameMajor(), getVersionNameMinor() + 1, 0, getAppVersionCode() + 1)
56 | }
57 | }
58 |
59 | task bumperVersionMajor () {
60 | group = 'bumper'
61 | doLast {
62 | save(getVersionNameMajor() + 1, 0, 0, getAppVersionCode() + 1)
63 | }
64 | }
--------------------------------------------------------------------------------
/core/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'kotlin'
2 | apply plugin: 'org.jetbrains.dokka'
3 | apply plugin: 'com.novoda.bintray-release'
4 |
5 | version = appVersionName
6 |
7 | dependencies {
8 | implementation fileTree(dir: 'libs', include: ['*.jar'])
9 |
10 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
11 | implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
12 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
13 |
14 | implementation "org.koin:koin-android:${koinVersion}"
15 |
16 | implementation 'com.google.code.gson:gson:2.8.5'
17 | implementation 'com.squareup.moshi:moshi:1.6.0'
18 | implementation 'com.fasterxml.jackson.core:jackson-core:2.9.6'
19 | implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.6'
20 |
21 | testImplementation 'junit:junit:4.12'
22 | testImplementation 'org.mockito:mockito-core:2.21.0'
23 | testImplementation "org.koin:koin-test:${koinVersion}"
24 | }
25 |
26 | dokka {
27 | outputFormat = 'html'
28 | outputDirectory = "$buildDir/javadoc"
29 | }
30 |
31 |
32 | publish {
33 | def groupProjectID = 'com.epam.coroutinecache'
34 | def artifactProjectID = 'coroutinecache'
35 | def publishVersionID = "$version"
36 |
37 | userOrg = 'epam-mobile-cc'
38 | repoName = 'coroutines-cache'
39 | groupId = groupProjectID
40 | artifactId = artifactProjectID
41 | publishVersion = publishVersionID
42 | desc = 'Library for caching your data with no effort at all.'
43 | website = 'https://github.com/epam/CoroutinesCache'
44 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/annotations/Expirable.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.annotations
2 |
3 | /**
4 | * Annotation class using to set caching function expirable value to true.
5 | * It means that function's result could be deleted from persistence if persistence reached its memory limit.
6 | */
7 | @Retention(AnnotationRetention.RUNTIME)
8 | @Target(AnnotationTarget.FUNCTION)
9 | annotation class Expirable
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/annotations/LifeTime.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.annotations
2 |
3 | import java.util.concurrent.TimeUnit
4 |
5 | /**
6 | * Annotation that describes how long function result should be saved in cache
7 | *
8 | * @param value - number that describes lifetime
9 | * @param unit - TimeUnit of lifetime
10 | */
11 | @Retention(AnnotationRetention.RUNTIME)
12 | @Target(AnnotationTarget.FUNCTION)
13 | annotation class LifeTime(val value: Long, val unit: TimeUnit)
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/annotations/ProviderKey.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.annotations
2 |
3 | import kotlin.reflect.KClass
4 |
5 | /**
6 | * Annotation that describes key, which will be used to store function result
7 | *
8 | * @param key - data's key in cache
9 | * @param entryClass - entry's class in cache, [@link com.epam.coroutinecache.annotations.EntryClass]
10 | */
11 | @Retention(AnnotationRetention.RUNTIME)
12 | @Target(AnnotationTarget.FUNCTION)
13 | annotation class ProviderKey(val key: String, val entryClass: EntryClass)
14 |
15 | /**
16 | * Annotation that describes how to build type for entry class
17 | *
18 | * @param rawType - data's raw type, could be Foo:class, for list List::class should be passed
19 | * @param typeParams - data's type params, for example, for List you should pass entire class, for example Foo::class, or for Map you should pass
20 | * two classes - first will be used for key and second for value
21 | */
22 | @Retention(AnnotationRetention.RUNTIME)
23 | @Target(AnnotationTarget.FUNCTION)
24 | annotation class EntryClass(val rawType: KClass<*>, vararg val typeParams: EntryClass = [])
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/annotations/UseIfExpired.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.annotations
2 |
3 | /**
4 | * If this annotation is applying to function, user will get data from cache even lifetime is exceeded
5 | * When data is received, they will be deleted from cache.
6 | */
7 | @Retention(AnnotationRetention.RUNTIME)
8 | @Target(AnnotationTarget.FUNCTION)
9 | annotation class UseIfExpired
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/api/CacheParams.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.api
2 |
3 | import com.epam.coroutinecache.mappers.JsonMapper
4 | import java.io.File
5 |
6 | /**
7 | * Class contains params for CoroutinesCache. Cache directory should exists and has a write permissions.
8 | *
9 | * @param maxPersistenceCacheMB - Int. Max size of cached data in Mb
10 | * @param mapper - JsonMapper. One of implementation JsonMapper interface that will be used for serialization data
11 | * @param directory - File. Directory, where cache files will be stored
12 | */
13 | data class CacheParams(
14 | val maxPersistenceCacheMB: Int,
15 | val mapper: JsonMapper,
16 | val directory: File
17 | ) {
18 | init {
19 | when {
20 | !this.directory.exists() -> throw IllegalArgumentException("Cache directory should be non-null")
21 | !this.directory.canWrite() -> throw IllegalArgumentException("Cache directory is not writable")
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/api/CoroutinesCache.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.api
2 |
3 | import com.epam.coroutinecache.di.actionsModule
4 | import com.epam.coroutinecache.di.cacheModule
5 | import com.epam.coroutinecache.internal.ProxyProvider
6 | import kotlinx.coroutines.CoroutineScope
7 | import kotlinx.coroutines.GlobalScope
8 | import org.koin.core.context.loadKoinModules
9 | import java.lang.reflect.Proxy
10 |
11 | /**
12 | * Entry point of CoroutinesCache. In initialization loads koin modules to apply Dependency Injection.
13 | *
14 | * @param cacheParams - CacheParams. {@see CacheParams}
15 | * @param scope - CoroutinesScope. Scope of the threads, where coroutines will be run
16 | */
17 | class CoroutinesCache(
18 | private val cacheParams: CacheParams,
19 | private val scope: CoroutineScope = GlobalScope
20 | ) {
21 |
22 | private lateinit var proxyProvider: ProxyProvider
23 |
24 | init {
25 | loadKoinModules(listOf(cacheModule, actionsModule))
26 | }
27 |
28 | /**
29 | * Function that receive interface as param and create proxy on it.
30 | */
31 | @Suppress("UNCHECKED_CAST")
32 | fun using(clazz: Class<*>): T {
33 | proxyProvider = ProxyProvider(cacheParams, scope)
34 |
35 | return Proxy.newProxyInstance(clazz.classLoader, arrayOf(clazz), proxyProvider) as T
36 | }
37 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/api/ParameterizedDataProvider.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.api
2 |
3 | /**
4 | * Implementation of this interface will allow CoroutineCache to get data when they are absent in cache.
5 | * It also will allow to modify cache key in situation of parameterized calls.
6 | */
7 | interface ParameterizedDataProvider {
8 |
9 | suspend fun getData(): T
10 |
11 | fun parameterizeKey(baseKey: String): String = baseKey
12 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/core/DiskCache.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.core
2 |
3 | import com.epam.coroutinecache.mappers.JsonMapper
4 | import java.io.File
5 | import java.io.FileWriter
6 | import java.lang.reflect.Type
7 |
8 | class DiskCache(
9 | external val cacheDirectory: File,
10 | private val jsonMapper: JsonMapper
11 | ) : Persistence {
12 |
13 | override fun saveRecord(key: String, record: Record, entryType: Type) = synchronized(this) {
14 | val safetyKey = safetyKey(key)
15 | val type = jsonMapper.newParameterizedType(Record::class.java, entryType)
16 | val serializedJson = jsonMapper.toJson(record, type)
17 |
18 | val resultedFile = File(cacheDirectory, safetyKey)
19 | val writer = FileWriter(resultedFile, false)
20 | writer.write(serializedJson)
21 | writer.flush()
22 | writer.close()
23 | }
24 |
25 | override fun deleteByKey(key: String) {
26 | synchronized(this) {
27 | val safetyKey = safetyKey(key)
28 | val resultedFile = File(cacheDirectory, safetyKey)
29 | resultedFile.delete()
30 | }
31 | }
32 |
33 | override fun deleteAll() {
34 | synchronized(this) {
35 | val files = cacheDirectory.listFiles()
36 | files?.forEach {
37 | it?.delete()
38 | }
39 | }
40 | }
41 |
42 | override fun allKeys(): List {
43 | synchronized(this) {
44 | val keys = mutableListOf()
45 | val files = cacheDirectory.listFiles()
46 | files?.forEach {
47 | if (it.isFile) {
48 | keys.add(it.name)
49 | }
50 | }
51 | return keys
52 | }
53 | }
54 |
55 | override fun storedMB(): Long {
56 | synchronized(this) {
57 | var result: Long = 0
58 | val files = cacheDirectory.listFiles()
59 | files?.forEach {
60 | if (it.isFile) {
61 | result += it.length()
62 | }
63 | }
64 | return (result.toFloat() / sizeMb).toLong()
65 | }
66 | }
67 |
68 | override fun getRecord(key: String, entryType: Type): Record? {
69 | synchronized(this) {
70 | @Suppress("TooGenericExceptionCaught")
71 | return try {
72 | val safetyKey = safetyKey(key)
73 | val resultedFile = File(cacheDirectory, safetyKey)
74 | val type = jsonMapper.newParameterizedType(Record::class.java, entryType)
75 | val diskRecord: Record? = jsonMapper.fromJson(resultedFile, type)
76 | diskRecord?.sizeOnMb = (resultedFile.length().toFloat() / sizeMb)
77 | diskRecord
78 | } catch (exception: Exception) {
79 | null
80 | }
81 | }
82 | }
83 |
84 | private fun safetyKey(key: String) = key.replace("/", "_")
85 |
86 | companion object {
87 | private const val sizeKb = 1024.0f
88 | private const val sizeMb = sizeKb * sizeKb
89 | }
90 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/core/Memory.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.core
2 |
3 | interface Memory {
4 |
5 | /**
6 | * Get the record associated with its particular key
7 | *
8 | * @param key The key associated with the Record to be retrieved from memory
9 | * @see Record
10 | */
11 | fun getRecord(key: String): Record?
12 |
13 | /**
14 | * Save the data supplied based on a certain mechanism which provides storage somehow
15 | *
16 | * @param key The key associated with the record to be saved
17 | * @param record The record to be saved
18 | */
19 | fun saveRecord(key: String, record: Record)
20 |
21 | /**
22 | * Retrieve the keys from all records saved
23 | */
24 | fun keySet(): Set
25 |
26 | /**
27 | * Delete the data associated with its particular key
28 | *
29 | * @param key The key associated with the object to be deleted from persistence
30 | */
31 | fun deleteByKey(key: String)
32 |
33 | /**
34 | * Clear all cached data
35 | */
36 | fun deleteAll()
37 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/core/MemoryCache.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.core
2 |
3 | import com.epam.coroutinecache.utils.CacheLog
4 | import java.util.concurrent.ConcurrentHashMap
5 |
6 | class MemoryCache : Memory {
7 |
8 | private val recordsMap: MutableMap> = ConcurrentHashMap()
9 |
10 | override fun getRecord(key: String): Record? {
11 | @Suppress("TooGenericExceptionCaught")
12 | return try {
13 | if (recordsMap[key] == null) {
14 | null
15 | } else {
16 | recordsMap[key] as Record
17 | }
18 | } catch (e: Exception) {
19 | CacheLog.logError("Cannot get record from memory", e)
20 | null
21 | }
22 | }
23 |
24 | override fun saveRecord(key: String, record: Record) {
25 | recordsMap[key] = record
26 | }
27 |
28 | override fun keySet(): Set = recordsMap.keys as Set
29 |
30 | override fun deleteByKey(key: String) {
31 | recordsMap.remove(key)
32 | }
33 |
34 | override fun deleteAll() {
35 | recordsMap.clear()
36 | }
37 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/core/Persistence.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.core
2 |
3 | import java.lang.reflect.Type
4 |
5 | /**
6 | * Provides the persistence layer for the cache A default implementation which store the objects in
7 | * disk is supplied:
8 | *
9 | * @see DiskCache
10 | */
11 | interface Persistence {
12 |
13 | /**
14 | * Save the data supplied based on a certain mechanism which provides persistence somehow
15 | *
16 | * @param key The key associated with the record to be persisted
17 | * @param record The record to be persisted
18 | */
19 | fun saveRecord(key: String, record: Record, entryType: Type)
20 |
21 | /**
22 | * Delete the data associated with its particular key
23 | *
24 | * @param key The key associated with the object to be deleted from persistence
25 | */
26 | fun deleteByKey(key: String)
27 |
28 | /**
29 | * Delete all the data
30 | */
31 | fun deleteAll()
32 |
33 | /**
34 | * Retrieve the keys from all records persisted
35 | */
36 | fun allKeys(): List
37 |
38 | /**
39 | * Retrieve accumulated memory records in megabytes
40 | */
41 | fun storedMB(): Long
42 |
43 | /**
44 | * Get the record associated with its particular key
45 | *
46 | * @param key The key associated with the Record to be retrieved from persistence
47 | * @param type Type that used to object deserialization
48 | * @see Record
49 | */
50 | fun getRecord(key: String, entryType: Type): Record?
51 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/core/Record.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.core
2 |
3 | /**
4 | * Class that will be stored in the cache. Contains data and all information about record in the cache
5 | *
6 | * @param data - Any data that need to be stored.
7 | * @param expirable - Boolean that defines possibility of deleting record from cache in low memory case
8 | * @param lifeTimeMillis - Long. Record life time in millis
9 | * @param source - Source from data was retrieved
10 | */
11 | class Record(
12 | private val data: T? = null,
13 | private var expirable: Boolean = true,
14 | private var lifeTimeMillis: Long = 0
15 | ) {
16 | private var source: Source = Source.MEMORY
17 | private val timeAtWhichWasPersisted: Long = System.currentTimeMillis()
18 |
19 | @Transient
20 | var sizeOnMb: Float = 0.0f
21 |
22 | fun setSource(source: Source) {
23 | this.source = source
24 | }
25 |
26 | fun setLifeTime(lifeTime: Long) {
27 | this.lifeTimeMillis = lifeTime
28 | }
29 |
30 | fun getData(): T? = data
31 |
32 | fun getLifeTimeMillis(): Long = lifeTimeMillis
33 |
34 | fun getTimeAtWhichWasPersisted(): Long = timeAtWhichWasPersisted
35 |
36 | fun getSource(): Source = source
37 |
38 | fun isExpirable(): Boolean = expirable
39 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/core/Source.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.core
2 |
3 | /**
4 | * Enum that shows where data came from
5 | */
6 | enum class Source(type: String) {
7 | MEMORY("memory"),
8 | PERSISTENCE("persistence"),
9 | CLOUD("cloud")
10 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/core/actions/DeleteExpirableRecordsAction.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.core.actions
2 |
3 | import com.epam.coroutinecache.core.Persistence
4 | import kotlinx.coroutines.CoroutineScope
5 | import kotlinx.coroutines.async
6 |
7 | /**
8 | * Action that works with persistence and remove expirable records if
9 | * persistence reached memory limit.
10 | */
11 | class DeleteExpirableRecordsAction(
12 | private val maxMgPersistenceCache: Int,
13 | private val diskCache: Persistence,
14 | private val scope: CoroutineScope
15 | ) {
16 |
17 | /**
18 | * Async function checks if persistence reached memory limit and remove expirable
19 | * records until memory is not enough to store new records.
20 | */
21 | fun deleteExpirableRecords() = scope.async {
22 | val storedMB = diskCache.storedMB()
23 | if (!reachedPercentageMemoryToStart(storedMB)) {
24 | return@async
25 | }
26 | val keys = diskCache.allKeys()
27 |
28 | var releasedMB = 0.0f
29 | run loop@{
30 | keys.forEach {
31 | if (reachedPercentageMemoryToStop(storedMB, releasedMB)) {
32 | return@loop
33 | }
34 | val record = diskCache.getRecord(it, Any::class.java)
35 | if (record == null || !record.isExpirable()) return@forEach
36 | diskCache.deleteByKey(it)
37 | releasedMB += record.sizeOnMb
38 | }
39 | }
40 | }
41 |
42 | private fun reachedPercentageMemoryToStop(storedMBWhenStarted: Long, releasedMBSoFar: Float): Boolean {
43 | val currentStoredMB = storedMBWhenStarted - releasedMBSoFar
44 | val requiredStoredMBToStop = maxMgPersistenceCache * PERCENTAGE_MEMORY_STORED_TO_STOP
45 | return currentStoredMB <= requiredStoredMBToStop
46 | }
47 |
48 | private fun reachedPercentageMemoryToStart(storedMB: Long): Boolean {
49 | val requiredStoredMBToStart = maxMgPersistenceCache * PERCENTAGE_MEMORY_STORED_TO_START
50 | return storedMB >= requiredStoredMBToStart
51 | }
52 |
53 | companion object {
54 | private const val PERCENTAGE_MEMORY_STORED_TO_START = 0.95f
55 | private const val PERCENTAGE_MEMORY_STORED_TO_STOP = 0.7f
56 | }
57 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/core/actions/DeleteExpiredRecordsAction.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.core.actions
2 |
3 | import com.epam.coroutinecache.core.Memory
4 | import com.epam.coroutinecache.core.Persistence
5 | import com.epam.coroutinecache.core.Record
6 | import com.epam.coroutinecache.internal.RecordExpiredChecker
7 |
8 | /**
9 | * Class that used to delete all expired records.
10 | */
11 | class DeleteExpiredRecordsAction(
12 | private val diskCache: Persistence,
13 | private val memory: Memory,
14 | private val recordExpiredChecker: RecordExpiredChecker
15 | ) {
16 |
17 | /**
18 | *
19 | */
20 | fun deleteExpiredRecords() {
21 | val diskCacheKeys = diskCache.allKeys()
22 |
23 | diskCacheKeys.forEach {
24 | val record = diskCache.getRecord(it, Any::class.java)
25 |
26 | if (record != null && recordExpiredChecker.hasRecordExpired(record)) {
27 | diskCache.deleteByKey(it)
28 | }
29 | }
30 |
31 | val memoryCacheKeys = memory.keySet()
32 |
33 | memoryCacheKeys.forEach {
34 | val record = memory.getRecord(it)
35 |
36 | if (record != null && recordExpiredChecker.hasRecordExpired(record)) {
37 | memory.deleteByKey(it)
38 | }
39 | }
40 | }
41 |
42 | /**
43 | * Fun to check that record is expired and if it is, delete it
44 | *
45 | * @param key - string, that is using to delete record from memory and persistence
46 | * @param record - Record, which is checked if it is expired
47 | */
48 | fun deleteExpiredRecord(key: String, record: Record<*>) {
49 | if (recordExpiredChecker.hasRecordExpired(record)) {
50 | diskCache.deleteByKey(key)
51 | memory.deleteByKey(key)
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/core/actions/DeleteRecordAction.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.core.actions
2 |
3 | import com.epam.coroutinecache.core.Memory
4 | import com.epam.coroutinecache.core.Persistence
5 | import kotlinx.coroutines.CoroutineScope
6 | import kotlinx.coroutines.async
7 |
8 | /**
9 | * Action to delete record from cache
10 | */
11 | class DeleteRecordAction(
12 | private val diskCache: Persistence,
13 | private val memory: Memory,
14 | private val scope: CoroutineScope
15 | ) {
16 |
17 | /**
18 | * Async function that deletes record from cache by key
19 | *
20 | * @param key - String, key of the record
21 | */
22 | suspend fun deleteByKey(key: String) = scope.async {
23 | diskCache.deleteByKey(key)
24 | memory.deleteByKey(key)
25 | }.await()
26 |
27 | /**
28 | * Async function that deletes all record from cache
29 | */
30 | suspend fun deleteAll() = scope.async {
31 | diskCache.deleteAll()
32 | memory.deleteAll()
33 | }.await()
34 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/core/actions/GetRecordAction.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.core.actions
2 |
3 | import com.epam.coroutinecache.core.Memory
4 | import com.epam.coroutinecache.core.Persistence
5 | import com.epam.coroutinecache.core.Record
6 | import com.epam.coroutinecache.core.Source
7 | import com.epam.coroutinecache.internal.RecordExpiredChecker
8 | import kotlinx.coroutines.CoroutineScope
9 | import kotlinx.coroutines.async
10 | import java.lang.reflect.Type
11 |
12 | /**
13 | * Action that retrieves record from the cache
14 | */
15 | class GetRecordAction(
16 | private val deleteExpiredRecordsAction: DeleteExpiredRecordsAction,
17 | private val recordExpiredChecker: RecordExpiredChecker,
18 | private val diskCache: Persistence,
19 | private val memory: Memory,
20 | private val scope: CoroutineScope
21 | ) {
22 |
23 | /**
24 | * Async function that retrieves record from cache.
25 | * If record is expired and flag useIfExpired is true then return record and delete it from cache,
26 | * otherwise only delete record from cache and return null
27 | */
28 | suspend fun getRecord(key: String, entryType: Type, useRecordEvenIfExpired: Boolean = false): Record? = scope.async {
29 | var record = memory.getRecord(key)
30 |
31 | if (record != null) {
32 | record.setSource(Source.MEMORY)
33 | } else {
34 | record = diskCache.getRecord(key, entryType)
35 | if (record != null) {
36 | record.setSource(Source.PERSISTENCE)
37 | memory.saveRecord(key, record)
38 | } else {
39 | return@async null
40 | }
41 | }
42 |
43 | if (recordExpiredChecker.hasRecordExpired(record)) {
44 | deleteExpiredRecordsAction.deleteExpiredRecord(key, record)
45 | return@async if (useRecordEvenIfExpired) record else null
46 | }
47 |
48 | return@async record
49 | }.await()
50 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/core/actions/SaveRecordAction.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.core.actions
2 |
3 | import com.epam.coroutinecache.core.Memory
4 | import com.epam.coroutinecache.core.Persistence
5 | import com.epam.coroutinecache.core.Record
6 | import kotlinx.coroutines.CoroutineScope
7 | import kotlinx.coroutines.async
8 | import org.koin.core.KoinComponent
9 | import org.koin.core.inject
10 | import org.koin.core.parameter.parametersOf
11 | import java.lang.reflect.Type
12 |
13 | /**
14 | * Action that save record in the cache
15 | */
16 | class SaveRecordAction(
17 | private val diskCache: Persistence,
18 | private val memory: Memory,
19 | private val maxMbCacheSize: Int,
20 | private val scope: CoroutineScope
21 | ) : KoinComponent {
22 |
23 | private val deleteExpirableRecordsAction: DeleteExpirableRecordsAction by inject { parametersOf(maxMbCacheSize, scope) }
24 |
25 | /**
26 | * Async function that saves data in the cache and memory. After saving delete all expirable data from cache
27 | * if memory limit is reached. If memory limit is reached, bot there is nothing to delete from cache,
28 | * then print message that record can't be saved
29 | */
30 | suspend fun save(key: String, data: Any?, entryType: Type, lifeTimeMillis: Long = 0, isExpirable: Boolean = true) = scope.async {
31 | val record = Record(data, isExpirable, lifeTimeMillis)
32 |
33 | memory.saveRecord(key, record)
34 |
35 | if (diskCache.storedMB() >= maxMbCacheSize) {
36 | System.out.println("Record can not be persisted because it would exceed the max limit megabytes settled down")
37 | } else {
38 | diskCache.saveRecord(key, record, entryType)
39 | }
40 |
41 | deleteExpirableRecordsAction.deleteExpirableRecords().await()
42 | }
43 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/di/ActionsModule.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.di
2 |
3 | import com.epam.coroutinecache.core.actions.DeleteExpirableRecordsAction
4 | import com.epam.coroutinecache.core.actions.DeleteExpiredRecordsAction
5 | import com.epam.coroutinecache.core.actions.DeleteRecordAction
6 | import com.epam.coroutinecache.core.actions.GetRecordAction
7 | import com.epam.coroutinecache.core.actions.SaveRecordAction
8 | import kotlinx.coroutines.CoroutineScope
9 | import org.koin.core.qualifier.StringQualifier
10 | import org.koin.dsl.bind
11 | import org.koin.dsl.module
12 |
13 | /**
14 | * Koin module that contains all actions with cache
15 | */
16 | val actionsModule = module(override = true) {
17 |
18 | single(qualifier = StringQualifier("DeleteExpiredRecordAction")) {
19 | DeleteExpiredRecordsAction(
20 | get(qualifier = StringQualifier("DiskCache")),
21 | get(qualifier = StringQualifier("MemoryCache")),
22 | get(qualifier = StringQualifier("RecordExpiredChecker")))
23 | } bind DeleteExpiredRecordsAction::class
24 |
25 | single(qualifier = StringQualifier("DeleteExpirableRecordsAction")) { (maxMgPersistenceCache: Int, scope: CoroutineScope) ->
26 | DeleteExpirableRecordsAction(maxMgPersistenceCache,
27 | get(qualifier = StringQualifier("DiskCache")),
28 | scope)
29 | } bind DeleteExpirableRecordsAction::class
30 |
31 | factory(qualifier = StringQualifier("DeleteRecordAction")) { (scope: CoroutineScope) ->
32 | DeleteRecordAction(
33 | get(qualifier = StringQualifier("DiskCache")),
34 | get(qualifier = StringQualifier("MemoryCache")), scope)
35 | } bind DeleteRecordAction::class
36 |
37 | factory(qualifier = StringQualifier("GetRecordAction")) { (scope: CoroutineScope) ->
38 | GetRecordAction(
39 | get(qualifier = StringQualifier("DeleteExpiredRecordAction")),
40 | get(qualifier = StringQualifier("RecordExpiredChecker")),
41 | get(qualifier = StringQualifier("DiskCache")),
42 | get(qualifier = StringQualifier("MemoryCache")), scope)
43 | } bind GetRecordAction::class
44 |
45 | factory(qualifier = StringQualifier("SaveAction")) { (maxMbCacheSize: Int, scope: CoroutineScope) ->
46 | SaveRecordAction(
47 | get(qualifier = StringQualifier("DiskCache")),
48 | get(qualifier = StringQualifier("MemoryCache")),
49 | maxMbCacheSize,
50 | scope)
51 | } bind SaveRecordAction::class
52 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/di/CacheModule.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.di
2 |
3 | import com.epam.coroutinecache.core.DiskCache
4 | import com.epam.coroutinecache.core.Memory
5 | import com.epam.coroutinecache.core.MemoryCache
6 | import com.epam.coroutinecache.core.Persistence
7 | import com.epam.coroutinecache.internal.ProxyTranslator
8 | import com.epam.coroutinecache.internal.RecordExpiredChecker
9 | import com.epam.coroutinecache.mappers.JsonMapper
10 | import org.koin.core.qualifier.StringQualifier
11 | import org.koin.dsl.bind
12 | import org.koin.dsl.module
13 | import java.io.File
14 |
15 | /**
16 | * Koin modules that contains all instances regarding to cache
17 | */
18 | val cacheModule = module(override = true) {
19 |
20 | single(qualifier = StringQualifier("DiskCache"), override = true) { (cacheDirectory: File, mapper: JsonMapper) ->
21 | DiskCache(cacheDirectory, mapper)
22 | } bind Persistence::class
23 |
24 | single(qualifier = StringQualifier("MemoryCache")) {
25 | MemoryCache()
26 | } bind Memory::class
27 |
28 | factory(qualifier = StringQualifier("RecordExpiredChecker")) {
29 | RecordExpiredChecker()
30 | }
31 |
32 | single(qualifier = StringQualifier("ProxyTranslator")) {
33 | ProxyTranslator()
34 | } bind ProxyTranslator::class
35 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/internal/CacheObjectParams.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.internal
2 |
3 | import com.epam.coroutinecache.api.ParameterizedDataProvider
4 | import java.lang.reflect.Type
5 | import java.util.concurrent.TimeUnit
6 |
7 | data class CacheObjectParams(
8 | var key: String = "",
9 | var lifeTime: Long = 0L,
10 | var timeUnit: TimeUnit = TimeUnit.MILLISECONDS,
11 | var isExpirable: Boolean = false,
12 | var useIfExpired: Boolean = false,
13 | var dataProvider: ParameterizedDataProvider<*>? = null,
14 | var entryType: Type? = null
15 | )
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/internal/ProcessorProvider.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.internal
2 |
3 | interface ProcessorProvider {
4 |
5 | /**
6 | * Provide the data from the cache
7 | *
8 | * @param cacheObjectParams params for requesting cache object
9 | * @return the associated data
10 | */
11 | suspend fun process(cacheObjectParams: CacheObjectParams?): T?
12 |
13 | /**
14 | * Destroy the entire cache
15 | */
16 | suspend fun deleteAll()
17 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/internal/ProcessorProviderImpl.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.internal
2 |
3 | import com.epam.coroutinecache.api.CacheParams
4 | import com.epam.coroutinecache.core.Source
5 | import com.epam.coroutinecache.core.actions.DeleteRecordAction
6 | import com.epam.coroutinecache.core.actions.GetRecordAction
7 | import com.epam.coroutinecache.core.actions.SaveRecordAction
8 | import com.epam.coroutinecache.utils.CacheLog
9 | import kotlinx.coroutines.CoroutineScope
10 | import org.koin.core.KoinComponent
11 | import org.koin.core.inject
12 | import org.koin.core.parameter.parametersOf
13 |
14 | class ProcessorProviderImpl(
15 | private val cacheParams: CacheParams,
16 | private val scope: CoroutineScope
17 | ) : ProcessorProvider, KoinComponent {
18 |
19 | private val saveRecordAction: SaveRecordAction by inject { parametersOf(cacheParams.maxPersistenceCacheMB, scope) }
20 |
21 | private val deleteRecordAction: DeleteRecordAction by inject { parametersOf(scope) }
22 |
23 | private val getRecordAction: GetRecordAction by inject { parametersOf(scope) }
24 |
25 | override suspend fun process(cacheObjectParams: CacheObjectParams?): T? {
26 | if (cacheObjectParams == null) return null
27 | val record = getRecordAction.getRecord(cacheObjectParams.key, cacheObjectParams.entryType!!, cacheObjectParams.useIfExpired)
28 | return if (record == null) {
29 | deleteRecordAction.deleteByKey(cacheObjectParams.key)
30 | val data = cacheObjectParams.dataProvider?.getData() as T?
31 | saveRecordAction
32 | .save(cacheObjectParams.key,
33 | data,
34 | cacheObjectParams.entryType!!,
35 | cacheObjectParams.timeUnit.toMillis(cacheObjectParams.lifeTime),
36 | cacheObjectParams.isExpirable)
37 | .await()
38 | CacheLog.logMessage("Got data from source: ${Source.CLOUD}")
39 | data
40 | } else {
41 | CacheLog.logMessage("Got data from source: ${record.getSource()}")
42 | record.getData()
43 | }
44 | }
45 |
46 | override suspend fun deleteAll() {
47 | deleteRecordAction.deleteAll()
48 | }
49 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/internal/ProxyProvider.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.internal
2 |
3 | import com.epam.coroutinecache.api.CacheParams
4 | import com.epam.coroutinecache.core.Persistence
5 | import kotlinx.coroutines.CoroutineScope
6 | import kotlinx.coroutines.launch
7 | import org.koin.core.KoinComponent
8 | import org.koin.core.get
9 | import org.koin.core.inject
10 | import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
11 | import org.koin.core.parameter.parametersOf
12 | import java.lang.reflect.InvocationHandler
13 | import java.lang.reflect.Method
14 | import kotlin.coroutines.Continuation
15 | import kotlin.coroutines.resume
16 |
17 | class ProxyProvider(
18 | private val cacheParams: CacheParams,
19 | private val scope: CoroutineScope
20 | ) : InvocationHandler, KoinComponent {
21 |
22 | private val diskCache: Persistence = get { parametersOf(cacheParams.directory, cacheParams.mapper) }
23 |
24 | private val proxyTranslator: ProxyTranslator by inject()
25 |
26 | private val processorProvider: ProcessorProvider = ProcessorProviderImpl(cacheParams, scope)
27 |
28 | override fun invoke(proxy: Any?, method: Method?, methodArgs: Array?): Any? {
29 | val lastArg = methodArgs?.lastOrNull()
30 | return if (lastArg is Continuation<*>) {
31 | @Suppress("UNCHECKED_CAST")
32 | val cont = lastArg as? Continuation
33 | val otherArgs = methodArgs.take(methodArgs.size - 1).toTypedArray()
34 | scope.launch {
35 | val data = processorProvider.process(proxyTranslator.processMethod(method, otherArgs))
36 | cont?.resume(data)
37 | }
38 | COROUTINE_SUSPENDED
39 | } else {
40 | null
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/internal/ProxyTranslator.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.internal
2 |
3 | import com.epam.coroutinecache.annotations.Expirable
4 | import com.epam.coroutinecache.annotations.LifeTime
5 | import com.epam.coroutinecache.annotations.ProviderKey
6 | import com.epam.coroutinecache.annotations.UseIfExpired
7 | import com.epam.coroutinecache.api.ParameterizedDataProvider
8 | import com.epam.coroutinecache.utils.NoParamsDataProvider
9 | import com.epam.coroutinecache.utils.Types
10 | import java.lang.reflect.Method
11 | import java.lang.reflect.Type
12 | import java.util.concurrent.TimeUnit
13 | import kotlin.reflect.KCallable
14 |
15 | /**
16 | * Class that retrieves all object params from annotations and function that
17 | * returns data that should be stored
18 | */
19 | class ProxyTranslator {
20 |
21 | @Suppress("ReturnCount")
22 | fun processMethod(method: Method?, methodArgs: Array?): CacheObjectParams? {
23 | if (method == null) {
24 | return null
25 | }
26 | val cacheObjectParams = CacheObjectParams()
27 | val lifeTime = getMethodLifeTime(method)
28 | if (lifeTime != null) {
29 | cacheObjectParams.lifeTime = lifeTime.first
30 | cacheObjectParams.timeUnit = lifeTime.second
31 | }
32 | cacheObjectParams.isExpirable = isMethodExpirable(method)
33 | cacheObjectParams.useIfExpired = useMethodIfExpired(method)
34 | cacheObjectParams.dataProvider = getDataSuspend(method, methodArgs)
35 | val baseKey = getMethodKey(method)
36 | cacheObjectParams.key = cacheObjectParams.dataProvider?.parameterizeKey(baseKey) ?: baseKey
37 | cacheObjectParams.entryType = getMethodType(method)
38 |
39 | return cacheObjectParams
40 | }
41 |
42 | private fun getMethodLifeTime(method: Method): Pair? {
43 | val lifeTimeAnnotation = method.getAnnotation(LifeTime::class.java) ?: return null
44 | return Pair(lifeTimeAnnotation.value, lifeTimeAnnotation.unit)
45 | }
46 |
47 | private fun isMethodExpirable(method: Method): Boolean {
48 | val annotation = method.getAnnotation(Expirable::class.java)
49 | return annotation != null
50 | }
51 |
52 | private fun useMethodIfExpired(method: Method): Boolean {
53 | val annotation = method.getAnnotation(UseIfExpired::class.java)
54 | return annotation != null
55 | }
56 |
57 | private fun getMethodKey(method: Method): String {
58 | val annotation = method.getAnnotation(ProviderKey::class.java)
59 | ?: return method.name + method.declaringClass + method.returnType
60 | return annotation.key
61 | }
62 |
63 | private fun getMethodType(method: Method): Type {
64 | val providerAnnotation = method.getAnnotation(ProviderKey::class.java)
65 | return Types.obtainTypeFromAnnotation(providerAnnotation.entryClass)
66 | }
67 |
68 | private fun getDataSuspend(method: Method, methodArgs: Array?): ParameterizedDataProvider<*> {
69 | val dataProvider = getObjectFromMethodParam(method, ParameterizedDataProvider::class.java, methodArgs)
70 | if (dataProvider != null) {
71 | return dataProvider
72 | }
73 | val callable = getObjectFromMethodParam(method, KCallable::class.java, methodArgs)
74 | if (callable == null || !callable.isSuspend)
75 | throw IllegalStateException("${method.name} requires a ParameterizedDataProvider implementation or parameterless suspend function")
76 | return NoParamsDataProvider(callable)
77 | }
78 |
79 | private fun getObjectFromMethodParam(method: Method, expectedClass: Class, methodArgs: Array?): T? {
80 | if (methodArgs == null) return null
81 | var countSameObjectsType = 0
82 | var expectedObject: T? = null
83 |
84 | for (objectParam in methodArgs) {
85 | if (expectedClass.isAssignableFrom(objectParam::class.java)) {
86 | expectedObject = objectParam as T
87 | ++countSameObjectsType
88 | }
89 | }
90 |
91 | if (countSameObjectsType > 1) {
92 | throw IllegalArgumentException("${method.name} requires just one instance of type ${expectedClass.simpleName}")
93 | }
94 |
95 | return expectedObject
96 | }
97 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/internal/RecordExpiredChecker.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.internal
2 |
3 | import com.epam.coroutinecache.core.Record
4 |
5 | /**
6 | * Helper class that checks is record expired
7 | */
8 | class RecordExpiredChecker {
9 |
10 | fun hasRecordExpired(record: Record<*>): Boolean {
11 | val now = System.currentTimeMillis()
12 |
13 | val lifetime = record.getLifeTimeMillis()
14 | if (lifetime == 0L) return false
15 |
16 | return now - record.getTimeAtWhichWasPersisted() > lifetime
17 | }
18 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/mappers/GsonMapper.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.mappers
2 |
3 | import com.epam.coroutinecache.utils.Types
4 | import com.google.gson.Gson
5 | import java.io.BufferedReader
6 | import java.io.File
7 | import java.io.FileReader
8 | import java.lang.reflect.ParameterizedType
9 | import java.lang.reflect.Type
10 |
11 | class GsonMapper(
12 | private val gson: Gson = Gson()
13 | ) : JsonMapper {
14 |
15 | override fun toJson(src: Any, typeOfSrc: Type): String = gson.toJson(src, typeOfSrc)
16 |
17 | override fun fromJson(json: String, type: Type): T? = gson.fromJson(json, type)
18 |
19 | override fun fromJson(file: File, typeOfT: Type): T? {
20 | val reader = BufferedReader(FileReader(file.absoluteFile))
21 | val objectValue: T = gson.fromJson(reader, typeOfT)
22 | reader.close()
23 | return objectValue
24 | }
25 |
26 | @Suppress("SpreadOperator")
27 | override fun newParameterizedType(rawType: Type, vararg typeArguments: Type): ParameterizedType =
28 | Types.newParameterizedType(rawType, *typeArguments)
29 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/mappers/JacksonMapper.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.mappers
2 |
3 | import com.epam.coroutinecache.utils.Types
4 | import com.fasterxml.jackson.core.type.TypeReference
5 | import com.fasterxml.jackson.databind.DeserializationFeature
6 | import com.fasterxml.jackson.databind.ObjectMapper
7 | import java.io.File
8 | import java.lang.reflect.ParameterizedType
9 | import java.lang.reflect.Type
10 |
11 | class JacksonMapper(
12 | private val objectMapper: ObjectMapper = ObjectMapper()
13 | ) : JsonMapper {
14 |
15 | init {
16 | this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
17 | }
18 |
19 | override fun toJson(src: Any, typeOfSrc: Type): String = objectMapper.writeValueAsString(src)
20 |
21 | override fun fromJson(json: String, type: Type): T {
22 | val typeReference: TypeReference = object : TypeReference() {
23 | override fun getType(): Type = type
24 | }
25 | return objectMapper.readValue(json, typeReference)
26 | }
27 |
28 | override fun fromJson(file: File, typeOfT: Type): T? {
29 | val typeReference: TypeReference = object : TypeReference() {
30 | override fun getType(): Type = typeOfT
31 | }
32 | return objectMapper.readValue(file, typeReference)
33 | }
34 |
35 | @Suppress("SpreadOperator")
36 | override fun newParameterizedType(rawType: Type, vararg typeArguments: Type): ParameterizedType =
37 | Types.newParameterizedType(rawType, *typeArguments)
38 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/mappers/JsonMapper.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.mappers
2 |
3 | import java.io.File
4 | import java.lang.reflect.ParameterizedType
5 | import java.lang.reflect.Type
6 |
7 | interface JsonMapper {
8 |
9 | /**
10 | * This method serializes the specified object, including those of generic types, into its
11 | * equivalent Json representation. This method must be used if the specified object is a generic
12 | * type. For non-generic objects, use [.toJson] instead.
13 | * @param src the object for which JSON representation is to be created
14 | * @param typeOfSrc The specific genericized type of src.
15 | * @return Json representation of `src`
16 | */
17 | fun toJson(src: Any, typeOfSrc: Type): String
18 |
19 | /**
20 | * This method deserializes the specified Json into an object of the specified type. This method
21 | * is useful if the specified object is a generic type. For non-generic objects, use
22 | * [.fromJson] instead. If you have the Json in a [File] instead of
23 | * a String, use [.fromJson] instead.
24 | */
25 | @Throws(RuntimeException::class)
26 | fun fromJson(json: String, type: Type): T?
27 |
28 | /**
29 | * This method deserializes the Json read from the specified reader into an object of the
30 | * specified type. This method is useful if the specified object is a generic type. For
31 | * non-generic objects, use [.fromJson] instead. If you have the Json in a
32 | * String form instead of a [File], use [.fromJson] instead.
33 | * @param the type of the desired object
34 | * @param file the file producing Json from which the object is to be deserialized
35 | * @param typeOfT The specific genericized type of src.
36 | * @return an object of type T from the json.
37 | */
38 | @Throws(RuntimeException::class)
39 | fun fromJson(file: File, typeOfT: Type): T?
40 |
41 | /**
42 | * Returns a new parameterized type, applying `typeArguments` to `rawType`.
43 | */
44 | fun newParameterizedType(rawType: Type, vararg typeArguments: Type): ParameterizedType
45 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/mappers/MoshiMapper.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.mappers
2 |
3 | import com.squareup.moshi.JsonReader
4 | import com.squareup.moshi.Moshi
5 | import com.squareup.moshi.Types
6 | import okio.Okio
7 | import java.io.File
8 | import java.lang.reflect.ParameterizedType
9 | import java.lang.reflect.Type
10 |
11 | class MoshiMapper(
12 | private val moshi: Moshi = Moshi.Builder().build()
13 | ) : JsonMapper {
14 |
15 | override fun toJson(src: Any, typeOfSrc: Type): String {
16 | val jsonAdapter = moshi.adapter(typeOfSrc)
17 | return jsonAdapter.toJson(src)
18 | }
19 |
20 | override fun fromJson(json: String, type: Type): T? {
21 | val jsonAdapter = moshi.adapter(type)
22 | return jsonAdapter.fromJson(json)
23 | }
24 |
25 | override fun fromJson(file: File, typeOfT: Type): T? {
26 | val bufferedSource = Okio.buffer(Okio.source(file))
27 | val jsonAdapter = moshi.adapter(typeOfT)
28 | return jsonAdapter.fromJson(JsonReader.of(bufferedSource))
29 | }
30 |
31 | @Suppress("SpreadOperator")
32 | override fun newParameterizedType(rawType: Type, vararg typeArguments: Type): ParameterizedType =
33 | Types.newParameterizedType(rawType, *typeArguments)
34 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/utils/CacheLog.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.utils
2 |
3 | @Suppress("ConstantConditionIf")
4 | object CacheLog {
5 |
6 | private const val logsEnabled = false
7 |
8 | fun logMessage(message: String) {
9 | if (logsEnabled) {
10 | println(message)
11 | }
12 | }
13 |
14 | fun logError(message: String, throwable: Throwable) {
15 | if (logsEnabled) {
16 | println("Error: $message and error message: ${throwable.localizedMessage}")
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/utils/NoParamsDataProvider.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.utils
2 |
3 | import com.epam.coroutinecache.api.ParameterizedDataProvider
4 | import kotlin.reflect.KCallable
5 | import kotlin.reflect.full.callSuspend
6 |
7 | class NoParamsDataProvider(private val callable: KCallable) : ParameterizedDataProvider {
8 |
9 | override suspend fun getData(): T = callable.callSuspend()
10 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/utils/Types.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.utils
2 |
3 | import com.epam.coroutinecache.annotations.EntryClass
4 | import java.lang.reflect.GenericArrayType
5 | import java.lang.reflect.Modifier
6 | import java.lang.reflect.ParameterizedType
7 | import java.lang.reflect.Type
8 | import java.lang.reflect.WildcardType
9 |
10 | /**
11 | * Factory methods for types.
12 | */
13 | object Types {
14 |
15 | private val EMPTY_TYPE_ARRAY = emptyArray()
16 |
17 | fun obtainTypeFromAnnotation(annotation: EntryClass): Type {
18 | return if (annotation.typeParams.isEmpty()) {
19 | if (annotation.rawType.javaObjectType.isArray) {
20 | arrayOf(annotation.rawType.javaObjectType)
21 | } else {
22 | annotation.rawType.javaObjectType
23 | }
24 | } else {
25 | @Suppress("SpreadOperator")
26 | newParameterizedType(annotation.rawType.javaObjectType, *annotation.typeParams.map { obtainTypeFromAnnotation(it) }.toTypedArray())
27 | }
28 | }
29 |
30 | /**
31 | * Returns a new parameterized type, applying {@code typeArguments} to {@code rawType}.
32 | */
33 | @Suppress("SpreadOperator")
34 | fun newParameterizedType(rawType: Type, vararg types: Type): ParameterizedType = ParameterizedTypeImpl(null, rawType, arrayOf(*types))
35 |
36 | /**
37 | * Returns an array type whose elements are all instances of `componentType`.
38 | */
39 | fun arrayOf(componentType: Type): GenericArrayType = GenericArrayTypeImpl(componentType)
40 |
41 | fun canonicalize(type: Type): Type {
42 | var result = type
43 | when (type) {
44 | is Class<*> -> {
45 | if (type.isArray) result = GenericArrayTypeImpl(canonicalize(type.componentType))
46 | }
47 | is ParameterizedType -> {
48 | if (type !is ParameterizedTypeImpl) result = ParameterizedTypeImpl(type.ownerType, type.rawType, type.actualTypeArguments)
49 | }
50 | is GenericArrayType -> {
51 | if (type !is GenericArrayTypeImpl) result = GenericArrayTypeImpl(type.genericComponentType)
52 | }
53 | is WildcardType -> {
54 | if (type !is WildcardTypeImpl) result = WildcardTypeImpl(type.upperBounds, type.lowerBounds)
55 | }
56 | }
57 | return result
58 | }
59 |
60 | private class ParameterizedTypeImpl(
61 | private var ownerType: Type?,
62 | private var rawType: Type,
63 | private var typeArguments: Array
64 | ) : ParameterizedType {
65 |
66 | init {
67 | if (rawType is Class<*>) {
68 | val isStaticOrTopLevelClass = Modifier.isStatic((rawType as Class<*>).modifiers) || (rawType as Class<*>).enclosingClass == null
69 | if (ownerType == null && !isStaticOrTopLevelClass) throw IllegalArgumentException()
70 | }
71 |
72 | this.ownerType = if (ownerType == null) null else canonicalize(this.ownerType!!)
73 | this.rawType = canonicalize(this.rawType)
74 | typeArguments = typeArguments.filter { it != null }.map { canonicalize(it!!) }.toTypedArray()
75 | }
76 |
77 | override fun getRawType(): Type = rawType
78 |
79 | override fun getOwnerType(): Type? = ownerType
80 |
81 | override fun getActualTypeArguments(): Array = typeArguments.clone()
82 | }
83 |
84 | private class GenericArrayTypeImpl(private var componentType: Type) : GenericArrayType {
85 |
86 | init {
87 | this.componentType = canonicalize(componentType)
88 | }
89 |
90 | override fun getGenericComponentType(): Type = componentType
91 | }
92 |
93 | /**
94 | * The WildcardType interface supports multiple upper bounds and multiple lower bounds. We only
95 | * support what the Java 6 language needs - at most one bound. If a lower bound is set, the upper
96 | * bound must be Object.class.
97 | */
98 | private class WildcardTypeImpl(upperBounds: Array, lowerBounds: Array) : WildcardType {
99 | private var upperBound: Type?
100 | private var lowerBound: Type?
101 |
102 | init {
103 | if (upperBounds.size != 1) throw IllegalArgumentException()
104 | if (lowerBounds.size > 1) throw IllegalArgumentException()
105 |
106 | if (lowerBounds.size == 1) {
107 | if (lowerBounds[0] == null) throw NullPointerException()
108 | if (upperBounds[0] !== Any::class.java) throw IllegalArgumentException()
109 | this.lowerBound = canonicalize(lowerBounds[0]!!)
110 | this.upperBound = Any::class.java
111 | } else {
112 | if (upperBounds[0] == null) throw NullPointerException()
113 | this.lowerBound = null
114 | this.upperBound = canonicalize(upperBounds[0]!!)
115 | }
116 | }
117 |
118 | override fun getLowerBounds(): Array = if (lowerBound != null) arrayOf(lowerBound!!) else EMPTY_TYPE_ARRAY
119 |
120 | override fun getUpperBounds(): Array = arrayOf(upperBound!!)
121 | }
122 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/epam/coroutinecache/utils/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache.utils
2 |
3 | fun Any.getClassName(): String = this::class.java.name
--------------------------------------------------------------------------------
/core/src/test/java/com/epam/coroutinecache/BaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache
2 |
3 | import com.epam.coroutinecache.core.Memory
4 | import com.epam.coroutinecache.core.Persistence
5 | import com.epam.coroutinecache.di.actionsModule
6 | import com.epam.coroutinecache.di.cacheModule
7 | import com.epam.coroutinecache.utils.JsonFactoryChooser
8 | import com.epam.coroutinecache.utils.MapperProvider
9 | import org.junit.After
10 | import org.junit.Before
11 | import org.junit.Rule
12 | import org.junit.rules.TemporaryFolder
13 | import org.koin.core.context.startKoin
14 | import org.koin.core.context.stopKoin
15 | import org.koin.core.parameter.parametersOf
16 | import org.koin.test.KoinTest
17 | import org.koin.test.get
18 |
19 | open class BaseTest : KoinTest {
20 |
21 | @Rule
22 | @JvmField
23 | val temporaryFolder = TemporaryFolder()
24 |
25 | private val mapperProvider = MapperProvider()
26 |
27 | lateinit var memory: Memory
28 |
29 | lateinit var diskCache: Persistence
30 |
31 | @Before
32 | fun beforeTest() {
33 | startKoin {
34 | modules(listOf(actionsModule, cacheModule))
35 | }
36 | memory = get()
37 | diskCache = get { parametersOf(temporaryFolder.root, mapperProvider.provideMapperByChooser(JsonFactoryChooser.MOSHI)) }
38 | }
39 |
40 | @After
41 | fun afterTest() {
42 | stopKoin()
43 | }
44 | }
--------------------------------------------------------------------------------
/core/src/test/java/com/epam/coroutinecache/DiskTest.kt:
--------------------------------------------------------------------------------
1 | package com.epam.coroutinecache
2 |
3 | import com.epam.coroutinecache.core.Record
4 | import com.epam.coroutinecache.utils.MockDataString
5 | import com.epam.coroutinecache.utils.Types
6 | import org.junit.Assert.assertArrayEquals
7 | import org.junit.Assert.assertEquals
8 | import org.junit.Assert.assertTrue
9 | import org.junit.Test
10 |
11 | class DiskTest : BaseTest() {
12 |
13 | @Test
14 | fun testRetrieveRecordAsObject() {
15 | diskCache.saveRecord(KEY, Record(MockDataString(VALUE_STRING)), MockDataString::class.java)
16 |
17 | val retrievedData = diskCache.getRecord(KEY, MockDataString::class.java)
18 | assertEquals(retrievedData?.getData()?.getMessage(), VALUE_STRING)
19 | }
20 |
21 | @Test
22 | fun testRetrieveRecordAsCollection() {
23 | val mocks = listOf(MockDataString(VALUE_STRING), MockDataString(VALUE_STRING + 1))
24 | val mockType = Types.newParameterizedType(List::class.java, MockDataString::class.java)
25 | diskCache.saveRecord(KEY, Record(mocks), mockType)
26 |
27 | val retrievedData = diskCache.getRecord>(KEY, mockType)
28 | for (i in 0 until mocks.size) {
29 | assertEquals(mocks[i], retrievedData?.getData()?.get(i))
30 | }
31 | }
32 |
33 | @Test
34 | fun testRetrieveRecordAsArray() {
35 | val mocks = arrayOf(MockDataString(VALUE_STRING), MockDataString(VALUE_STRING + 1))
36 | val mockType = Types.arrayOf(MockDataString::class.java)
37 | diskCache.saveRecord(KEY, Record(mocks), mockType)
38 |
39 | val retrievedData = diskCache.getRecord>(KEY, mockType)
40 | assertArrayEquals(mocks, retrievedData?.getData())
41 | }
42 |
43 | @Test
44 | fun testRetrieveRecordAsMap() {
45 | val testMap = HashMap()
46 | val testType = Types.newParameterizedType(Map::class.java, Int::class.javaObjectType, MockDataString::class.java)
47 | testMap[0] = MockDataString(VALUE_STRING)
48 | testMap[1] = MockDataString(VALUE_STRING + 1)
49 | diskCache.saveRecord(KEY, Record(testMap), testType)
50 |
51 | val retrievedData = diskCache.getRecord