Skip to content

Commit dd7d59d

Browse files
committed
Merge commit '38c4de832e305114dd2f9c41c6d07ab542aaa0e8'
Conflicts: Scalatron/webui/client/Tutorial.js
2 parents 219bf1d + 38c4de8 commit dd7d59d

File tree

13 files changed

+289
-200
lines changed

13 files changed

+289
-200
lines changed

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

Lines changed: 95 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
package scalatron.scalatron.api
66

77

8-
import scalatron.scalatron.api.Scalatron.{Sample, SourceFile, User}
8+
import scalatron.scalatron.api.Scalatron.{Sample, User, SourceFileCollection}
99
import akka.actor.ActorSystem
10+
import scala.io.Source
11+
import java.io.{FileWriter, File}
1012

1113

1214
/** The trait representing the main API entry point of the Scalatron server. */
@@ -70,7 +72,7 @@ trait Scalatron
7072
* @throws ScalatronException.CreationFailure if the user could not be created (e.g. because /src dir could not be created).
7173
* @throws IOError if the user's initial source files could not be written to disk.
7274
*/
73-
def createUser(name: String, password: String, initialSourceFiles: Iterable[SourceFile]): User
75+
def createUser(name: String, password: String, initialSourceFiles: SourceFileCollection): User
7476

7577
/** Checks if the given string would represent a valid user name by examining the characters
7678
* in the string. A variety of characters are not allowed primarily for the following reasons:
@@ -98,7 +100,7 @@ trait Scalatron
98100

99101
/** Creates a new sample with the given name from the given source file collection.
100102
* Users can employ this to share bot code across the network. */
101-
def createSample(name: String, sourceFiles: Iterable[SourceFile]): Sample
103+
def createSample(name: String, sourceFiles: SourceFileCollection): Sample
102104

103105

