Skip to content
This repository was archived by the owner on Aug 18, 2020. It is now read-only.

Commit 1e087c8

Browse files
authored
Merge pull request #2 from codeoverflow-org/master
merge
2 parents b485e15 + 4771db6 commit 1e087c8

File tree

5 files changed

+255
-53
lines changed

5 files changed

+255
-53
lines changed

bootstrap/build.sbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ version := "0.1"
33
assemblyJarName in assembly := "ChatOverflow.jar"
44

55
libraryDependencies += "org.scala-lang.modules" %% "scala-xml" % "1.1.1"
6+
libraryDependencies += "org.jline" % "jline-terminal-jansi" % "3.11.0" // used for terminal width
67
fork := true

bootstrap/src/main/scala/Bootstrap.scala

Lines changed: 139 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,6 @@ object Bootstrap {
3434
if (javaPath.isDefined) {
3535
println("Found java installation. Starting ChatOverflow...")
3636

37-
// Create config folder, if not existent
38-
if (!new File("config/").exists()) {
39-
new File("config/").mkdir()
40-
}
41-
4237
// Start chat overflow!
4338
val process = new java.lang.ProcessBuilder(javaPath.get, "-cp", s"bin/*${File.pathSeparator}lib/*", chatOverflowMainClass)
4439
.inheritIO().start()
@@ -85,69 +80,118 @@ object Bootstrap {
8580
}
8681

8782
/**
88-
* Checks if the library folder exists or the reload-flag is set. Triggers the download-process.
83+
* Checks if the library folder exists or the reload-flag is set. Triggers the download-process if libraries are missing.
8984
*
9085
* @param args the args, the launcher has been called with
9186
* @return false, if there is a serious problem
9287
*/
9388
def checkLibraries(args: Array[String]): Boolean = {
9489

95-
// TODO: Someday in the future, we need incremental library checking to manage updates without full download
96-
9790
val libFolder = new File(s"$currentFolderPath/lib")
98-
// Args contains --reload or lib folder is non existent?
99-
if ((args.length > 0 && args.head == "--reload") || !libFolder.exists()) {
100-
101-
// Create or clean directory
102-
if (libFolder.exists()) {
103-
for (libFile <- libFolder.listFiles()) {
104-
try {
105-
libFile.delete()
106-
} catch {
107-
case e: Exception => println(s"Unable to delete file '${libFile.getName}'. Message: ${e.getMessage}")
108-
}
109-
}
110-
} else {
91+
92+
// Create folder for libs if missing
93+
if (!libFolder.exists()) {
94+
try {
95+
libFolder.mkdir()
96+
} catch {
97+
case e: Exception => println(s"Unable to create library directory. Message: ${e.getMessage}")
98+
return false
99+
}
100+
}
101+
102+
// --reload flags instructs to delete all downloaded libraries and to re-download them
103+
if (args.contains("--reload")) {
104+
for (libFile <- libFolder.listFiles()) {
111105
try {
112-
libFolder.mkdir()
106+
libFile.delete()
113107
} catch {
114-
case e: Exception => println(s"Unable to create library directory. Message: ${e.getMessage}")
108+
case e: Exception => println(s"Unable to delete file '${libFile.getName}'. Message: ${e.getMessage}")
109+
return false
115110
}
116111
}
112+
}
117113

118-
// Download all libraries
119-
// TODO: Check validity if everything is downloaded
120-
println("Downloading libraries...")
121-
downloadLibraries()
114+
val dependencies = getDependencies
122115

123-
} else {
124-
println("Found libraries folder. Assuming all dependencies are available properly.")
125-
true
126-
}
116+
// Download all libraries
117+
// TODO: Check validity if everything is downloaded
118+
// try downloading libs and only if it succeeded (returned true) then try to delete older libs
119+
downloadMissingLibraries(dependencies) && deleteUndesiredLibraries(dependencies)
127120
}
128121

129122
/**
130-
* Reads the dependency xml file and tries to download every library.
123+
* Reads the dependency xml file and tries to download every library that is missing.
131124
*
132125
* @return false, if there is a serious problem
133126
*/
134-
def downloadLibraries(): Boolean = {
135-
136-
// Get dependency xml and read dependencies with their download URL
137-
val dependencyStream = getClass.getResourceAsStream("/dependencies.xml")
138-
val dependencyXML = xml.XML.load(dependencyStream)
139-
val dependencies = for (dependency <- dependencyXML \\ "dependency")
140-
yield ((dependency \ "name").text.trim, (dependency \ "url").text.trim)
141-
142-
for (i <- dependencies.indices) {
143-
val dependency = dependencies(i)
144-
println(s"[${i + 1}/${dependencies.length}] ${dependency._1} (${dependency._2})")
145-
if (!downloadLibrary(dependency._1, dependency._2)) {
146-
// Second try, just in case
147-
downloadLibrary(dependency._1, dependency._2)
127+
private def downloadMissingLibraries(dependencies: List[(String, String)]): Boolean = {
128+
val pb = new ProgressBar(dependencies.length)
129+
130+
// using par here to make multiple http requests in parallel, otherwise its awfully slow on internet connections with high RTTs
131+
val missing = dependencies.par.filterNot(dep => {
132+
val (name, url) = dep
133+
pb.countUp()
134+
pb.updateDescription(s"$name@$url")
135+
136+
isLibraryDownloaded(url)
137+
}).toList
138+
139+
pb.finish()
140+
141+
if (missing.isEmpty) {
142+
println("All required libraries are already downloaded.")
143+
} else {
144+
println(s"Downloading ${missing.length} missing libraries...")
145+
146+
val pb = new ProgressBar(missing.length)
147+
148+
for ((name, url) <- missing) {
149+
pb.countUp()
150+
pb.updateDescription(s"$name@$url")
151+
152+
if (!downloadLibrary(name, url)) {
153+
// Second try, just in case
154+
if (!downloadLibrary(name, url)) {
155+
return false // error has been displayed, stop bootstrapper from starting with missing lib
156+
}
157+
}
148158
}
159+
160+
pb.finish()
161+
}
162+
true // everything went fine
163+
}
164+
165+
/**
166+
* Deletes all undesired libraries. Currently these are all libs that aren't on the list of dependencies.
167+
* The main responsibility is to delete old libs that got updated or libs that aren't required anymore by Chat Overflow.
168+
*
169+
* @param dependencies the libs that should be kept
170+
* @return false, if a file couldn't be deleted
171+
*/
172+
private def deleteUndesiredLibraries(dependencies: List[(String, String)]): Boolean = {
173+
val libDir = new File(s"$currentFolderPath/lib")
174+
if (libDir.exists() && libDir.isDirectory) {
175+
// Desired filenames
176+
val libraryFilenames = dependencies.map(d => libraryFile(d._2).getName)
177+
178+
val undesiredFiles = libDir.listFiles().filterNot(file => libraryFilenames.contains(file.getName)) // filter out libs on the dependency list
179+
180+
// Count errors while trying to remove undesired files
181+
val errorCount = undesiredFiles.count(file => {
182+
println(s"Deleting old or unnecessary library at $file")
183+
if (file.delete()) {
184+
false // no error
185+
} else {
186+
println(s"Error: Couldn't delete file $file.")
187+
true // error
188+
}
189+
})
190+
errorCount == 0 // return false if at least one error occurred
191+
} else {
192+
// Shouldn't be possible, because this is called from checkLibraries, which creates this directory.
193+
true
149194
}
150-
true
151195
}
152196

153197
/**
@@ -169,7 +213,7 @@ object Bootstrap {
169213
else {
170214
// Save file in the lib folder (keeping the name and type)
171215
try {
172-
url #> new File(s"$currentFolderPath/lib/${libraryURL.substring(libraryURL.lastIndexOf("/"))}") !!
216+
url #> libraryFile(libraryURL) !!
173217

174218
true
175219
} catch {
@@ -187,6 +231,53 @@ object Bootstrap {
187231
}
188232
}
189233

234+
/**
235+
* Gets all required dependencies from the dependencies.xml in the jar file
236+
*
237+
* @return a list of tuples that contain the name (e.g. log4j) without org or version and the url.
238+
*/
239+
private def getDependencies: List[(String, String)] = {
240+
val stream = getClass.getResourceAsStream("/dependencies.xml")
241+
val depXml = xml.XML.load(stream)
242+
val dependencies = depXml \\ "dependency"
243+
val dependencyTuples = dependencies.map(dep => {
244+
val name = (dep \ "name").text.trim
245+
val url = (dep \ "url").text.trim
246+
(name, url)
247+
})
248+
249+
dependencyTuples.toList
250+
}
251+
252+
/**
253+
* Checks whether this library is fully downloaded
254+
*
255+
* @param libraryURL the url of the library
256+
* @return true if it is completely downloaded, false if only partially downloaded or not downloaded at all
257+
*/
258+
private def isLibraryDownloaded(libraryURL: String): Boolean = {
259+
val f = libraryFile(libraryURL)
260+
261+
if (!f.exists()) {
262+
false
263+
} else {
264+
try {
265+
// We assume here that the libs don't change at the repo.
266+
// While this is true for Maven Central, which is immutable once a file has been uploaded, its not for JCenter.
267+
// Updating a released artifact generally isn't valued among developers
268+
// and the odds of the updated artifact having the same size is very unlikely.
269+
val url = new URL(libraryURL)
270+
url.openConnection().getContentLengthLong == f.length()
271+
} catch {
272+
case _: Exception => false
273+
}
274+
}
275+
}
276+
277+
private def libraryFile(libraryURL: String): File = {
278+
new File(s"$currentFolderPath/lib/${libraryURL.substring(libraryURL.lastIndexOf("/"))}")
279+
}
280+
190281
/**
191282
* Checks, if the installation is valid
192283
*/
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import org.jline.terminal.TerminalBuilder
2+
3+
/**
4+
* Progress bar used reporting the status while checking and downloading libs.
5+
*
6+
* @param max count of events e.g. length of the list which progress is monitored.
7+
*/
8+
class ProgressBar(max: Int) {
9+
// Width of the terminal, used for size calculations
10+
private val width = {
11+
val width = TerminalBuilder.builder().dumb(true).build().getWidth
12+
13+
// Size couldn't be figured out, use a default
14+
if (width <= 10)
15+
80
16+
else
17+
width
18+
}
19+
20+
private var count = 0
21+
private var description = ""
22+
23+
// We need to create a empty line so that latest line before creation won't be overwritten by the draw method.
24+
println()
25+
draw() // Initial draw
26+
27+
/**
28+
* Increases count by 1 and re-draws the progress bar with the updated count.
29+
*/
30+
def countUp(): Unit = {
31+
// Thread-safeness when working with parallel collections
32+
count.synchronized {
33+
count += 1
34+
draw()
35+
}
36+
}
37+
38+
/**
39+
* Updates the description and re-draws the description line.
40+
* @param desc the new description
41+
*/
42+
def updateDescription(desc: String): Unit = {
43+
// Thread-safeness when working with parallel collections
44+
description.synchronized {
45+
description = desc
46+
drawDescription()
47+
}
48+
}
49+
50+
/**
51+
* Deletes the description to result in a blank line. The progress bar will still be visible.
52+
* After this you can normally print at the beginning of a new line and don't start at the end of the description.
53+
*/
54+
def finish(): Unit = {
55+
description = ""
56+
drawDescription()
57+
}
58+
59+
/**
60+
* Draws the progress bar in the line above the current one.
61+
*/
62+
private def draw(): Unit = {
63+
val barWidth = width - 16 // Width of the bar without percentage and counts
64+
val percentage = count * 100 / max
65+
val equalsSigns = "=" * (barWidth * percentage / 100)
66+
val whiteSpaces = " " * (barWidth - equalsSigns.length)
67+
68+
val content = "%3d%% (%2d|%2d) [%s]".format(percentage, count, max, equalsSigns + ">" + whiteSpaces)
69+
70+
print(s"\033[1A\r$content\n")
71+
// | | | |
72+
// Go up 1 line | | |
73+
// Go to beginning of line
74+
// | |
75+
// Actual progress bar
76+
// |
77+
// Go back down for description
78+
}
79+
80+
/**
81+
* Draws the description which is located in the current line.
82+
*/
83+
private def drawDescription(): Unit = {
84+
// Cap the description at the width of the terminal, otherwise a new line is created and everything would shift.
85+
// If the user needs to see a really long url he can just widen his terminal.
86+
val content = description.take(width)
87+
88+
print(s"\r\033[0K$content")
89+
// | |
90+
// Go to beginning
91+
// |
92+
// Clear from cursor to end of the line
93+
}
94+
95+
}

src/main/scala/org/codeoverflow/chatoverflow/configuration/ConfigurationService.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,12 @@ class ConfigurationService(val configFilePath: String) extends WithLogger {
153153
* Saves the xml content to the config xml.
154154
*/
155155
private def saveXML(xmlContent: Node): Unit = {
156+
// Create config folder, if not existent
157+
val dir = new File(configFilePath).getParentFile
158+
if(!dir.exists()) {
159+
dir.mkdir()
160+
}
161+
156162
val writer = new PrintWriter(configFilePath)
157163
writer.print(new PrettyPrinter(120, 2).format(xmlContent))
158164
writer.close()

src/main/scala/org/codeoverflow/chatoverflow/requirement/service/twitch/chat/TwitchChatConnector.scala

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,27 @@ class TwitchChatConnector(override val sourceIdentifier: String) extends Connect
3030
}
3131

3232
def joinChannel(channel: String): Unit = {
33-
bot.send().joinChannel(channel)
34-
channels += channel
33+
val formattedChan = formatChannel(channel)
34+
bot.send().joinChannel(formattedChan)
35+
channels += formattedChan
3536
}
3637

3738
def sendChatMessage(channel: String, chatMessage: String): Unit = {
38-
if (!isJoined(channel)) throw new IllegalArgumentException(s"you must join the '$channel' channel, before you can send messages to it")
39-
bot.send().message(channel, chatMessage)
39+
val formattedChan = formatChannel(channel)
40+
if (!isJoined(formattedChan)) throw new IllegalArgumentException(s"you must join the '$channel' channel, before you can send messages to it")
41+
bot.send().message(formattedChan, chatMessage)
4042
}
4143

4244
override def getUniqueTypeString: String = this.getClass.getName
4345

44-
def isJoined(channel: String): Boolean = channels.contains(channel)
46+
def isJoined(channel: String): Boolean = channels.contains(formatChannel(channel))
47+
48+
/**
49+
* Ensures that the channel is in following format: "#lowercasename"
50+
* @param chan the unmodified channel
51+
* @return the channel in the correct format, changes nothing if already correct
52+
*/
53+
private def formatChannel(chan: String): String = s"#${chan.stripPrefix("#").toLowerCase}"
4554

4655
private def getConfig: Configuration = {
4756

0 commit comments

Comments
 (0)