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

Commit 8ba906c

Browse files
committed
Merged with branch develop.
2 parents 5a80ee5 + 61f0665 commit 8ba906c

18 files changed

+230
-95
lines changed

.gitignore

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ project/plugins/project/
2121
/plugins-public/
2222
/plugins-private/
2323
/gui/
24+
/setup/
2425

2526
# Plugin Framework
2627
/plugins/
@@ -36,7 +37,7 @@ project/plugins/project/
3637
/wiki/
3738

3839
# Plugin Data
39-
data/
40+
/data/
4041

41-
# Built gui
42-
/src/main/resources/chatoverflow-gui
42+
# Log Output
43+
/log/

build.sbt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,13 @@ deploy := BootstrapUtility.prepareDeploymentTask(streams.value.log, scalaMajorVe
101101
deployDev := BootstrapUtility.prepareDevDeploymentTask(streams.value.log, scalaMajorVersion, apiProjectPath.value, libraryDependencies.value.toList)
102102
gui := BuildUtility(streams.value.log).guiTask(guiProjectPath.value, streams.value.cacheDirectory / "gui")
103103

104+
Compile / packageBin := {
105+
BuildUtility(streams.value.log).packageGUITask(guiProjectPath.value, scalaMajorVersion, crossTarget.value)
106+
(Compile / packageBin).value
107+
}
108+
109+
Compile / unmanagedJars := (crossTarget.value ** "chatoverflow-gui*.jar").classpath
110+
104111
// ---------------------------------------------------------------------------------------------------------------------
105112
// UTIL
106113
// ---------------------------------------------------------------------------------------------------------------------
@@ -122,4 +129,3 @@ lazy val getDependencyList = Def.task[List[ModuleID]] {
122129

123130
// Clears the built GUI dirs on clean
124131
cleanFiles += baseDirectory.value / guiProjectPath.value / "dist"
125-
cleanFiles += baseDirectory.value / "src" / "main" / "resources" / "chatoverflow-gui"

project/BuildUtility.scala

Lines changed: 81 additions & 66 deletions
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
*
@@ -141,90 +145,64 @@ class BuildUtility(logger: ManagedLogger) {
141145
return
142146
}
143147

144-
if (installGuiDeps(guiDir, cacheDir).isEmpty)
145-
return // Early return on failure, error has already been displayed
146-
147-
val outDir = buildGui(guiDir, cacheDir)
148-
if (outDir.isEmpty)
149-
return // Again early return on failure
150-
151-
// Copy built gui into resources, will be included in the classpath on execution of the framework
152-
sbt.IO.copyDirectory(outDir.get, new File("src/main/resources/chatoverflow-gui"))
153-
}
154-
}
155-
156-
/**
157-
* Download the dependencies of the gui using npm.
158-
*
159-
* @param guiDir the directory of the gui.
160-
* @param cacheDir a dir, where sbt can store files for caching in the "install" sub-dir.
161-
* @return None, if a error occurs which will be displayed, otherwise the output directory with the built gui.
162-
*/
163-
private def installGuiDeps(guiDir: File, cacheDir: File): Option[File] = {
164-
// Check buildGui for a explanation, it's almost the same.
165-
166-
val install = FileFunction.cached(new File(cacheDir, "install"), FilesInfo.hash)(_ => {
167-
168-
logger info "Installing GUI dependencies."
169-
170-
val exitCode = new ProcessBuilder(getNpmCommand :+ "install": _*)
171-
.inheritIO()
172-
.directory(guiDir)
173-
.start()
174-
.waitFor()
148+
val packageJson = new File(guiDir, "package.json")
175149

176-
if (exitCode != 0) {
177-
logger error "GUI dependencies couldn't be installed, please check above log for further details."
178-
return None
179-
} else {
180-
logger info "GUI dependencies successfully installed."
181-
Set(new File(guiDir, "node_modules"))
150+
if (!executeNpmCommand(guiDir, cacheDir, Set(packageJson), "install",
151+
() => logger error "GUI dependencies couldn't be installed, please check above log for further details.",
152+
() => new File(guiDir, "node_modules")
153+
)) {
154+
return // early return on failure, error has already been displayed
182155
}
183-
})
184156

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

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

213-
if (buildExitCode != 0) {
214-
logger error "GUI couldn't be built, please check above log for further details."
215-
return None
195+
if (exitCode != 0) {
196+
failed()
197+
return false
216198
} else {
217-
logger info "GUI successfully built."
218-
Set(new File(guiDir, "dist"))
199+
Set(success())
219200
}
220-
})
221-
222-
223-
val srcDir = new File(guiDir, "src")
224-
val packageJson = new File(guiDir, "package.json")
225-
val inputs = recursiveFileListing(srcDir) + packageJson
201+
}
202+
}
226203

227-
build(inputs).headOption
204+
cachedFn(inputs)
205+
true
228206
}
229207

230208
private def getNpmCommand: List[String] = {
@@ -235,6 +213,43 @@ class BuildUtility(logger: ManagedLogger) {
235213
}
236214
}
237215

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

project/plugins.sbt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.2")
22
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9")
3+
4+
5+
// JSON lib (Jackson) used for parsing the GUI version in the package.json file
6+
libraryDependencies += "org.json4s" %% "json4s-jackson" % "3.5.2"