104106
//----------------------------------------------------------------------------------------------
@@ -235,7 +237,7 @@ object Scalatron {
235237
* @throws IllegalStateException if the source directory does not exist.
236238
* @throws IOError if the source files could not be read.
237239
*/
238-
def sourceFiles : Iterable[SourceFile]
240+
def sourceFiles : SourceFileCollection
239241

240242
/** Updates the source code of the given user to the given source files.
241243
* The updated source file(s) are written to e.g. "/Scalatron/users/{user}/src/".
@@ -245,7 +247,7 @@ object Scalatron {
245247
* before instructing it to build them.
246248
* @throws IOError if the source files could not be written.
247249
* */
248-
def updateSourceFiles(sourceFiles: Iterable[SourceFile])
250+
def updateSourceFiles(sourceFiles: SourceFileCollection)
249251

250252
/** Builds a local (unpublished) .jar bot plug-in from the given (in-memory) source files.
251253
*
@@ -262,7 +264,7 @@ object Scalatron {
262264
* @throws IllegalStateException if compilation service is unavailable, sources don't exist etc.
263265
* @throws IOError if source files cannot be read from disk, etc.
264266
*/
265-
def buildSourceFiles(sourceFiles: Iterable[SourceFile]): BuildResult
267+
def buildSourceFiles(sourceFiles: SourceFileCollection): BuildResult
266268

267269
/** Builds a local (unpublished) .jar bot plug-in from the sources currently present in
268270
* the user's workspace.
@@ -316,7 +318,7 @@ object Scalatron {
316318
* @throws IllegalStateException if version (base) directory could not be created
317319
* @throws IOError if source files cannot be written to disk, etc.
318320
* */
319-
def createVersion(label: String, sourceFiles: Iterable[SourceFile]): Version
321+
def createVersion(label: String, sourceFiles: SourceFileCollection): Version
320322

321323
/** Creates a new version by storing a backup copy of the source files currently present in the source
322324
* code directory of the user if the given version creation policy requires it. This method is intended as
@@ -332,7 +334,7 @@ object Scalatron {
332334
* directory could not be read.
333335
* @throws IOError if source files cannot be written to disk, etc.
334336
* */
335-
def createBackupVersion(policy: VersionPolicy, label: String, sourceFiles: Iterable[SourceFile]): Option[Version]
337+
def createBackupVersion(policy: VersionPolicy, label: String, sourceFiles: SourceFileCollection): Option[Version]
336338

337339

338340
//----------------------------------------------------------------------------------------------
@@ -368,6 +370,7 @@ object Scalatron {
368370
}
369371

370372

373+
371374
/** Scalatron.SourceFile: trait that provides an interface for dealing with source code
372375
* files handed through the API.
373376
* CBB: do we need to specify some particular kind of encoding?
@@ -381,6 +384,88 @@ object Scalatron {
381384
}
382385

383386

387+
/** Type alias for a collection of source files; each holds a filename and code. */
388+
type SourceFileCollection = Iterable[SourceFile]
389+
390+
/** Utility methods for working with collections of source files. */
391+
object SourceFileCollection
392+
{
393+
/** Returns an initial source file collection that incorporates the given user name.
394+
* @param userName the name of the user to generate an initial source file collection for.
395+
* @return a non-empty collection of SourceFile objects
396+
*/
397+
def initial(userName: String): SourceFileCollection =
398+
Iterable(SourceFile(
399+
"Bot.scala",
400+
"// this is the source code for your bot - have fun!\n" +
401+
"\n" +
402+
"class ControlFunctionFactory {\n" +
403+
" def create = new Bot().respond _\n" +
404+
"}\n" +
405+
"\n" +
406+
"class Bot {\n" +
407+
" def respond(input: String) = \"Status(text=" + userName + ")\"\n" +
408+
"}\n"
409+
))
410+
411+
/** Returns a collection of SourceFile objects representing source code files residing in the given directory.
412+
* Excludes directories and files that have the same name as the config files ("config.txt").
413+
* If the directory does not exist, is empty or does not contain valid files, an empty collection is returned.
414+
* @param directoryPath the directory to scan for source files.
415+
* @param verbose if true, information about the files read is logged to the console.
416+
* @return a collection of SourceFile objects; may be empty
417+
* @throws IOError on IO errors encountered while loading source file contents from disk
418+
*/
419+
def loadFrom(directoryPath: String, verbose: Boolean = false): SourceFileCollection = {
420+
val directory = new File(directoryPath)
421+
if(!directory.exists) {
422+
System.err.println("warning: directory expected to contain source files does not exist: %s".format(directoryPath))
423+
Iterable.empty
424+
}
425+
else
426+
{
427+
val sourceFiles = directory.listFiles()
428+
if( sourceFiles == null || sourceFiles.isEmpty ) {
429+
// no source files there!
430+
Iterable.empty
431+
} else {
432+
// read whatever is on disk now
433+
sourceFiles
434+
.filter(file => file.isFile && file.getName != Constants.ConfigFilename)
435+
.map(file => {
436+
val filename = file.getName
437+
val code = Source.fromFile(file).mkString
438+
if(verbose) println("loaded source code from file: '%s'".format(file.getAbsolutePath))
439+
Scalatron.SourceFile(filename, code)
440+
})
441+
}
442+
}
443+
}
444+
445+
446+
/** Writes the given collection of source files into the given directory, which must exist
447+
* and should be empty. Throws an exception on IO errors.
448+
* @param directoryPath the directory to write the source files to.
449+
* @param sourceFileCollection the collection of source files to write to disk.
450+
* @param verbose if true, information about the written files is logged to the console.
451+
* @throws IOError on IO errors encountered while writing source file contents to disk.
452+
*/
453+
def writeTo(directoryPath: String, sourceFileCollection: SourceFileCollection, verbose: Boolean = false) {
454+
sourceFileCollection.foreach(sf => {
455+
val path = directoryPath + "/" + sf.filename
456+
val sourceFile = new FileWriter(path)
457+
sourceFile.append(sf.code)
458+
sourceFile.close()
459+
if(verbose) println("wrote source file: " + path)
460+
})
461+
}
462+
}
463+
464+
465+
466+
467+
468+
384469
/** Policy used for optional version generation (e.g. when uploading new source files) that dictates whether
385470
* and under which circumstances a backup version should be generated.
386471
*/
@@ -424,7 +509,7 @@ object Scalatron {
424509
def name: String
425510

426511
/** Returns the source code files associated with this sample. */
427-
def sourceFiles: Iterable[SourceFile]
512+
def sourceFiles: SourceFileCollection
428513

429514
/** Deletes this sample, including all associated source code files. */
430515
def delete()
@@ -448,7 +533,7 @@ object Scalatron {
448533
def user: User
449534

450535
/** Returns the source code files associated with this version. */
451-
def sourceFiles: Iterable[SourceFile]
536+
def sourceFiles: SourceFileCollection
452537

453538
/** Deletes this version, including all associated source code files. */
454539
def delete()
@@ -551,20 +636,6 @@ object Scalatron {
551636

552637
val JarFilename = "ScalatronBot.jar"
553638
val BackupJarFilename = "ScalatronBot.backup.jar"
554-
555-
def initialSources(userName: String): Iterable[SourceFile] =
556-
Iterable(SourceFile(
557-
"Bot.scala",
558-
"// this is the source code for your bot - have fun!\n" +
559-
"\n" +
560-
"class ControlFunctionFactory {\n" +
561-
" def create = new Bot().respond _\n" +
562-
"}\n" +
563-
"\n" +
564-
"class Bot {\n" +
565-
" def respond(input: String) = \"Status(text=" + userName + ")\"\n" +
566-
"}\n"
567-
))
568639
}
569640

570641

Scalatron/src/scalatron/scalatron/api/ScalatronDemo.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
package scalatron.scalatron.api
55

66
import scalatron.scalatron.api.Scalatron.Constants._
7-
import scalatron.scalatron.impl.ScalatronUser
87
import java.io.{IOException, File}
98
import akka.actor.ActorSystem
9+
import scalatron.scalatron.impl.{FileUtil, ScalatronUser}
1010

1111

1212
object ScalatronDemo
@@ -233,7 +233,7 @@ object ScalatronDemo
233233

234234
} finally {
235235
// delete the temporary directory
236-
ScalatronUser.deleteRecursively(tmpDirPath)
236+
FileUtil.deleteRecursively(tmpDirPath)
237237
}
238238
}
239239
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package scalatron.scalatron.impl
2+
3+
import java.io.{File, FileWriter}
4+
import scala.io.Source
5+
import FileUtil.use
6+
7+
8+
object ConfigFile
9+
{
10+
/** Loads and parses a file with one key/val pair per line to Map[String,String].
11+
* Throws an exception if an error occurs. */
12+
def loadConfigFile(absolutePath: String) =
13+
Source
14+
.fromFile(absolutePath)
15+
.getLines()
16+
.map(_.split('='))
17+
.map(a => if( a.length == 2 ) (a(0), a(1)) else (a(0), ""))
18+
.toMap
19+
20+
21+
/** Loads, parses, updates and writes back a file with one key/val pair per line. */
22+
def updateConfigFile(absolutePath: String, key: String, value: String) {
23+
updateConfigFileMulti(absolutePath, Map(( key -> value )))
24+
}
25+
26+
/** Loads, parses, updates and writes back a file with one key/val pair per line. */
27+
def updateConfigFileMulti(absolutePath: String, kvMap: Map[String, String]) {
28+
val updatedMap =
29+
if( new File(absolutePath).exists() ) {
30+
val paramMap = loadConfigFile(absolutePath)
31+
paramMap ++ kvMap
32+
} else {
33+
kvMap
34+
}
35+
36+
use(new FileWriter(absolutePath)) {
37+
fileWriter =>
38+
updatedMap.foreach(entry =>
39+
fileWriter.append(entry._1 + "=" + entry._2 + "\n"))
40+
}
41+
}
42+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package scalatron.scalatron.impl
2+
3+
import java.io.{FileInputStream, FileOutputStream, File}
4+
5+
6+
object FileUtil
7+
{
8+
/** Manages a closeable resource by applying the given closure to it, then closing the resource.
9+
* @param closable the closeable resource.
10+
* @param block the closure to execute with the resource.
11+
* @tparam T the resource type.
12+
*/
13+
def use[T <: {def close()}](closable: T)(block: T => Unit) {
14+
try {
15+
block(closable)
16+
}
17+
finally {
18+
closable.close()
19+
}
20+
}
21+
22+
23+
/** code from http://stackoverflow.com/a/3028853
24+
* Using a later Java version (1.7?) would make this neater.
25+
* @throws IOError if there is a problem reading/writing the files
26+
*/
27+
def copyFile(from: String, to: String) {
28+
use(new FileInputStream(from)) {
29+
in =>
30+
use(new FileOutputStream(to)) {
31+
out =>
32+
val buffer = new Array[Byte](1024)
33+
Iterator.continually(in.read(buffer))
34+
.takeWhile(_ != -1)
35+
.foreach {out.write(buffer, 0, _)}
36+
}
37+
}
38+
}
39+
40+
41+
/** Recursively deletes the given directory and all of its contents (CAUTION!)
42+
* @throws IllegalStateException if there is a problem deleting a file or directory
43+
*/
44+
def deleteRecursively(directoryPath: String, verbose: Boolean = false) {
45+
val directory = new File(directoryPath)
46+
if( directory.exists ) {
47+
// caller handles exceptions
48+
if( verbose ) println(" deleting contents of directory at: " + directoryPath)
49+
if( directory.isDirectory ) {
50+
val filesInsideUserDir = directory.listFiles()
51+
if( filesInsideUserDir != null ) {
52+
filesInsideUserDir.foreach(file => deleteRecursively(file.getAbsolutePath, verbose))
53+
}
54+
if( verbose ) println(" deleting directory: " + directoryPath)
55+
if( !directory.delete() )
56+
throw new IllegalStateException("failed to delete directory at: " + directoryPath)
57+
} else {
58+
if( !directory.delete() )
59+
throw new IllegalStateException("failed to delete file: " + directory.getAbsolutePath)
60+
}
61+
}
62+
}
63+
}

Scalatron/src/scalatron/scalatron/impl/ScalatronImpl.scala

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@ import akka.actor._
88
import scalatron.botwar.BotWar
99
import scalatron.scalatron.api.Scalatron.Constants._
1010
import scalatron.scalatron.api.Scalatron
11-
import scalatron.scalatron.impl.ScalatronUser.writeSourceFiles
1211
import scalatron.Version
13-
import scalatron.scalatron.api.Scalatron.{ScalatronException, SourceFile, User}
1412
import java.text.DateFormat
1513
import java.util.Date
14+
import scalatron.scalatron.api.Scalatron.{SourceFileCollection, ScalatronException, SourceFile, User}
1615

1716

1817
object ScalatronImpl
@@ -256,7 +255,7 @@ case class ScalatronImpl(
256255
val adminUserConfigFile = new File(adminUserConfigFilePath)
257256
try {
258257
if(!adminUserConfigFile.exists()) {
259-
ScalatronUser.updateConfigFileMulti(
258+
ConfigFile.updateConfigFileMulti(
260259
adminUserConfigFilePath,
261260
Map(
262261
"password" -> Scalatron.Constants.AdminDefaultPassword,
@@ -335,7 +334,7 @@ case class ScalatronImpl(
335334
System.err.println("configuration file for user '" + name + "' already exists")
336335
throw ScalatronException.Exists(name + ": configuration file")
337336
} else {
338-
ScalatronUser.updateConfigFileMulti(
337+
ConfigFile.updateConfigFileMulti(
339338
userConfigFilePath,
340339
Map(
341340
"password" -> password,
@@ -428,7 +427,7 @@ case class ScalatronImpl(
428427
throw new IllegalStateException("could not create sample source directory: " + sampleDirectoryPath)
429428
}
430429
if(verbose) println("created sample source directory: " + sampleDirectoryPath)
431-
writeSourceFiles(sampleSourceDirectoryPath, sourceFiles, verbose)
430+
SourceFileCollection.writeTo(sampleSourceDirectoryPath, sourceFiles, verbose)
432431

433432
// future: write documentation file(s) to disk
434433
// future: write bot file

0 commit comments

Comments
 (0)