/**
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.appjet.oui;
import net.appjet.bodylock.{BodyLock, Executable};
import net.appjet.common.util.BetterFile;
import java.io.{File, FileNotFoundException, FileInputStream, IOException, ByteArrayInputStream};
import java.util.concurrent.ConcurrentHashMap;
import java.util.WeakHashMap;
import scala.collection.mutable.{Subscriber, Message, Reset=>SReset};
import scala.collection.jcl.Conversions._;
trait WeakPublisher[A, This <: WeakPublisher[A, This]] { self: This =>
val subscribers = new WeakHashMap[Subscriber[A, This], Unit];
protected def publish(event: A): Unit = {
subscribers.synchronized {
val subsCopy = for (sub <- subscribers.keySet()) yield sub;
for (sub <- subsCopy) {
sub.notify(this, event);
}
}
}
def subscribe(sub: Subscriber[A, This]): Unit = {
subscribers.synchronized {
subscribers.put(sub, ());
}
}
def removeSubscription(sub: Subscriber[A, This]): Unit = {
subscribers.synchronized {
subscribers.remove(sub);
}
}
}
object Reset extends SReset[Unit];
object FileCache {
val files = new ConcurrentHashMap[String, CachedFile];
def file(path: String): CachedFile = {
if (files.containsKey(path)) {
files.get(path);
} else {
val f = new CachedFile(new File(path));
val oldFile = files.putIfAbsent(path, f);
if (oldFile != null) {
oldFile
} else {
f
}
}
}
def file(path: String, subscriber: Subscriber[Message[Unit], CachedFile]): CachedFile = {
val f = file(path);
f.subscribe(subscriber);
f;
}
def testFiles() = {
val iter = files.values().iterator();
var filesHaveChanged = false;
while (iter.hasNext()) {
if (iter.next().test()) {
filesHaveChanged = true;
}
}
filesHaveChanged;
}
}
class CachedFile(f: File) extends WeakPublisher[Message[Unit], CachedFile] {
var cachedContent: Option[Array[Byte]] = None;
def content = synchronized {
if (cachedContent.isEmpty) {
cachedContent = Some(BetterFile.getFileBytes(f));
}
cachedContent.get;
}
def stream = new ByteArrayInputStream(content);
var cachedExistence: Option[Boolean] = None;
def exists = synchronized {
if (cachedExistence.isEmpty) {
cachedExistence = Some(f.exists());
}
cachedExistence.get;
}
var cachedDirectory: Option[Boolean] = None;
def isDirectory = synchronized {
if (cachedDirectory.isEmpty) {
cachedDirectory = Some(f.isDirectory());
}
cachedDirectory.get;
}
def underlyingLastModified = f.lastModified;
var lastModified = underlyingLastModified;
def hasBeenModified = underlyingLastModified != lastModified;
def test() = synchronized {
if (hasBeenModified) {
reset;
true;
} else {
false;
}
}
def reset = synchronized {
lastModified = underlyingLastModified;
cachedContent = None;
cachedExistence = None;
cachedDirectory = None;
publish(Reset);
}
}
class SpecialJarOrNotFile(root: String, fname: String) extends JarOrNotFile(root, fname) {
override val classBase = "/net/appjet/ajstdlib/";
override val fileSep = "/../";
override def clone(fname: String) = new SpecialJarOrNotFile(root, fname);
}
// A JarOrNotFile that reads from the /mirror directory in the classpath.
class MirroredJarOrNotFile(root: String, fname: String) extends JarOrNotFile(root, fname) {
override val classBase = "/mirror/";
override def clone(fname: String) = new MirroredJarOrNotFile(root, fname);
}
class JarVirtualFile(fname: String) extends MirroredJarOrNotFile(config.useVirtualFileRoot, fname);
class JarOrNotFile(root: String, fname: String) extends Subscriber[Message[Unit], CachedFile] with WeakPublisher[Message[Unit], JarOrNotFile] {
val classBase = "/net/appjet/ajstdlib/modules/";
val fileSep = "/";
val isJar = (root == null);
val streamBase = if (isJar) getClass().getResource((classBase+fname).replaceAll("/+", "/")) else null;
val file = if (! isJar) FileCache.file(root+fileSep+fname, this) else null;
def openStream() = {
if (isJar) streamBase.openStream;
else file.stream;
}
def exists = {
if (isJar) streamBase != null;
else file.exists;
}
def isDirectory = if (isJar) false else file.isDirectory;
lazy val streamModified = streamBase.openConnection().getLastModified();
def lastModified = {
if (isJar) streamModified;
else file.lastModified;
}
def name = fname;
override def toString() =
getClass.getName+": "+hashCode()+"; fname: "+fname+"; streambase: "+streamBase+"; file: "+file+(if (isJar) " from: "+classBase+fname else "");
// override def equals(o: AnyRef) =
// o match {
// case jf: JarOrNotFile => {
// classBase == jf.classBase &&
// fileSep == jf.fileSep &&
// root == jf.root &&
// fname == jf.fname
// }
// case _ => false
// }
// override def hashCode() =
// classBase.hashCode + fileSep.hashCode + root.hashCode + fname.hashCode
def notify(pub: CachedFile, event: Message[Unit]) = synchronized {
publish(event);
}
def clone(fname: String) = new JarOrNotFile(root, fname);
}
abstract class AutoUpdateFile(val fname: String) extends Subscriber[Message[Unit], JarOrNotFile] {
def files: Array[JarOrNotFile]; // = config.moduleRoots.map(f => new JarOrNotFile(f, libName));
def exists = files.exists(_.exists);
def file = files.find(_.exists).getOrElse(null);
def fileLastModified = if (exists) file.lastModified else 0L;
// var lastModified = fileLastModified;
// var cachedContents: Option[String] = None;
def fail(): Nothing = {
throw new FileNotFoundException("No such module: "+fname);
}
// def hasBeenModified = {
// if (exists) {
// val newModTime = try {
// fileLastModified
// } catch {
// case e: NoSuchElementException => fail();
// case e: NullPointerException => fail();
// }
// newModTime > lastModified;
// } else {
// false;
// }
// }
// def update() = synchronized {
// try {
// lastModified = fileLastModified;
// val contents = BetterFile.getStreamContents(file.openStream()).replace("\r\n", "\n").replace("\r", "\n");
// if (contents == null) {
// fail();
// }
// cachedContents = Some(contents);
// } catch {
// case e: IOException => {
// exceptionlog(e);
// e.printStackTrace();
// fail();
// }
// }
// }
def notify(pub: JarOrNotFile, event: Message[Unit]) {
event match {
case Reset => cachedContents = None;
}
}
var cachedContents: Option[String] = None;
def update() = synchronized {
if (cachedContents.isEmpty) {
cachedContents = Some(BetterFile.getStreamContents(file.openStream()).replace("\r\n", "\n").replace("\r", "\n"));
}
}
def contents = synchronized {
update();
cachedContents.get;
}
override def toString() = "[AutoUpdateFile: "+fname+"]";
}
class FixedDiskResource(srcfile: JarOrNotFile) extends AutoUpdateFile(srcfile.name) {
lazy val files0 = Array(srcfile);
files0.foreach(_.subscribe(this));
override def files = files0;
}
abstract class DiskLibrary(fname: String) extends AutoUpdateFile(fname) {
var cachedExecutable: Option[Executable] = None;
lazy val classFiles = files.map({ f =>
val parts = f.name.split("/");
val pathIfAny = parts.reverse.drop(1).reverse.mkString("/");
val newFname =
if (pathIfAny == "")
className(f.name);
else
pathIfAny+"/"+className(parts.last);
val newFile = f.clone(newFname+".class");
newFile.subscribe(this);
newFile;
});
def className(fname: String): String = "JS$"+fname.split("\\.").reverse.drop(1).reverse.mkString(".").replaceAll("[^A-Za-z0-9]", "\\$");
def className: String = classFile.name.split("\\.").reverse.drop(1).reverse.mkString(".");
override def exists = super.exists || classFiles.exists(_.exists);
override def file = if (super.exists) super.file else classFile;
def classFile = classFiles.find(_.exists).getOrElse(null);
// println("Made DiskLibrary on "+fname+", with classFile: "+classFile);
def updateExecutable() = synchronized {
if (classFile == null)
super.update();
if (cachedExecutable.isEmpty) {
try {
if (classFile != null) {
cachedExecutable = Some(BodyLock.executableFromBytes(BetterFile.getStreamBytes(classFile.openStream()), className.split("/").last));
} else {
cachedExecutable = Some(BodyLock.compileString(contents, "module "+fname, 1));
}
} catch {
case e => { cachedExecutable = None; throw e; }
}
}
}
def executable = synchronized {
updateExecutable();
cachedExecutable.get
}
override def notify(pub: JarOrNotFile, event: Message[Unit]) = synchronized {
super.notify(pub, event);
event match {
case Reset => cachedExecutable = None;
}
}
override def equals(o: Any) =
o match {
case dl: DiskLibrary => {
getClass.getName == dl.getClass.getName &&
fname == dl.fname
}
case _ => false;
}
override def hashCode() =
getClass.getName.hashCode + fname.hashCode
}
class FixedDiskLibrary(srcfile: JarOrNotFile) extends DiskLibrary(srcfile.name) {
lazy val files0 = Array(srcfile);
files0.foreach(_.subscribe(this));
override def files = files0;
}
class VariableDiskLibrary(libName: String) extends DiskLibrary(libName) {
lazy val files0 =
Array.concat(Array(new MirroredJarOrNotFile(null, libName)),
config.moduleRoots.map(f => new JarOrNotFile(f, libName)))
files0.foreach(_.subscribe(this));
override def files = files0;
}