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

Commit 61f0665

Browse files
authored
Merge pull request #109 from codeoverflow-org/feature/102-gui-task-rework
Split GUI into its own jar
2 parents 8497176 + af041bb commit 61f0665

File tree

8 files changed

+164
-85
lines changed

8 files changed

+164
-85
lines changed

.gitignore

-3
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,3 @@ project/plugins/project/
4040

4141
# Log Output
4242
/log/
43-
44-
# Built gui
45-
/src/main/resources/chatoverflow-gui

build.sbt

+7-1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ bs := BootstrapUtility.bootstrapGenTask(streams.value.log, s"$scalaMajorVersion$
9999
deploy := BootstrapUtility.prepareDeploymentTask(streams.value.log, scalaMajorVersion)
100100
gui := BuildUtility(streams.value.log).guiTask(guiProjectPath.value, streams.value.cacheDirectory / "gui")
101101

102+
Compile / packageBin := {
103+
BuildUtility(streams.value.log).packageGUITask(guiProjectPath.value, scalaMajorVersion, crossTarget.value)
104+
(Compile / packageBin).value
105+
}
106+
107+
Compile / unmanagedJars := (crossTarget.value ** "chatoverflow-gui*.jar").classpath
108+
102109
// ---------------------------------------------------------------------------------------------------------------------
103110
// UTIL
104111
// ---------------------------------------------------------------------------------------------------------------------
@@ -117,4 +124,3 @@ lazy val getDependencyList = Def.task[List[ModuleID]] {
117124

118125
// Clears the built GUI dirs on clean
119126
cleanFiles += baseDirectory.value / guiProjectPath.value / "dist"
120-
cleanFiles += baseDirectory.value / "src" / "main" / "resources" / "chatoverflow-gui"

project/BootstrapUtility.scala

+6-10
Original file line numberDiff line numberDiff line change
@@ -165,19 +165,15 @@ object BootstrapUtility {
165165
}
166166

167167
/**
168-
* Copies ONE jar file from the source to all target directories. Useful for single packaged jar files.
169-
*/
168+
* Copies all jar files from the source to all target directories.
169+
*/
170170
private def copyJars(sourceDirectory: String, targetDirectories: List[String], logger: ManagedLogger): Unit = {
171171
val candidates = new File(sourceDirectory)
172172
.listFiles().filter(f => f.isFile && f.getName.toLowerCase.endsWith(".jar"))
173-
if (candidates.length != 1) {
174-
logger warn s"Unable to identify jar file in $sourceDirectory"
175-
} else {
176-
for (targetDirectory <- targetDirectories) {
177-
Files.copy(Paths.get(candidates.head.getAbsolutePath),
178-
Paths.get(s"$targetDirectory/${candidates.head.getName}"))
179-
logger info s"Finished copying file '${candidates.head.getAbsolutePath}' to '$targetDirectory'."
180-
}
173+
for (targetDirectory <- targetDirectories; file <- candidates) {
174+
Files.copy(Paths.get(file.getAbsolutePath),
175+
Paths.get(s"$targetDirectory/${file.getName}"))
176+
logger info s"Finished copying file '${file.getAbsolutePath}' to '$targetDirectory'."
181177
}
182178
}
183179
}

project/BuildUtility.scala

+81-66
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import java.io.{File, IOException}
22
import java.nio.file.{Files, StandardCopyOption}
3+
import java.util.jar.Manifest
34

5+
import com.fasterxml.jackson.databind.ObjectMapper
46
import sbt.internal.util.ManagedLogger
57
import sbt.util.{FileFunction, FilesInfo}
68

9+
import scala.io.Source
10+
711
/**
812
* A build utility instance handles build tasks and prints debug information using the managed logger.
913
*
@@ -139,90 +143,64 @@ class BuildUtility(logger: ManagedLogger) {
139143
return
140144
}
141145

142-
if (installGuiDeps(guiDir, cacheDir).isEmpty)
143-
return // Early return on failure, error has already been displayed
144-
145-
val outDir = buildGui(guiDir, cacheDir)
146-
if (outDir.isEmpty)
147-
return // Again early return on failure
148-
149-
// Copy built gui into resources, will be included in the classpath on execution of the framework
150-
sbt.IO.copyDirectory(outDir.get, new File("src/main/resources/chatoverflow-gui"))
151-
}
152-
}
153-
154-
/**
155-
* Download the dependencies of the gui using npm.
156-
*
157-
* @param guiDir the directory of the gui.
158-
* @param cacheDir a dir, where sbt can store files for caching in the "install" sub-dir.
159-
* @return None, if a error occurs which will be displayed, otherwise the output directory with the built gui.
160-
*/
161-
private def installGuiDeps(guiDir: File, cacheDir: File): Option[File] = {
162-
// Check buildGui for a explanation, it's almost the same.
146+
val packageJson = new File(guiDir, "package.json")
163147

164-
val install = FileFunction.cached(new File(cacheDir, "install"), FilesInfo.hash)(_ => {
165-
166-
logger info "Installing GUI dependencies."
167-
168-
val exitCode = new ProcessBuilder(getNpmCommand :+ "install": _*)
169-
.inheritIO()
170-
.directory(guiDir)
171-
.start()
172-
.waitFor()
173-
174-
if (exitCode != 0) {
175-
logger error "GUI dependencies couldn't be installed, please check above log for further details."
176-
return None
177-
} else {
178-
logger info "GUI dependencies successfully installed."
179-
Set(new File(guiDir, "node_modules"))
148+
if (!executeNpmCommand(guiDir, cacheDir, Set(packageJson), "install",
149+
() => logger error "GUI dependencies couldn't be installed, please check above log for further details.",
150+
() => new File(guiDir, "node_modules")
151+
)) {
152+
return // early return on failure, error has already been displayed
180153
}
181-
})
182154

183-
val input = new File(guiDir, "package.json")
184-
install(Set(input)).headOption
155+
val srcFiles = recursiveFileListing(new File(guiDir, "src"))
156+
val outDir = new File(guiDir, "dist")
157+
158+
executeNpmCommand(guiDir, cacheDir, srcFiles + packageJson, "run build",
159+
() => logger error "GUI couldn't be built, please check above log for further details.",
160+
() => outDir
161+
)
162+
}
185163
}
186164

187165
/**
188-
* Builds the gui using npm.
189-
*
190-
* @param guiDir the directory of the gui.
191-
* @param cacheDir a dir, where sbt can store files for caching in the "build" sub-dir.
192-
* @return None, if a error occurs which will be displayed, otherwise the output directory with the built gui.
193-
*/
194-
private def buildGui(guiDir: File, cacheDir: File): Option[File] = {
166+
* Executes a npm command in the given directory and skips executing the given command
167+
* if no input files have changed and the output file still exists.
168+
*
169+
* @param workDir the directory in which npm should be executed
170+
* @param cacheDir a directory required for caching using sbt
171+
* @param inputs the input files, which will be used for caching.
172+
* If any one of these files change the cache is invalidated.
173+
* @param command the npm command to execute
174+
* @param failed called if npm returned an non-zero exit code
175+
* @param success called if npm returned successfully. Needs to return a file for caching.
176+
* If the returned file doesn't exist the npm command will ignore the cache.
177+
* @return true if npm returned zero as a exit code and false otherwise
178+
*/
179+
private def executeNpmCommand(workDir: File, cacheDir: File, inputs: Set[File], command: String,
180+
failed: () => Unit, success: () => File): Boolean = {
195181
// sbt allows easily to cache our external build using FileFunction.cached
196182
// sbt will only invoke the passed function when at least one of the input files (passed in the last line of this method)
197183
// has been modified. For the gui these input files are all files in the src directory of the gui and the package.json.
198184
// sbt passes these input files to the passed function, but they aren't used, we just instruct npm to build the gui.
199185
// sbt invalidates the cache as well if any of the output files (returned by the passed function) doesn't exist anymore.
200-
201-
val build = FileFunction.cached(new File(cacheDir, "build"), FilesInfo.hash)(_ => {
202-
203-
logger info "Building GUI."
204-
205-
val buildExitCode = new ProcessBuilder(getNpmCommand :+ "run" :+ "build": _*)
186+
val cachedFn = FileFunction.cached(new File(cacheDir, command), FilesInfo.hash) { _ => {
187+
val exitCode = new ProcessBuilder(getNpmCommand ++ command.split("\\s+"): _*)
206188
.inheritIO()
207-
.directory(guiDir)
189+
.directory(workDir)
208190
.start()
209191
.waitFor()
210192

211-
if (buildExitCode != 0) {
212-
logger error "GUI couldn't be built, please check above log for further details."
213-
return None
193+
if (exitCode != 0) {
194+
failed()
195+
return false
214196
} else {
215-
logger info "GUI successfully built."
216-
Set(new File(guiDir, "dist"))
197+
Set(success())
217198
}
218-
})
219-
220-
221-
val srcDir = new File(guiDir, "src")
222-
val packageJson = new File(guiDir, "package.json")
223-
val inputs = recursiveFileListing(srcDir) + packageJson
199+
}
200+
}
224201

225-
build(inputs).headOption
202+
cachedFn(inputs)
203+
true
226204
}
227205

228206
private def getNpmCommand: List[String] = {
@@ -233,6 +211,43 @@ class BuildUtility(logger: ManagedLogger) {
233211
}
234212
}
235213

214+
def packageGUITask(guiProjectPath: String, scalaMajorVersion: String, crossTargetDir: File): Unit = {
215+
val dir = new File(guiProjectPath, "dist")
216+
if (!dir.exists()) {
217+
logger info "GUI hasn't been compiled. Won't create a jar for it."
218+
return
219+
}
220+
221+
val files = recursiveFileListing(dir)
222+
223+
// contains tuples with the actual file as the first value and the name with directory in the jar as the second value
224+
val jarEntries = files.map(file => file -> s"/chatoverflow-gui/${dir.toURI.relativize(file.toURI).toString}")
225+
226+
val guiVersion = getGUIVersion(guiProjectPath).getOrElse("unknown")
227+
228+
sbt.IO.jar(jarEntries, new File(crossTargetDir, s"chatoverflow-gui_$scalaMajorVersion-$guiVersion.jar"), new Manifest())
229+
}
230+
231+
private def getGUIVersion(guiProjectPath: String): Option[String] = {
232+
val packageJson = new File(s"$guiProjectPath/package.json")
233+
if (!packageJson.exists()) {
234+
logger error "The package.json file of the GUI doesn't exist. Have you cloned the GUI in the correct directory?"
235+
return None
236+
}
237+
238+
val content = Source.fromFile(packageJson)
239+
val version = new ObjectMapper().reader().readTree(content.mkString).get("version").asText()
240+
241+
content.close()
242+
243+
if (version.isEmpty) {
244+
logger warn "The GUI version couldn't be loaded from the package.json."
245+
None
246+
} else {
247+
Option(version)
248+
}
249+
}
250+
236251
/**
237252
* Creates a file listing with all files including files in any sub-dir.
238253
*

project/plugins.sbt

+4-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.2")
1+
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.2")
2+
3+
// JSON lib (Jackson) used for parsing the GUI version in the package.json file
4+
libraryDependencies += "org.json4s" %% "json4s-jackson" % "3.5.2"

src/main/scala/ScalatraBootstrap.scala

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import org.codeoverflow.chatoverflow.ui.web.rest.connector.ConnectorController
44
import org.codeoverflow.chatoverflow.ui.web.rest.events.{EventsController, EventsDispatcher}
55
import org.codeoverflow.chatoverflow.ui.web.rest.plugin.PluginInstanceController
66
import org.codeoverflow.chatoverflow.ui.web.rest.types.TypeController
7-
import org.codeoverflow.chatoverflow.ui.web.{CodeOverflowSwagger, OpenAPIServlet}
7+
import org.codeoverflow.chatoverflow.ui.web.{CodeOverflowSwagger, GUIServlet, OpenAPIServlet}
88
import org.scalatra._
99

1010
/**
@@ -30,5 +30,7 @@ class ScalatraBootstrap extends LifeCycle {
3030
context.mount(new PluginInstanceController(), "/instances/*", "instances")
3131
context.mount(new ConnectorController(), "/connectors/*", "connectors")
3232
context.mount(new OpenAPIServlet(), "/api-docs")
33+
34+
context.mount(new GUIServlet(), "/*")
3335
}
3436
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package org.codeoverflow.chatoverflow.ui.web
2+
3+
import java.io.File
4+
import java.net.URI
5+
import java.util.jar.JarFile
6+
7+
import org.codeoverflow.chatoverflow.WithLogger
8+
import org.eclipse.jetty.http.MimeTypes
9+
import org.eclipse.jetty.util.Loader
10+
import org.scalatra.{ActionResult, ScalatraServlet}
11+
12+
import scala.io.Source
13+
14+
/**
15+
* A servlet to serve the GUI files of the chatoverflow-gui dir from the classpath.
16+
* This directory is provided if the gui jar is added on the classpath.
17+
* Responds with an error if the gui jar isn't on the classpath.
18+
*/
19+
class GUIServlet extends ScalatraServlet with WithLogger {
20+
21+
private val jarFilePath = {
22+
val res = Loader.getResource(s"/chatoverflow-gui/")
23+
24+
// directory couldn't be found
25+
if (res == null) {
26+
logger error "GUI couldn't be found on the classpath! Has the GUI been built?"
27+
None
28+
} else {
29+
// remove the path inside the jar and only keep the file path to the jar file
30+
val jarPath = res.getFile.split("!").head
31+
logger info s"GUI jar file found at ${new File(".").toURI.relativize(new URI(jarPath))}"
32+
33+
Some(jarPath)
34+
}
35+
}
36+
37+
get("/*") {
38+
if (jarFilePath.isEmpty) {
39+
ActionResult(500, "GUI couldn't be found on the classpath! Has the GUI been built?", Map())
40+
} else {
41+
val jarFile = new JarFile(new File(new URI(jarFilePath.get)))
42+
43+
val path = if (requestPath == "/")
44+
"/index.html"
45+
else
46+
requestPath
47+
48+
val entry = jarFile.getEntry(s"/chatoverflow-gui$path")
49+
50+
val res = if (entry == null) {
51+
ActionResult(404, s"Requested file '$path' couldn't be found in the GUI jar!", Map())
52+
} else {
53+
contentType = MimeTypes.getDefaultMimeByExtension(entry.getName)
54+
Source.fromInputStream(jarFile.getInputStream(entry)).mkString
55+
}
56+
57+
response.setHeader("Cache-Control", "no-cache,no-store")
58+
jarFile.close()
59+
res
60+
}
61+
}
62+
}

src/main/scala/org/codeoverflow/chatoverflow/ui/web/Server.scala

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package org.codeoverflow.chatoverflow.ui.web
22

33
import org.codeoverflow.chatoverflow.{ChatOverflow, WithLogger}
4-
import org.eclipse.jetty.util.resource.Resource
54
import org.eclipse.jetty.webapp.WebAppContext
65
import org.scalatra.servlet.ScalatraListener
76

@@ -16,9 +15,8 @@ class Server(val chatOverflow: ChatOverflow, val port: Int) extends WithLogger {
1615
private val server = new org.eclipse.jetty.server.Server(port)
1716
private val context = new WebAppContext()
1817
context.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false")
19-
context.setInitParameter("org.eclipse.jetty.servlet.Default.cacheControl", "no-cache,no-store")
2018
context setContextPath "/"
21-
context.setBaseResource(Resource.newClassPathResource("/chatoverflow-gui/"))
19+
context setResourceBase "/"
2220
context.addEventListener(new ScalatraListener)
2321

2422
server.setHandler(context)

0 commit comments

Comments
 (0)