src/main/scala/ScalatraBootstrap.scala

Lines changed: 3 additions & 1 deletion
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
}

src/main/scala/org/codeoverflow/chatoverflow/ChatOverflow.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.codeoverflow.chatoverflow
22

3+
import java.io.File
34
import java.security.Policy
45

56
import org.codeoverflow.chatoverflow.api.APIVersion
@@ -49,6 +50,9 @@ class ChatOverflow(val pluginFolderPath: String,
4950

5051
logger debug "Initialization started."
5152

53+
logger debug "Ensuring that all required directories exist."
54+
createDirectories()
55+
5256
logger debug "Enabling framework security policy."
5357
enableFrameworkSecurity()
5458

@@ -99,6 +103,11 @@ class ChatOverflow(val pluginFolderPath: String,
99103
System.setSecurityManager(new SecurityManager)
100104
}
101105

106+
private def createDirectories(): Unit = {
107+
Set(pluginFolderPath, configFolderPath, Launcher.pluginDataPath)
108+
.foreach(path => new File(path).mkdir())
109+
}
110+
102111
/**
103112
* Saves all settings and credentials to the corresponding files in the config folder.
104113
*/

src/main/scala/org/codeoverflow/chatoverflow/Launcher.scala

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package org.codeoverflow.chatoverflow
22

3+
import java.time.OffsetDateTime
4+
import java.time.format.DateTimeFormatter
5+
6+
import org.apache.log4j.{FileAppender, Level, Logger, PatternLayout}
37
import org.codeoverflow.chatoverflow.ui.CLI.parse
48
import org.codeoverflow.chatoverflow.ui.web.Server
59

@@ -19,6 +23,24 @@ object Launcher extends WithLogger {
1923
def main(args: Array[String]): Unit = {
2024
parse(args) { config =>
2125

26+
// Enable logging to log files if specified
27+
if (config.logFileOutput) {
28+
val fileName = s"log/${
29+
OffsetDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm-ss-SSS"))
30+
}.log"
31+
32+
// Try to retrieve the console appender layout or create a default one
33+
val pattern = if (logger.getAppender("CA") != null) {
34+
logger.getAppender("CA").getLayout
35+
} else new PatternLayout("%-4r %d{HH:mm:ss.SSS} [%t] %-5p %c{2} %x - %m%n")
36+
37+
// This is the pattern from our basic console logger
38+
val appender = new FileAppender(pattern, fileName, true)
39+
appender.setThreshold(Level.ALL)
40+
41+
Logger.getRootLogger.addAppender(appender)
42+
}
43+
2244
// Set globally available plugin data path
2345
this.pluginDataPath = config.pluginDataPath
2446

@@ -56,6 +78,8 @@ object Launcher extends WithLogger {
5678
logger warn "Unable to run startup plugins. No/wrong password supplied."
5779
}
5880
}
81+
82+
Runtime.getRuntime.addShutdownHook(new Thread(() => exit()))
5983
}
6084
}
6185

@@ -98,6 +122,5 @@ object Launcher extends WithLogger {
98122
}
99123

100124
logger info "Bye Bye. Stay minzig!"
101-
System.exit(0)
102125
}
103126
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import java.io.{File, PrintWriter}
44

55
import org.codeoverflow.chatoverflow.WithLogger
66
import org.codeoverflow.chatoverflow.api.io
7-
import org.codeoverflow.chatoverflow.api.plugin.configuration.{Requirement, Requirements}
7+
import org.codeoverflow.chatoverflow.api.plugin.configuration.Requirements
88
import org.codeoverflow.chatoverflow.connector.ConnectorRegistry
99
import org.codeoverflow.chatoverflow.framework.PluginFramework
1010
import org.codeoverflow.chatoverflow.instance.PluginInstanceRegistry
@@ -197,7 +197,7 @@ class ConfigurationService(val configFilePath: String) extends WithLogger {
197197
* Creates the xml for all requirements of a plugin.
198198
*/
199199
private def createRequirementXML(requirements: Requirements): Array[Elem] = {
200-
val requirementMap = requirements.getRequirementMap
200+
val requirementMap = requirements.getAccess.getRequirementMap
201201
val keys = requirementMap.keySet().toArray
202202

203203
for {
@@ -264,7 +264,7 @@ object ConfigurationService extends WithLogger {
264264
false
265265
} else {
266266
val requirements = instance.get.getRequirements
267-
val requirement = requirements.getRequirementById(requirementId)
267+
val requirement = requirements.getAccess.getRequirementById(requirementId)
268268

269269
if (!requirement.isPresent) {
270270
logger error s"Unable to find requirement with the given id '$requirementId'."
@@ -289,7 +289,7 @@ object ConfigurationService extends WithLogger {
289289

290290
try {
291291
val reqContent = loadedRequirementType.get.newInstance().asInstanceOf[io.Serializable]
292-
requirement.get.asInstanceOf[Requirement[io.Serializable]].set(reqContent)
292+
requirements.getAccess.setRequirementContent(requirementId, reqContent)
293293
reqContent.deserialize(content)
294294

295295
logger info s"Created requirement content for '$requirementId' and deserialized its content."

0 commit comments

Comments
 (0)