Skip to content

Commit 7345ff5

Browse files
committed
Added hands-on lab
1 parent 56f4424 commit 7345ff5

26 files changed

+949
-0
lines changed

resources/ajax-loader.gif

673 Bytes
Loading

resources/logback.xml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<configuration>
2+
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
3+
<encoder>
4+
<pattern>%r [%thread] %-5level %logger{36} - %msg%n</pattern>
5+
</encoder>
6+
</appender>
7+
8+
<root level="debug">
9+
<appender-ref ref="STDOUT" />
10+
</root>
11+
</configuration>

src/contributors/Contributors.kt

+208
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
package contributors
2+
3+
import contributors.Contributors.LoadingStatus.*
4+
import contributors.Variant.*
5+
import kotlinx.coroutines.*
6+
import kotlinx.coroutines.swing.Swing
7+
import tasks.*
8+
import java.awt.event.ActionListener
9+
import javax.swing.SwingUtilities
10+
import kotlin.coroutines.CoroutineContext
11+
12+
enum class Variant {
13+
BLOCKING, // Request1Blocking
14+
BACKGROUND, // Request2Background
15+
CALLBACKS, // Request3Callbacks
16+
SUSPEND, // Request4Coroutine
17+
CONCURRENT, // Request5Concurrent
18+
NOT_CANCELLABLE, // Request6NotCancellable
19+
PROGRESS, // Request6Progress
20+
CHANNELS // Request7Channels
21+
}
22+
23+
interface Contributors: CoroutineScope {
24+
25+
val job: Job
26+
27+
override val coroutineContext: CoroutineContext
28+
get() = job + Dispatchers.Main
29+
30+
fun init() {
31+
// Start a new loading on 'load' click
32+
addLoadListener {
33+
saveParams()
34+
loadContributors()
35+
}
36+
37+
// Save preferences and exit on closing the window
38+
addOnWindowClosingListener {
39+
job.cancel()
40+
saveParams()
41+
System.exit(0)
42+
}
43+
44+
// Load stored params (user & password values)
45+
loadInitialParams()
46+
}
47+
48+
fun loadContributors() {
49+
val (username, password, org, _) = getParams()
50+
val req = RequestData(username, password, org)
51+
52+
clearResults()
53+
val service = createGitHubService(req.username, req.password)
54+
55+
val startTime = System.currentTimeMillis()
56+
when (getSelectedVariant()) {
57+
BLOCKING -> { // Blocking UI thread
58+
val users = loadContributorsBlocking(service, req)
59+
updateResults(users, startTime)
60+
}
61+
BACKGROUND -> { // Blocking a background thread
62+
loadContributorsBackground(service, req) { users ->
63+
SwingUtilities.invokeLater {
64+
updateResults(users, startTime)
65+
}
66+
}
67+
}
68+
CALLBACKS -> { // Using callbacks
69+
loadContributorsCallbacks(service, req) { users ->
70+
SwingUtilities.invokeLater {
71+
updateResults(users, startTime)
72+
}
73+
}
74+
}
75+
SUSPEND -> { // Using coroutines
76+
launch {
77+
val users = loadContributorsSuspend(service, req)
78+
updateResults(users, startTime)
79+
}.setUpCancellation()
80+
}
81+
CONCURRENT -> { // Performing requests concurrently
82+
launch(Dispatchers.Default) {
83+
val users = loadContributorsConcurrent(service, req)
84+
withContext(Dispatchers.Main) {
85+
updateResults(users, startTime)
86+
}
87+
}.setUpCancellation()
88+
}
89+
NOT_CANCELLABLE -> { // Performing requests in a non-cancellable way
90+
launch {
91+
val users = loadContributorsNotCancellable(service, req)
92+
updateResults(users, startTime)
93+
}.setUpCancellation()
94+
}
95+
PROGRESS -> { // Showing progress
96+
launch(Dispatchers.Default) {
97+
loadContributorsProgress(service, req) { users, completed ->
98+
withContext(Dispatchers.Main) {
99+
updateResults(users, startTime, completed)
100+
}
101+
}
102+
}.setUpCancellation()
103+
}
104+
CHANNELS -> { // Performing requests concurrently and showing progress
105+
launch(Dispatchers.Default) {
106+
loadContributorsChannels(service, req) { users, completed ->
107+
withContext(Dispatchers.Main) {
108+
updateResults(users, startTime, completed)
109+
}
110+
}
111+
}.setUpCancellation()
112+
}
113+
}
114+
}
115+
116+
private enum class LoadingStatus { COMPLETED, CANCELED, IN_PROGRESS }
117+
118+
private fun clearResults() {
119+
updateContributors(listOf())
120+
updateLoadingStatus(IN_PROGRESS)
121+
setActionsStatus(newLoadingEnabled = false)
122+
}
123+
124+
private fun updateResults(
125+
users: List<User>,
126+
startTime: Long,
127+
completed: Boolean = true
128+
) {
129+
updateContributors(users)
130+
updateLoadingStatus(if (completed) COMPLETED else IN_PROGRESS, startTime)
131+
if (completed) {
132+
setActionsStatus(newLoadingEnabled = true)
133+
}
134+
}
135+
136+
private fun updateLoadingStatus(
137+
status: LoadingStatus,
138+
startTime: Long? = null
139+
) {
140+
val time = if (startTime != null) {
141+
val time = System.currentTimeMillis() - startTime
142+
"${(time / 1000)}.${time % 1000 / 100} sec"
143+
} else ""
144+
145+
val text = "Loading status: " +
146+
when (status) {
147+
COMPLETED -> "completed in $time"
148+
IN_PROGRESS -> "in progress $time"
149+
CANCELED -> "canceled"
150+
}
151+
setLoadingStatus(text, status == IN_PROGRESS)
152+
}
153+
154+
private fun Job.setUpCancellation() {
155+
// make active the 'cancel' button
156+
setActionsStatus(newLoadingEnabled = false, cancellationEnabled = true)
157+
158+
val loadingJob = this
159+
160+
// cancel the loading job if the 'cancel' button was clicked
161+
val listener = ActionListener {
162+
loadingJob.cancel()
163+
updateLoadingStatus(CANCELED)
164+
}
165+
addCancelListener(listener)
166+
167+
// update the status and remove the listener after the loading job is completed
168+
launch {
169+
loadingJob.join()
170+
setActionsStatus(newLoadingEnabled = true)
171+
removeCancelListener(listener)
172+
}
173+
}
174+
175+
fun loadInitialParams() {
176+
setParams(loadStoredParams())
177+
}
178+
179+
fun saveParams() {
180+
val params = getParams()
181+
if (params.username.isEmpty() && params.password.isEmpty()) {
182+
removeStoredParams()
183+
}
184+
else {
185+
saveParams(params)
186+
}
187+
}
188+
189+
fun getSelectedVariant(): Variant
190+
191+
fun updateContributors(users: List<User>)
192+
193+
fun setLoadingStatus(text: String, iconRunning: Boolean)
194+
195+
fun setActionsStatus(newLoadingEnabled: Boolean, cancellationEnabled: Boolean = false)
196+
197+
fun addCancelListener(listener: ActionListener)
198+
199+
fun removeCancelListener(listener: ActionListener)
200+
201+
fun addLoadListener(listener: () -> Unit)
202+
203+
fun addOnWindowClosingListener(listener: () -> Unit)
204+
205+
fun setParams(params: Params)
206+
207+
fun getParams(): Params
208+
}

