Skip to content

Commit 42ae84a

Browse files
committed
Added server-side compilation of *.java files (along with *.scala files)
1 parent 71994ab commit 42ae84a

File tree

8 files changed

+157
-32
lines changed

8 files changed

+157
-32
lines changed

Scalatron/Readme.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,16 @@ in the public domain. Feel free to use, copy, and improve them!
5151

5252
## Version History
5353

54+
### Version 1.1.0.2 -- 2012-05-23
55+
56+
* Adds support for server-side compilation of bots written in Java. This relies on Java's `tools.jar`,
57+
which may not be available on all systems. Works with Java 1.6 on MacOSX, fails on Windows.
58+
59+
60+
5461
### Version 1.1.0.1 -- 2012-05-22
5562

56-
* Adds support for bots written in Java. Implement a bot as follows:
63+
* Adds support for bots written in Java. Implement a simple bot by placing this code in a file `ControlFunction.java`:
5764
public class ControlFunction { final public String respond(String input) { return "Status(text=Written in Java)"; } }
5865

5966

Scalatron/src/scalatron/scalatron/api/ScalatronOutwardDemo.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ object ScalatronOutwardDemo
227227

228228
} finally {
229229
// delete the temporary directory
230-
FileUtil.deleteRecursively(tmpDirPath)
230+
FileUtil.deleteRecursively(tmpDirPath, atThisLevel = true, verbose = false)
231231
}
232232
}
233233
}

Scalatron/src/scalatron/scalatron/impl/CompileActor.scala

Lines changed: 107 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import tools.nsc.reporters.StoreReporter
1010
import scalatron.core.Scalatron
1111
import scala.tools.nsc.util.{BatchSourceFile, Position}
1212
import akka.actor.Actor
13+
import scala.util.parsing.input.OffsetPosition
14+
import java.util.Locale
15+
import java.io.IOException
1316

1417

