Added hands-on lab
This commit is contained in:
BIN
resources/ajax-loader.gif
Normal file
BIN
resources/ajax-loader.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 673 B |
11
resources/logback.xml
Normal file
11
resources/logback.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%r [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="debug">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
208
src/contributors/Contributors.kt
Normal file
208
src/contributors/Contributors.kt
Normal file
@ -0,0 +1,208 @@
|
||||
package contributors
|
||||
|
||||
import contributors.Contributors.LoadingStatus.*
|
||||
import contributors.Variant.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import tasks.*
|
||||
import java.awt.event.ActionListener
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
enum class Variant {
|
||||
BLOCKING, // Request1Blocking
|
||||
BACKGROUND, // Request2Background
|
||||
CALLBACKS, // Request3Callbacks
|
||||
SUSPEND, // Request4Coroutine
|
||||
CONCURRENT, // Request5Concurrent
|
||||
NOT_CANCELLABLE, // Request6NotCancellable
|
||||
PROGRESS, // Request6Progress
|
||||
CHANNELS // Request7Channels
|
||||
}
|
||||
|
||||
interface Contributors: CoroutineScope {
|
||||
|
||||
val job: Job
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
fun init() {
|
||||
// Start a new loading on 'load' click
|
||||
addLoadListener {
|
||||
saveParams()
|
||||
loadContributors()
|
||||
}
|
||||
|
||||
// Save preferences and exit on closing the window
|
||||
addOnWindowClosingListener {
|
||||
job.cancel()
|
||||
saveParams()
|
||||
System.exit(0)
|
||||
}
|
||||
|
||||
// Load stored params (user & password values)
|
||||
loadInitialParams()
|
||||
}
|
||||
|
||||
fun loadContributors() {
|
||||
val (username, password, org, _) = getParams()
|
||||
val req = RequestData(username, password, org)
|
||||
|
||||
clearResults()
|
||||
val service = createGitHubService(req.username, req.password)
|
||||
|
||||
val startTime = System.currentTimeMillis()
|
||||
when (getSelectedVariant()) {
|
||||
BLOCKING -> { // Blocking UI thread
|
||||
val users = loadContributorsBlocking(service, req)
|
||||
updateResults(users, startTime)
|
||||
}
|
||||
BACKGROUND -> { // Blocking a background thread
|
||||
loadContributorsBackground(service, req) { users ->
|
||||
SwingUtilities.invokeLater {
|
||||
updateResults(users, startTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
CALLBACKS -> { // Using callbacks
|
||||
loadContributorsCallbacks(service, req) { users ->
|
||||
SwingUtilities.invokeLater {
|
||||
updateResults(users, startTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
SUSPEND -> { // Using coroutines
|
||||
launch {
|
||||
val users = loadContributorsSuspend(service, req)
|
||||
updateResults(users, startTime)
|
||||
}.setUpCancellation()
|
||||
}
|
||||
CONCURRENT -> { // Performing requests concurrently
|
||||
launch(Dispatchers.Default) {
|
||||
val users = loadContributorsConcurrent(service, req)
|
||||
withContext(Dispatchers.Main) {
|
||||
updateResults(users, startTime)
|
||||
}
|
||||
}.setUpCancellation()
|
||||
}
|
||||
NOT_CANCELLABLE -> { // Performing requests in a non-cancellable way
|
||||
launch {
|
||||
val users = loadContributorsNotCancellable(service, req)
|
||||
updateResults(users, startTime)
|
||||
}.setUpCancellation()
|
||||
}
|
||||
PROGRESS -> { // Showing progress
|
||||
launch(Dispatchers.Default) {
|
||||
loadContributorsProgress(service, req) { users, completed ->
|
||||
withContext(Dispatchers.Main) {
|
||||
updateResults(users, startTime, completed)
|
||||
}
|
||||
}
|
||||
}.setUpCancellation()
|
||||
}
|
||||
CHANNELS -> { // Performing requests concurrently and showing progress
|
||||
launch(Dispatchers.Default) {
|
||||
loadContributorsChannels(service, req) { users, completed ->
|
||||
withContext(Dispatchers.Main) {
|
||||
updateResults(users, startTime, completed)
|
||||
}
|
||||
}
|
||||
}.setUpCancellation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class LoadingStatus { COMPLETED, CANCELED, IN_PROGRESS }
|
||||
|
||||
private fun clearResults() {
|
||||
updateContributors(listOf())
|
||||
updateLoadingStatus(IN_PROGRESS)
|
||||
setActionsStatus(newLoadingEnabled = false)
|
||||
}
|
||||
|
||||
private fun updateResults(
|
||||
users: List<User>,
|
||||
startTime: Long,
|
||||
completed: Boolean = true
|
||||
) {
|
||||
updateContributors(users)
|
||||
updateLoadingStatus(if (completed) COMPLETED else IN_PROGRESS, startTime)
|
||||
if (completed) {
|
||||
setActionsStatus(newLoadingEnabled = true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLoadingStatus(
|
||||
status: LoadingStatus,
|
||||
startTime: Long? = null
|
||||
) {
|
||||
val time = if (startTime != null) {
|
||||
val time = System.currentTimeMillis() - startTime
|
||||
"${(time / 1000)}.${time % 1000 / 100} sec"
|
||||
} else ""
|
||||
|
||||
val text = "Loading status: " +
|
||||
when (status) {
|
||||
COMPLETED -> "completed in $time"
|
||||
IN_PROGRESS -> "in progress $time"
|
||||
CANCELED -> "canceled"
|
||||
}
|
||||
setLoadingStatus(text, status == IN_PROGRESS)
|
||||
}
|
||||
|
||||
private fun Job.setUpCancellation() {
|
||||
// make active the 'cancel' button
|
||||
setActionsStatus(newLoadingEnabled = false, cancellationEnabled = true)
|
||||
|
||||
val loadingJob = this
|
||||
|
||||
// cancel the loading job if the 'cancel' button was clicked
|
||||
val listener = ActionListener {
|
||||
loadingJob.cancel()
|
||||
updateLoadingStatus(CANCELED)
|
||||
}
|
||||
addCancelListener(listener)
|
||||
|
||||
// update the status and remove the listener after the loading job is completed
|
||||
launch {
|
||||
loadingJob.join()
|
||||
setActionsStatus(newLoadingEnabled = true)
|
||||
removeCancelListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadInitialParams() {
|
||||
setParams(loadStoredParams())
|
||||
}
|
||||
|
||||
fun saveParams() {
|
||||
val params = getParams()
|
||||
if (params.username.isEmpty() && params.password.isEmpty()) {
|
||||
removeStoredParams()
|
||||
}
|
||||
else {
|
||||
saveParams(params)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSelectedVariant(): Variant
|
||||
|
||||
fun updateContributors(users: List<User>)
|
||||
|
||||
fun setLoadingStatus(text: String, iconRunning: Boolean)
|
||||
|
||||
fun setActionsStatus(newLoadingEnabled: Boolean, cancellationEnabled: Boolean = false)
|
||||
|
||||
fun addCancelListener(listener: ActionListener)
|
||||
|
||||
fun removeCancelListener(listener: ActionListener)
|
||||
|
||||
fun addLoadListener(listener: () -> Unit)
|
||||
|
||||
fun addOnWindowClosingListener(listener: () -> Unit)
|
||||
|
||||
fun setParams(params: Params)
|
||||
|
||||
fun getParams(): Params
|
||||
}
|
154
src/contributors/ContributorsUI.kt
Normal file
154
src/contributors/ContributorsUI.kt
Normal file
@ -0,0 +1,154 @@
|
||||
package contributors
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
import java.awt.Dimension
|
||||
import java.awt.GridBagConstraints
|
||||
import java.awt.GridBagLayout
|
||||
import java.awt.Insets
|
||||
import java.awt.event.ActionListener
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import javax.swing.*
|
||||
import javax.swing.table.DefaultTableModel
|
||||
|
||||
private val INSETS = Insets(3, 10, 3, 10)
|
||||
private val COLUMNS = arrayOf("Login", "Contributions")
|
||||
|
||||
@Suppress("CONFLICTING_INHERITED_JVM_DECLARATIONS")
|
||||
class ContributorsUI : JFrame("GitHub Contributors"), Contributors {
|
||||
private val username = JTextField(20)
|
||||
private val password = JPasswordField(20)
|
||||
private val org = JTextField(20)
|
||||
private val variant = JComboBox<Variant>(Variant.values())
|
||||
private val load = JButton("Load contributors")
|
||||
private val cancel = JButton("Cancel").apply { isEnabled = false }
|
||||
|
||||
private val resultsModel = DefaultTableModel(COLUMNS, 0)
|
||||
private val results = JTable(resultsModel)
|
||||
private val resultsScroll = JScrollPane(results).apply {
|
||||
preferredSize = Dimension(200, 200)
|
||||
}
|
||||
|
||||
private val loadingIcon = ImageIcon(javaClass.classLoader.getResource("ajax-loader.gif"))
|
||||
private val loadingStatus = JLabel("Start new loading", loadingIcon, SwingConstants.CENTER)
|
||||
|
||||
override val job = Job()
|
||||
|
||||
init {
|
||||
// Create UI
|
||||
rootPane.contentPane = JPanel(GridBagLayout()).apply {
|
||||
addLabeled("GitHub Username", username)
|
||||
addLabeled("Password/Token", password)
|
||||
addWideSeparator()
|
||||
addLabeled("Organization", org)
|
||||
addLabeled("Variant", variant)
|
||||
addWideSeparator()
|
||||
addWide(JPanel().apply {
|
||||
add(load)
|
||||
add(cancel)
|
||||
})
|
||||
addWide(resultsScroll) {
|
||||
weightx = 1.0
|
||||
weighty = 1.0
|
||||
fill = GridBagConstraints.BOTH
|
||||
}
|
||||
addWide(loadingStatus)
|
||||
}
|
||||
// Initialize actions
|
||||
init()
|
||||
}
|
||||
|
||||
override fun getSelectedVariant(): Variant = variant.getItemAt(variant.selectedIndex)
|
||||
|
||||
override fun updateContributors(users: List<User>) {
|
||||
if (users.isNotEmpty()) {
|
||||
log.info("Updating result with ${users.size} rows")
|
||||
}
|
||||
else {
|
||||
log.info("Clearing result")
|
||||
}
|
||||
resultsModel.setDataVector(users.map {
|
||||
arrayOf(it.login, it.contributions)
|
||||
}.toTypedArray(), COLUMNS)
|
||||
}
|
||||
|
||||
override fun setLoadingStatus(text: String, iconRunning: Boolean) {
|
||||
loadingStatus.text = text
|
||||
loadingStatus.icon = if (iconRunning) loadingIcon else null
|
||||
}
|
||||
|
||||
override fun addCancelListener(listener: ActionListener) {
|
||||
cancel.addActionListener(listener)
|
||||
}
|
||||
|
||||
override fun removeCancelListener(listener: ActionListener) {
|
||||
cancel.removeActionListener(listener)
|
||||
}
|
||||
|
||||
override fun addLoadListener(listener: () -> Unit) {
|
||||
load.addActionListener { listener() }
|
||||
}
|
||||
|
||||
override fun addOnWindowClosingListener(listener: () -> Unit) {
|
||||
addWindowListener(object : WindowAdapter() {
|
||||
override fun windowClosing(e: WindowEvent?) {
|
||||
listener()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun setActionsStatus(newLoadingEnabled: Boolean, cancellationEnabled: Boolean) {
|
||||
load.isEnabled = newLoadingEnabled
|
||||
cancel.isEnabled = cancellationEnabled
|
||||
}
|
||||
|
||||
override fun setParams(params: Params) {
|
||||
username.text = params.username
|
||||
password.text = params.password
|
||||
org.text = params.org
|
||||
variant.selectedIndex = params.variant.ordinal
|
||||
}
|
||||
|
||||
override fun getParams(): Params {
|
||||
return Params(username.text, password.password.joinToString(""), org.text, getSelectedVariant())
|
||||
}
|
||||
}
|
||||
|
||||
fun JPanel.addLabeled(label: String, component: JComponent) {
|
||||
add(JLabel(label), GridBagConstraints().apply {
|
||||
gridx = 0
|
||||
insets = INSETS
|
||||
})
|
||||
add(component, GridBagConstraints().apply {
|
||||
gridx = 1
|
||||
insets = INSETS
|
||||
anchor = GridBagConstraints.WEST
|
||||
fill = GridBagConstraints.HORIZONTAL
|
||||
weightx = 1.0
|
||||
})
|
||||
}
|
||||
|
||||
fun JPanel.addWide(component: JComponent, constraints: GridBagConstraints.() -> Unit = {}) {
|
||||
add(component, GridBagConstraints().apply {
|
||||
gridx = 0
|
||||
gridwidth = 2
|
||||
insets = INSETS
|
||||
constraints()
|
||||
})
|
||||
}
|
||||
|
||||
fun JPanel.addWideSeparator() {
|
||||
addWide(JSeparator()) {
|
||||
fill = GridBagConstraints.HORIZONTAL
|
||||
}
|
||||
}
|
||||
|
||||
fun setDefaultFontSize(size: Float) {
|
||||
for (key in UIManager.getLookAndFeelDefaults().keys.toTypedArray()) {
|
||||
if (key.toString().toLowerCase().contains("font")) {
|
||||
val font = UIManager.getDefaults().getFont(key) ?: continue
|
||||
val newFont = font.deriveFont(size)
|
||||
UIManager.put(key, newFont)
|
||||
}
|
||||
}
|
||||
}
|
92
src/contributors/GitHubService.kt
Normal file
92
src/contributors/GitHubService.kt
Normal file
@ -0,0 +1,92 @@
|
||||
package contributors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.Observable
|
||||
//import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Call
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
|
||||
import retrofit2.converter.jackson.JacksonConverterFactory
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import java.util.*
|
||||
|
||||
interface GitHubService {
|
||||
@GET("orgs/{org}/repos?per_page=100")
|
||||
fun getOrgReposCall(
|
||||
@Path("org") org: String
|
||||
): Call<List<Repo>>
|
||||
|
||||
@GET("repos/{owner}/{repo}/contributors?per_page=100")
|
||||
fun getRepoContributorsCall(
|
||||
@Path("owner") owner: String,
|
||||
@Path("repo") repo: String
|
||||
): Call<List<User>>
|
||||
|
||||
@GET("orgs/{org}/repos?per_page=100")
|
||||
suspend fun getOrgRepos(
|
||||
@Path("org") org: String
|
||||
): Response<List<Repo>>
|
||||
|
||||
@GET("repos/{owner}/{repo}/contributors?per_page=100")
|
||||
suspend fun getRepoContributors(
|
||||
@Path("owner") owner: String,
|
||||
@Path("repo") repo: String
|
||||
): Response<List<User>>
|
||||
|
||||
@GET("orgs/{org}/repos?per_page=100")
|
||||
fun getOrgReposRx(
|
||||
@Path("org") org: String
|
||||
): Observable<Response<List<Repo>>>
|
||||
|
||||
@GET("repos/{owner}/{repo}/contributors?per_page=100")
|
||||
fun getRepoContributorsRx(
|
||||
@Path("owner") owner: String,
|
||||
@Path("repo") repo: String
|
||||
): Observable<Response<List<User>>>
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class Repo(
|
||||
val id: Long,
|
||||
val name: String
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class User(
|
||||
val login: String,
|
||||
val contributions: Int
|
||||
)
|
||||
|
||||
data class RequestData(
|
||||
val username: String,
|
||||
val password: String,
|
||||
val org: String
|
||||
)
|
||||
|
||||
fun createGitHubService(username: String, password: String): GitHubService {
|
||||
val authToken = "Basic " + Base64.getEncoder().encode("$username:$password".toByteArray()).toString(Charsets.UTF_8)
|
||||
val httpClient = OkHttpClient.Builder()
|
||||
.addInterceptor { chain ->
|
||||
val original = chain.request()
|
||||
val builder = original.newBuilder()
|
||||
.header("Accept", "application/vnd.github.v3+json")
|
||||
.header("Authorization", authToken)
|
||||
val request = builder.build()
|
||||
chain.proceed(request)
|
||||
}
|
||||
.build()
|
||||
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl("https://api.github.com")
|
||||
.addConverterFactory(JacksonConverterFactory.create(jacksonObjectMapper()))
|
||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
||||
.client(httpClient)
|
||||
.build()
|
||||
return retrofit.create(GitHubService::class.java)
|
||||
}
|
39
src/contributors/Logger.kt
Normal file
39
src/contributors/Logger.kt
Normal file
@ -0,0 +1,39 @@
|
||||
package contributors
|
||||
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import retrofit2.Response
|
||||
|
||||
val log: Logger = LoggerFactory.getLogger("Contributors")
|
||||
|
||||
/*fun logRepos(req: RequestData, repos: List<Repo>) {
|
||||
log.info("${req.org}: loaded ${repos.size} repos")
|
||||
}
|
||||
|
||||
fun logUsers(repo: Repo, users: List<User>) {
|
||||
log.info("${repo.name}: loaded ${users.size} contributors")
|
||||
}*/
|
||||
|
||||
fun log(msg: String?) {
|
||||
log.info(msg)
|
||||
}
|
||||
|
||||
fun logRepos(req: RequestData, response: Response<List<Repo>>) {
|
||||
val repos = response.body()
|
||||
if (!response.isSuccessful || repos == null) {
|
||||
log.error("Failed loading repos for ${req.org} with response: '${response.code()}: ${response.message()}'")
|
||||
}
|
||||
else {
|
||||
log.info("${req.org}: loaded ${repos.size} repos")
|
||||
}
|
||||
}
|
||||
|
||||
fun logUsers(repo: Repo, response: Response<List<User>>) {
|
||||
val users = response.body()
|
||||
if (!response.isSuccessful || users == null) {
|
||||
log.error("Failed loading contributors for ${repo.name} with response '${response.code()}: ${response.message()}'")
|
||||
}
|
||||
else {
|
||||
log.info("${repo.name}: loaded ${users.size} contributors")
|
||||
}
|
||||
}
|
32
src/contributors/Params.kt
Normal file
32
src/contributors/Params.kt
Normal file
@ -0,0 +1,32 @@
|
||||
package contributors
|
||||
|
||||
import java.util.prefs.Preferences
|
||||
|
||||
private fun prefNode(): Preferences = Preferences.userRoot().node("ContributorsUI")
|
||||
|
||||
data class Params(val username: String, val password: String, val org: String, val variant: Variant)
|
||||
|
||||
fun loadStoredParams(): Params {
|
||||
return prefNode().run {
|
||||
Params(
|
||||
get("username", ""),
|
||||
get("password", ""),
|
||||
get("org", "kotlin"),
|
||||
Variant.valueOf(get("variant", Variant.BLOCKING.name))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeStoredParams() {
|
||||
prefNode().removeNode()
|
||||
}
|
||||
|
||||
fun saveParams(params: Params) {
|
||||
prefNode().apply {
|
||||
put("username", params.username)
|
||||
put("password", params.password)
|
||||
put("org", params.org)
|
||||
put("variant", params.variant.name)
|
||||
sync()
|
||||
}
|
||||
}
|
10
src/contributors/main.kt
Normal file
10
src/contributors/main.kt
Normal file
@ -0,0 +1,10 @@
|
||||
package contributors
|
||||
|
||||
fun main() {
|
||||
setDefaultFontSize(18f)
|
||||
ContributorsUI().apply {
|
||||
pack()
|
||||
setLocationRelativeTo(null)
|
||||
isVisible = true
|
||||
}
|
||||
}
|
12
src/tasks/Aggregation.kt
Normal file
12
src/tasks/Aggregation.kt
Normal file
@ -0,0 +1,12 @@
|
||||
package tasks
|
||||
|
||||
import contributors.User
|
||||
|
||||
// TODO: Write aggregation code.
|
||||
// In the initial list each user is present several times, once for each
|
||||
// repository he or she contributed to.
|
||||
// Merge duplications: each user should be present only once in the resulting list
|
||||
// with the total value of contributions for all the repositories.
|
||||
// Users should be sorted in a descending order by their contributions.
|
||||
fun List<User>.aggregate(): List<User> =
|
||||
this
|
24
src/tasks/Request1Blocking.kt
Normal file
24
src/tasks/Request1Blocking.kt
Normal file
@ -0,0 +1,24 @@
|
||||
package tasks
|
||||
|
||||
import contributors.*
|
||||
import retrofit2.Response
|
||||
|
||||
fun loadContributorsBlocking(service: GitHubService, req: RequestData) : List<User> {
|
||||
val repos = service
|
||||
.getOrgReposCall(req.org)
|
||||
.execute() // Executes request and blocks the current thread
|
||||
.also { logRepos(req, it) }
|
||||
.body() ?: listOf()
|
||||
|
||||
return repos.flatMap { repo ->
|
||||
service
|
||||
.getRepoContributorsCall(req.org, repo.name)
|
||||
.execute() // Executes request and blocks the current thread
|
||||
.also { logUsers(repo, it) }
|
||||
.bodyList()
|
||||
}.aggregate()
|
||||
}
|
||||
|
||||
fun <T> Response<List<T>>.bodyList(): List<T> {
|
||||
return body() ?: listOf()
|
||||
}
|
12
src/tasks/Request2Background.kt
Normal file
12
src/tasks/Request2Background.kt
Normal file
@ -0,0 +1,12 @@
|
||||
package tasks
|
||||
|
||||
import contributors.GitHubService
|
||||
import contributors.RequestData
|
||||
import contributors.User
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
fun loadContributorsBackground(service: GitHubService, req: RequestData, updateResults: (List<User>) -> Unit) {
|
||||
thread {
|
||||
loadContributorsBlocking(service, req)
|
||||
}
|
||||
}
|
37
src/tasks/Request3Callbacks.kt
Normal file
37
src/tasks/Request3Callbacks.kt
Normal file
@ -0,0 +1,37 @@
|
||||
package tasks
|
||||
|
||||
import contributors.*
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
fun loadContributorsCallbacks(service: GitHubService, req: RequestData, updateResults: (List<User>) -> Unit) {
|
||||
service.getOrgReposCall(req.org).onResponse { responseRepos ->
|
||||
logRepos(req, responseRepos)
|
||||
val repos = responseRepos.bodyList()
|
||||
val allUsers = mutableListOf<User>()
|
||||
for (repo in repos) {
|
||||
service.getRepoContributorsCall(req.org, repo.name).onResponse { responseUsers ->
|
||||
logUsers(repo, responseUsers)
|
||||
val users = responseUsers.bodyList()
|
||||
allUsers += users
|
||||
}
|
||||
}
|
||||
// TODO: Why this code doesn't work? How to fix that?
|
||||
updateResults(allUsers.aggregate())
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> Call<T>.onResponse(crossinline callback: (Response<T>) -> Unit) {
|
||||
enqueue(object : Callback<T> {
|
||||
override fun onResponse(call: Call<T>, response: Response<T>) {
|
||||
callback(response)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<T>, t: Throwable) {
|
||||
log.error("Call failed", t)
|
||||
}
|
||||
})
|
||||
}
|
7
src/tasks/Request4Suspend.kt
Normal file
7
src/tasks/Request4Suspend.kt
Normal file
@ -0,0 +1,7 @@
|
||||
package tasks
|
||||
|
||||
import contributors.*
|
||||
|
||||
suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
|
||||
TODO()
|
||||
}
|
8
src/tasks/Request5Concurrent.kt
Normal file
8
src/tasks/Request5Concurrent.kt
Normal file
@ -0,0 +1,8 @@
|
||||
package tasks
|
||||
|
||||
import contributors.*
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun loadContributorsConcurrent(service: GitHubService, req: RequestData): List<User> = coroutineScope {
|
||||
TODO()
|
||||
}
|
9
src/tasks/Request5NotCancellable.kt
Normal file
9
src/tasks/Request5NotCancellable.kt
Normal file
@ -0,0 +1,9 @@
|
||||
package tasks
|
||||
|
||||
import contributors.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
suspend fun loadContributorsNotCancellable(service: GitHubService, req: RequestData): List<User> {
|
||||
TODO()
|
||||
}
|
11
src/tasks/Request6Progress.kt
Normal file
11
src/tasks/Request6Progress.kt
Normal file
@ -0,0 +1,11 @@
|
||||
package tasks
|
||||
|
||||
import contributors.*
|
||||
|
||||
suspend fun loadContributorsProgress(
|
||||
service: GitHubService,
|
||||
req: RequestData,
|
||||
updateResults: suspend (List<User>, completed: Boolean) -> Unit
|
||||
) {
|
||||
TODO()
|
||||
}
|
16
src/tasks/Request7Channels.kt
Normal file
16
src/tasks/Request7Channels.kt
Normal file
@ -0,0 +1,16 @@
|
||||
package tasks
|
||||
|
||||
import contributors.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
suspend fun loadContributorsChannels(
|
||||
service: GitHubService,
|
||||
req: RequestData,
|
||||
updateResults: suspend (List<User>, completed: Boolean) -> Unit
|
||||
) {
|
||||
coroutineScope {
|
||||
TODO()
|
||||
}
|
||||
}
|
36
test/contributors/MockGithubService.kt
Normal file
36
test/contributors/MockGithubService.kt
Normal file
@ -0,0 +1,36 @@
|
||||
package contributors
|
||||
|
||||
import io.reactivex.Observable
|
||||
import kotlinx.coroutines.delay
|
||||
import retrofit2.Call
|
||||
import retrofit2.Response
|
||||
import retrofit2.mock.Calls
|
||||
|
||||
object MockGithubService : GitHubService {
|
||||
override fun getOrgReposCall(org: String): Call<List<Repo>> {
|
||||
return Calls.response(repos)
|
||||
}
|
||||
|
||||
override fun getRepoContributorsCall(owner: String, repo: String): Call<List<User>> {
|
||||
return Calls.response(reposMap.getValue(repo).users)
|
||||
}
|
||||
|
||||
override suspend fun getOrgRepos(org: String): Response<List<Repo>> {
|
||||
delay(getReposDelay)
|
||||
return Response.success(repos)
|
||||
}
|
||||
|
||||
override suspend fun getRepoContributors(owner: String, repo: String): Response<List<User>> {
|
||||
val testRepo = reposMap.getValue(repo)
|
||||
delay(testRepo.delay)
|
||||
return Response.success(testRepo.users)
|
||||
}
|
||||
|
||||
override fun getOrgReposRx(org: String): Observable<Response<List<Repo>>> {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override fun getRepoContributorsRx(owner: String, repo: String): Observable<Response<List<User>>> {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
}
|
73
test/contributors/testData.kt
Normal file
73
test/contributors/testData.kt
Normal file
@ -0,0 +1,73 @@
|
||||
package contributors
|
||||
|
||||
val testRequestData = RequestData("username", "password", "org")
|
||||
|
||||
data class TestRepo(val name: String, val delay: Long, val users: List<User>)
|
||||
|
||||
data class TestResults(val timeFromStart: Long, val users: List<User>)
|
||||
|
||||
val testRepos = listOf(
|
||||
TestRepo(
|
||||
"repo-1", 1000, listOf(
|
||||
User("user-1", 10),
|
||||
User("user-2", 20)
|
||||
)
|
||||
),
|
||||
TestRepo(
|
||||
"repo-2", 1200, listOf(
|
||||
User("user-2", 30),
|
||||
User("user-1", 40)
|
||||
)
|
||||
),
|
||||
TestRepo(
|
||||
"repo-3", 800, listOf(
|
||||
User("user-2", 50),
|
||||
User("user-3", 60)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
const val getReposDelay = 1000L
|
||||
|
||||
val repos = testRepos.mapIndexed { index, testRepo -> Repo(index.toLong(), testRepo.name) }
|
||||
|
||||
val reposMap = testRepos.associate { it.name to it }
|
||||
|
||||
val expectedResults = TestResults(
|
||||
4000,
|
||||
listOf(
|
||||
User("user-2", 100),
|
||||
User("user-3", 60),
|
||||
User("user-1", 50)
|
||||
)
|
||||
)
|
||||
|
||||
val expectedConcurrentResults = TestResults(
|
||||
2200,
|
||||
expectedResults.users
|
||||
)
|
||||
|
||||
val progressResults = listOf(
|
||||
TestResults(
|
||||
2000,
|
||||
listOf(User(login = "user-2", contributions = 20), User(login = "user-1", contributions = 10))
|
||||
),
|
||||
TestResults(
|
||||
3200,
|
||||
listOf(User(login = "user-2", contributions = 50), User(login = "user-1", contributions = 50))
|
||||
),
|
||||
expectedResults
|
||||
)
|
||||
|
||||
val concurrentProgressResults = listOf(
|
||||
TestResults(
|
||||
1800,
|
||||
listOf(User(login = "user-3", contributions = 60), User(login = "user-2", contributions = 50))
|
||||
),
|
||||
TestResults(
|
||||
2000,
|
||||
listOf(User(login = "user-2", contributions = 70), User(login = "user-3", contributions = 60),
|
||||
User(login = "user-1", contributions = 10))
|
||||
),
|
||||
expectedConcurrentResults
|
||||
)
|
22
test/tasks/AggregationKtTest.kt
Normal file
22
test/tasks/AggregationKtTest.kt
Normal file
@ -0,0 +1,22 @@
|
||||
package tasks
|
||||
|
||||
import contributors.User
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class AggregationKtTest {
|
||||
@Test
|
||||
fun testAggregation() {
|
||||
val actual = listOf(
|
||||
User("Alice", 1), User("Bob", 3),
|
||||
User("Alice", 2), User("Bob", 7),
|
||||
User("Charlie", 3), User("Alice", 5)
|
||||
).aggregate()
|
||||
val expected = listOf(
|
||||
User("Bob", 10),
|
||||
User("Alice", 8),
|
||||
User("Charlie", 3)
|
||||
)
|
||||
Assert.assertEquals("Wrong result for 'aggregation'", expected, actual)
|
||||
}
|
||||
}
|
17
test/tasks/Request1BlockingKtTest.kt
Normal file
17
test/tasks/Request1BlockingKtTest.kt
Normal file
@ -0,0 +1,17 @@
|
||||
package tasks
|
||||
|
||||
import contributors.MockGithubService
|
||||
import contributors.expectedResults
|
||||
import contributors.testRequestData
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class Request1BlockingKtTest {
|
||||
@Test
|
||||
fun testAggregation() {
|
||||
val users = loadContributorsBlocking(MockGithubService, testRequestData)
|
||||
Assert.assertEquals("List of contributors should be sorted" +
|
||||
"by the number of contributions in a descending order",
|
||||
expectedResults.users, users)
|
||||
}
|
||||
}
|
17
test/tasks/Request3CallbacksKtTest.kt
Normal file
17
test/tasks/Request3CallbacksKtTest.kt
Normal file
@ -0,0 +1,17 @@
|
||||
package tasks
|
||||
|
||||
import contributors.*
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class Request3CallbacksKtTest {
|
||||
@Test
|
||||
fun testDataIsLoaded() {
|
||||
loadContributorsCallbacks(MockGithubService, testRequestData) {
|
||||
Assert.assertEquals(
|
||||
"Wrong result for 'loadContributorsCallbacks'",
|
||||
expectedResults.users, it
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
26
test/tasks/Request4SuspendKtTest.kt
Normal file
26
test/tasks/Request4SuspendKtTest.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package tasks
|
||||
|
||||
import contributors.MockGithubService
|
||||
import contributors.expectedConcurrentResults
|
||||
import contributors.expectedResults
|
||||
import contributors.testRequestData
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
class Request4SuspendKtTest {
|
||||
@Test
|
||||
fun testSuspend() = runBlockingTest {
|
||||
val startTime = currentTime
|
||||
val result = loadContributorsSuspend(MockGithubService, testRequestData)
|
||||
Assert.assertEquals("Wrong result for 'loadContributorsSuspend'", expectedResults.users, result)
|
||||
val virtualTime = currentTime - startTime
|
||||
Assert.assertEquals(
|
||||
"The calls run consequently, so the total virtual time should be 4000 ms: " +
|
||||
"1000 for repos request plus (1000 + 1200 + 800) = 3000 for contributors sequential requests)",
|
||||
expectedResults.timeFromStart, virtualTime
|
||||
)
|
||||
}
|
||||
}
|
26
test/tasks/Request5ConcurrentKtTest.kt
Normal file
26
test/tasks/Request5ConcurrentKtTest.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package tasks
|
||||
|
||||
import contributors.MockGithubService
|
||||
import contributors.expectedConcurrentResults
|
||||
import contributors.expectedResults
|
||||
import contributors.testRequestData
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
class Request5ConcurrentKtTest {
|
||||
@Test
|
||||
fun testConcurrent() = runBlockingTest {
|
||||
val startTime = currentTime
|
||||
val result = loadContributorsConcurrent(MockGithubService, testRequestData)
|
||||
Assert.assertEquals("Wrong result for 'loadContributorsConcurrent'", expectedConcurrentResults.users, result)
|
||||
val virtualTime = currentTime - startTime
|
||||
Assert.assertEquals(
|
||||
"The calls run concurrently, so the total virtual time should be 2200 ms: " +
|
||||
"1000 for repos request plus max(1000, 1200, 800) = 1200 for contributors concurrent requests)",
|
||||
expectedConcurrentResults.timeFromStart, virtualTime
|
||||
)
|
||||
}
|
||||
}
|
24
test/tasks/Request6ProgressKtTest.kt
Normal file
24
test/tasks/Request6ProgressKtTest.kt
Normal file
@ -0,0 +1,24 @@
|
||||
package tasks
|
||||
|
||||
import contributors.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
class Request6ProgressKtTest {
|
||||
@Test
|
||||
fun testProgress() = runBlockingTest {
|
||||
val startTime = currentTime
|
||||
var index = 0
|
||||
loadContributorsProgress(MockGithubService, testRequestData) {
|
||||
users, _ ->
|
||||
val expected = progressResults[index++]
|
||||
val virtualTime = currentTime - startTime
|
||||
Assert.assertEquals("Expected intermediate results after virtual ${expected.timeFromStart} ms:",
|
||||
expected.timeFromStart, virtualTime)
|
||||
Assert.assertEquals("Wrong progress result after $virtualTime:", expected.users, users)
|
||||
}
|
||||
}
|
||||
}
|
26
test/tasks/Request7ChannelsKtTest.kt
Normal file
26
test/tasks/Request7ChannelsKtTest.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package tasks
|
||||
|
||||
import contributors.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
class Request7ChannelsKtTest {
|
||||
@Test
|
||||
fun testChannels() = runBlockingTest {
|
||||
val startTime = currentTime
|
||||
var index = 0
|
||||
loadContributorsChannels(MockGithubService, testRequestData) {
|
||||
users, _ ->
|
||||
val expected = concurrentProgressResults[index++]
|
||||
val virtualTime = currentTime - startTime
|
||||
Assert.assertEquals("Expected intermediate results after virtual ${expected.timeFromStart} ms:",
|
||||
expected.timeFromStart, virtualTime)
|
||||
Assert.assertEquals("Wrong progress result after $virtualTime:", expected.users, users)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user