src/contributors/ContributorsUI.kt

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package contributors
2+
3+
import kotlinx.coroutines.Job
4+
import java.awt.Dimension
5+
import java.awt.GridBagConstraints
6+
import java.awt.GridBagLayout
7+
import java.awt.Insets
8+
import java.awt.event.ActionListener
9+
import java.awt.event.WindowAdapter
10+
import java.awt.event.WindowEvent
11+
import javax.swing.*
12+
import javax.swing.table.DefaultTableModel
13+
14+
private val INSETS = Insets(3, 10, 3, 10)
15+
private val COLUMNS = arrayOf("Login", "Contributions")
16+
17+
@Suppress("CONFLICTING_INHERITED_JVM_DECLARATIONS")
18+
class ContributorsUI : JFrame("GitHub Contributors"), Contributors {
19+
private val username = JTextField(20)
20+
private val password = JPasswordField(20)
21+
private val org = JTextField(20)
22+
private val variant = JComboBox<Variant>(Variant.values())
23+
private val load = JButton("Load contributors")
24+
private val cancel = JButton("Cancel").apply { isEnabled = false }
25+
26+
private val resultsModel = DefaultTableModel(COLUMNS, 0)
27+
private val results = JTable(resultsModel)
28+
private val resultsScroll = JScrollPane(results).apply {
29+
preferredSize = Dimension(200, 200)
30+
}
31+
32+
private val loadingIcon = ImageIcon(javaClass.classLoader.getResource("ajax-loader.gif"))
33+
private val loadingStatus = JLabel("Start new loading", loadingIcon, SwingConstants.CENTER)
34+
35+
override val job = Job()
36+
37+
init {
38+
// Create UI
39+
rootPane.contentPane = JPanel(GridBagLayout()).apply {
40+
addLabeled("GitHub Username", username)
41+
addLabeled("Password/Token", password)
42+
addWideSeparator()
43+
addLabeled("Organization", org)
44+
addLabeled("Variant", variant)
45+
addWideSeparator()
46+
addWide(JPanel().apply {
47+
add(load)
48+
add(cancel)
49+
})
50+
addWide(resultsScroll) {
51+
weightx = 1.0
52+
weighty = 1.0
53+
fill = GridBagConstraints.BOTH
54+
}
55+
addWide(loadingStatus)
56+
}
57+
// Initialize actions
58+
init()
59+
}
60+
61+
override fun getSelectedVariant(): Variant = variant.getItemAt(variant.selectedIndex)
62+
63+
override fun updateContributors(users: List<User>) {
64+
if (users.isNotEmpty()) {
65+
log.info("Updating result with ${users.size} rows")
66+
}
67+
else {
68+
log.info("Clearing result")
69+
}
70+
resultsModel.setDataVector(users.map {
71+
arrayOf(it.login, it.contributions)
72+
}.toTypedArray(), COLUMNS)
73+
}
74+
75+
override fun setLoadingStatus(text: String, iconRunning: Boolean) {
76+
loadingStatus.text = text
77+
loadingStatus.icon = if (iconRunning) loadingIcon else null
78+
}
79+
80+
override fun addCancelListener(listener: ActionListener) {
81+
cancel.addActionListener(listener)
82+
}
83+
84+
override fun removeCancelListener(listener: ActionListener) {
85+
cancel.removeActionListener(listener)
86+
}
87+
88+
override fun addLoadListener(listener: () -> Unit) {
89+
load.addActionListener { listener() }
90+
}
91+
92+
override fun addOnWindowClosingListener(listener: () -> Unit) {
93+
addWindowListener(object : WindowAdapter() {
94+
override fun windowClosing(e: WindowEvent?) {
95+
listener()
96+
}
97+
})
98+
}
99+
100+
override fun setActionsStatus(newLoadingEnabled: Boolean, cancellationEnabled: Boolean) {
101+
load.isEnabled = newLoadingEnabled
102+
cancel.isEnabled = cancellationEnabled
103+
}
104+
105+
override fun setParams(params: Params) {
106+
username.text = params.username
107+
password.text = params.password
108+
org.text = params.org
109+
variant.selectedIndex = params.variant.ordinal
110+
}
111+
112+
override fun getParams(): Params {
113+
return Params(username.text, password.password.joinToString(""), org.text, getSelectedVariant())
114+
}
115+
}
116+
117+
fun JPanel.addLabeled(label: String, component: JComponent) {
118+
add(JLabel(label), GridBagConstraints().apply {
119+
gridx = 0
120+
insets = INSETS
121+
})
122+
add(component, GridBagConstraints().apply {
123+
gridx = 1
124+
insets = INSETS
125+
anchor = GridBagConstraints.WEST
126+
fill = GridBagConstraints.HORIZONTAL
127+
weightx = 1.0
128+
})
129+
}
130+
131+
fun JPanel.addWide(component: JComponent, constraints: GridBagConstraints.() -> Unit = {}) {
132+
add(component, GridBagConstraints().apply {
133+
gridx = 0
134+
gridwidth = 2
135+
insets = INSETS
136+
constraints()
137+
})
138+
}
139+
140+
fun JPanel.addWideSeparator() {
141+
addWide(JSeparator()) {
142+
fill = GridBagConstraints.HORIZONTAL
143+
}
144+
}
145+
146+
fun setDefaultFontSize(size: Float) {
147+
for (key in UIManager.getLookAndFeelDefaults().keys.toTypedArray()) {
148+
if (key.toString().toLowerCase().contains("font")) {
149+
val font = UIManager.getDefaults().getFont(key) ?: continue
150+
val newFont = font.deriveFont(size)
151+
UIManager.put(key, newFont)
152+
}
153+
}
154+
}

0 commit comments

Comments
 (0)