1518
/** Each compile actor holds a Scala compiler instance and uses it to process CompileJob messages
@@ -85,6 +88,14 @@ object CompileActor {
8588

8689
classPaths
8790
}
91+
92+
93+
// constants
94+
object Constants {
95+
val ScalaCompilerInfo = 0 // see scala.tools.nsc.reporters.Reporter.INFO
96+
val ScalaCompilerWarning = 1 // see scala.tools.nsc.reporters.Reporter.WARNING
97+
val ScalaCompilerError = 2 // see scala.tools.nsc.reporters.Reporter.ERROR
98+
}
8899
}
89100

90101
case class CompileActor(verbose: Boolean) extends Actor {
@@ -125,12 +136,96 @@ case class CompileActor(verbose: Boolean) extends Actor {
125136
* @param outputDirectoryPath the output directory path where the class files should go
126137
*/
127138
private def compileFromFiles(sourceFilePathList: Iterable[String], outputDirectoryPath: String): CompileResult = {
128-
// System.err.println("COMPILE WORKER ACTOR = " + self.toString())
129-
compile(
139+
// feed all source files to the Scala compiler, including *.java files
140+
// it will parse the Java files to resolve dependencies, but will not generate code for them
141+
val scalaCompilationResult = compileScalaCode(
130142
(run: Global#Run) => { run.compile(sourceFilePathList.toList) },
131143
outputDirectoryPath,
132144
sourceFilePathList.mkString(", ")
133145
)
146+
147+
// Test: are there *.java sources among the source files?
148+
val javaFilePathList = sourceFilePathList.filter(_.endsWith(".java"))
149+
if(javaFilePathList.isEmpty) {
150+
// no *.java files, just *.scala
151+
scalaCompilationResult
152+
} else {
153+
// there are some *.java files => compile them and merge the error messages!
154+
try {
155+
import javax.tools._
156+
157+
var javaCompilerErrors = 0
158+
var javaCompilerWarnings = 0
159+
val javaCompilerMessageBuilder = Vector.newBuilder[CompilerMessage]
160+
val diagnosticListener = new DiagnosticListener[Any] {
161+
def report(diagnostic: Diagnostic[_]) {
162+
val sourceFilePath = diagnostic.getSource match {
163+
case t: Any => t.toString // TODO
164+
}
165+
166+
import CompileActor.Constants._
167+
val severity = diagnostic.getKind match {
168+
case Diagnostic.Kind.ERROR => javaCompilerErrors += 1; ScalaCompilerError
169+
case Diagnostic.Kind.WARNING => javaCompilerWarnings += 1; ScalaCompilerWarning
170+
case Diagnostic.Kind.MANDATORY_WARNING => javaCompilerWarnings += 1; ScalaCompilerWarning
171+
case _ => ScalaCompilerInfo
172+
}
173+
174+
javaCompilerMessageBuilder +=
175+
CompilerMessage(
176+
CompilerMessagePosition(sourceFilePath, diagnostic.getLineNumber.toInt, diagnostic.getColumnNumber.toInt),
177+
msg = diagnostic.getMessage(Locale.ENGLISH),
178+
severity = severity
179+
)
180+
}
181+
}
182+
183+
// Prepare the compilation options to be used during Java compilation
184+
// We are asking the compiler to place the output files under the /out folder.
185+
val compileOptions = scala.collection.JavaConversions.asJavaIterable(Iterable("-d", outputDirectoryPath))
186+
187+
val compiler = ToolProvider.getSystemJavaCompiler
188+
if(compiler==null) throw new IllegalStateException("Java Compiler not available (on this platform)")
189+
190+
val fileManager = compiler.getStandardFileManager(diagnosticListener, null, null)
191+
val fileObjects = fileManager.getJavaFileObjectsFromStrings(scala.collection.JavaConversions.asJavaIterable(javaFilePathList))
192+
val task = compiler.getTask(null, fileManager, diagnosticListener, compileOptions, null, fileObjects)
193+
val javaCompilationSuccessful = task.call()
194+
195+
try {
196+
fileManager.close()
197+
} catch {
198+
case t: Throwable =>
199+
System.err.println("error: while closing Java compiler standard file manager: " + t)
200+
}
201+
202+
val javaCompilationDuration = 0 // TODO: milliseconds
203+
val javaCompilerMessages = javaCompilerMessageBuilder.result()
204+
CompileResult(
205+
javaCompilationSuccessful && scalaCompilationResult.compilationSuccessful,
206+
javaCompilationDuration + scalaCompilationResult.duration,
207+
scalaCompilationResult.errorCount,
208+
scalaCompilationResult.warningCount,
209+
scalaCompilationResult.compilerMessages ++ javaCompilerMessages
210+
)
211+
} catch {
212+
case t: Throwable =>
213+
// Uh, something went wrong with the Java compilation - maybe not working on this system?
214+
System.err.println("error: exception during attempt to compile Java files: " + t)
215+
CompileResult(
216+
false,
217+
scalaCompilationResult.duration,
218+
scalaCompilationResult.errorCount,
219+
scalaCompilationResult.warningCount,
220+
scalaCompilationResult.compilerMessages ++
221+
Iterable(CompilerMessage(
222+
CompilerMessagePosition(javaFilePathList.head, 0, 0),
223+
msg = "exception during attempt to compile Java files: " + t,
224+
severity = CompileActor.Constants.ScalaCompilerError
225+
))
226+
)
227+
}
228+
}
134229
}
135230

136231
/** Compiles a given collection of source files residing in memory into a given output directory.
@@ -157,7 +252,7 @@ case class CompileActor(verbose: Boolean) extends Actor {
157252
at scala.tools.nsc.Global$Run.compileSources(Global.scala:916)
158253
at scalatron.scalatron.impl.CompileActor$$anonfun$scalatron$scalatron$impl$CompileActor$$compileFromMemory$1.apply(CompileActor.scala:143)
159254
*/
160-
compile(
255+
compileScalaCode(
161256
(run: Global#Run) => {
162257
val batchSourceFileList = sourceFiles.map(sf => {
163258
new BatchSourceFile(sf.filename, sf.code.toCharArray)
@@ -178,7 +273,7 @@ case class CompileActor(verbose: Boolean) extends Actor {
178273
* @param runDescription a description for verbose output, e.g. a list of filenames
179274
* @return a CompileResult instance holding any compiler messages
180275
*/
181-
private def compile(compilerInvocation: (Global#Run) => Unit, outputDirectoryPath: String, runDescription: String): CompileResult = {
276+
private def compileScalaCode(compilerInvocation: (Global#Run) => Unit, outputDirectoryPath: String, runDescription: String): CompileResult = {
182277
compilerGlobalOpt match {
183278
case None =>
184279
throw new IllegalStateException("compiler not initialized")
@@ -197,7 +292,12 @@ case class CompileActor(verbose: Boolean) extends Actor {
197292
val elapsed = (endTime - startTime).toInt
198293
if( verbose ) println(" ...compilation completed (" + elapsed + "ms)")
199294

200-
val errorList = compilerGlobal.reporter.asInstanceOf[StoreReporter].infos.map(info => CompilerMessage(info.pos, info.msg, info.severity.id))
295+
val errorList =
296+
compilerGlobal.reporter.asInstanceOf[StoreReporter].infos
297+
.map(info => CompilerMessage(
298+
CompilerMessagePosition(info.pos.source.path, info.pos.line, info.pos.column),
299+
info.msg,
300+
info.severity.id))
201301
val hasErrors = compilerGlobal.reporter.hasErrors
202302
val errorCount = compilerGlobal.reporter.ERROR.count
203303
val warningCount = compilerGlobal.reporter.WARNING.count
@@ -211,7 +311,8 @@ case class CompileActor(verbose: Boolean) extends Actor {
211311

212312

213313
/** Records one error or warning that was generated by the compiler. */
214-
case class CompilerMessage(pos: Position, msg: String, severity: Int)
314+
case class CompilerMessagePosition(sourceFilePath: String, line: Int, column: Int)
315+
case class CompilerMessage(pos: CompilerMessagePosition, msg: String, severity: Int)
215316

216317

217318
/** Messages passed to and from the compile actor. */

Scalatron/src/scalatron/scalatron/impl/FileUtil.scala

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,28 +40,35 @@ object FileUtil
4040

4141

4242
/** Recursively deletes the given directory and all of its contents (CAUTION!)
43+
* @param path the path of the directory to begin recursive deletion at
44+
* @param atThisLevel if true: the directory at `path` is deleted; if false: only its children
45+
* @param verbose if true, log verbosely to the console
4346
* @throws IllegalStateException if there is a problem deleting a file or directory
4447
*/
45-
def deleteRecursively(path: String, verbose: Boolean = false) {
48+
def deleteRecursively(path: String, atThisLevel: Boolean, verbose: Boolean) {
4649
val itemAtPath = new File(path)
4750
if(itemAtPath.exists) {
4851
// caller handles exceptions
4952
if(itemAtPath.isDirectory) {
5053
if(verbose) println(" deleting contents of directory at: " + path)
5154
val filesInsideUserDir = itemAtPath.listFiles()
5255
if(filesInsideUserDir != null) {
53-
filesInsideUserDir.foreach(file => deleteRecursively(file.getAbsolutePath, verbose))
56+
filesInsideUserDir.foreach(file => deleteRecursively(file.getAbsolutePath, true, verbose))
5457
}
55-
if(verbose) println(" deleting directory: " + path)
56-
if(!itemAtPath.delete()) {
57-
System.err.println("error: failed to delete directory: %s".format(itemAtPath.getAbsolutePath))
58-
throw new IllegalStateException("failed to delete directory at: " + itemAtPath.getAbsolutePath)
58+
if(atThisLevel) {
59+
if(verbose) println(" deleting directory: " + path)
60+
if(!itemAtPath.delete()) {
61+
System.err.println("error: failed to delete directory: %s".format(itemAtPath.getAbsolutePath))
62+
throw new IllegalStateException("failed to delete directory at: " + itemAtPath.getAbsolutePath)
63+
}
5964
}
6065
} else {
61-
if(verbose) println(" deleting file at: " + path)
62-
if(!itemAtPath.delete()) {
63-
System.err.println("error: failed to delete file: %s".format(itemAtPath.getAbsolutePath))
64-
throw new IllegalStateException("failed to delete file: " + itemAtPath.getAbsolutePath)
66+
if(atThisLevel) {
67+
if(verbose) println(" deleting file at: " + path)
68+
if(!itemAtPath.delete()) {
69+
System.err.println("error: failed to delete file: %s".format(itemAtPath.getAbsolutePath))
70+
throw new IllegalStateException("failed to delete file: " + itemAtPath.getAbsolutePath)
71+
}
6572
}
6673
}
6774
}

Scalatron/src/scalatron/scalatron/impl/ScalatronSample.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ case class ScalatronSample(name: String, scalatron: ScalatronImpl) extends Scala
1313

1414
def sourceFiles: Iterable[SourceFile] = SourceFileCollection.loadFrom(sampleSourceDirectoryPath)
1515

16-
def delete() { deleteRecursively(sampleDirectoryPath, scalatron.verbose) }
16+
def delete() { deleteRecursively(sampleDirectoryPath, atThisLevel = true, verbose = scalatron.verbose) }
1717
}

Scalatron/src/scalatron/scalatron/impl/ScalatronUser.scala

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ case class ScalatronUser(name: String, scalatron: ScalatronImpl) extends Scalatr
8181
throw ScalatronException.Forbidden("deleting '" + Scalatron.Constants.AdminUserName + "' account is not permitted")
8282
} else {
8383
// caller must handle IOError exceptions
84-
deleteRecursively(userDirectoryPath, scalatron.verbose)
85-
deleteRecursively(userPluginDirectoryPath, scalatron.verbose)
84+
deleteRecursively(userDirectoryPath, atThisLevel = true, verbose = scalatron.verbose)
85+
deleteRecursively(userPluginDirectoryPath, atThisLevel = true, verbose = scalatron.verbose)
8686

8787
// remove from cache
8888
release()
@@ -144,12 +144,17 @@ case class ScalatronUser(name: String, scalatron: ScalatronImpl) extends Scalatr
144144
*/
145145
val gameSpecificPackagePath = scalatron.game.gameSpecificPackagePath
146146
val packagePath = gameSpecificPackagePath + "." + name
147-
val packageStatementWithNewline = "package " + packagePath + "\n"
147+
val packageStatement = "package " + packagePath
148148
val patchedSourceFiles = transientSourceFiles.map(sf => {
149149
val localCode = sf.code
150150
// CBB: warn the user about conflicts if she embeds her own package name
151151
// but if(localCode.contains("package")) ... is too dumb
152-
val patchedCode = packageStatementWithNewline + localCode
152+
val patchedCode =
153+
if(sf.filename.endsWith(".java")) {
154+
packageStatement + ";\n" + localCode
155+
} else {
156+
packageStatement + "\n" + localCode
157+
}
153158
if(scalatron.verbose) println(" patching '%s' with 'package %s'".format(sf.filename, packagePath))
154159
SourceFile(sf.filename, patchedCode)
155160
})
@@ -163,7 +168,9 @@ case class ScalatronUser(name: String, scalatron: ScalatronImpl) extends Scalatr
163168
// so, as a temporary work-around, we create temp files on disk:
164169
// TODO: this code should probably exist within writeSourceFiles() - refactor!
165170
val patchedSourceDirectory = new File(patchedSourceDirectoryPath)
166-
if(!patchedSourceDirectory.exists) {
171+
if(patchedSourceDirectory.exists) {
172+
deleteRecursively(patchedSourceDirectoryPath, atThisLevel = false, verbose = scalatron.verbose)
173+
} else {
167174
if(!patchedSourceDirectory.mkdirs()) {
168175
System.err.println("error: cannot create patched source directory at: " + patchedSourceDirectory)
169176
throw new IllegalStateException("error: cannot create patched source directory at: " + patchedSourceDirectory)
@@ -424,10 +431,11 @@ object ScalatronUser {
424431
val outputDirectoryPath = compileJob.outputDirectoryPath
425432
val outputDirectory = new File(outputDirectoryPath)
426433
if( outputDirectory.exists() ) {
427-
// it should not exist before we start. If it does, we delete it
428-
deleteRecursively(outputDirectoryPath, scalatron.verbose)
434+
// it should be empty before we start. If it exists, we clear it
435+
deleteRecursively(outputDirectoryPath, atThisLevel = false, verbose = scalatron.verbose)
436+
} else {
437+
outputDirectory.mkdirs()
429438
}
430-
outputDirectory.mkdirs()
431439

432440

433441
// compile the source file, using an Akka Actor with a fixed time-out
@@ -451,8 +459,10 @@ object ScalatronUser {
451459
// build the .jar archive file
452460
JarBuilder(outputDirectoryPath, jarFilePath, scalatron.verbose)
453461

454-
// delete the output directory - it is no longer needed
455-
deleteRecursively(outputDirectoryPath, scalatron.verbose)
462+
// we DO NOT delete the contents of the output directory, even though they are no longer needed
463+
// the directory will be cleared before the next compilation starts, and leaving the files is
464+
// useful when diagnosing problems.
465+
// deleteRecursively(outputDirectoryPath, atThisLevel = false, verbose = scalatron.verbose)
456466
}
457467

458468
// transform compiler output into the BuildResult format expected by the Scalatron API
@@ -463,7 +473,7 @@ object ScalatronUser {
463473
compileResult.errorCount,
464474
compileResult.warningCount,
465475
compileResult.compilerMessages.map(msg => {
466-
val absoluteSourceFilePath = msg.pos.source.path
476+
val absoluteSourceFilePath = msg.pos.sourceFilePath
467477
val relativeSourceFilePath =
468478
if(absoluteSourceFilePath.startsWith(sourceDirectoryPrefix)) {
469479
absoluteSourceFilePath.drop(sourceDirectoryPrefix.length)

Scalatron/test/scalatron/scalatron/api/ScalatronApiSpec.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ object ScalatronApiTest
363363
result
364364
} finally {
365365
// delete the temporary directory
366-
FileUtil.deleteRecursively(tmpDirPath, verbose)
366+
FileUtil.deleteRecursively(tmpDirPath, atThisLevel = true, verbose = verbose)
367367
}
368368
}
369369

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ organization := "Scalatron"
22

33
name := "Scalatron"
44

5-
version in Global := "1.1.0.1"
5+
version in Global := "1.1.0.2"
66

77
scalaVersion := "2.9.1"

0 commit comments

Comments
 (0)