diff --git a/akka-actor-tests/src/test/java/akka/actor/JavaAPI.java b/akka-actor-tests/src/test/java/akka/actor/JavaAPI.java index d0ef5104cd..f8600f3e1b 100644 --- a/akka-actor-tests/src/test/java/akka/actor/JavaAPI.java +++ b/akka-actor-tests/src/test/java/akka/actor/JavaAPI.java @@ -2,21 +2,36 @@ package akka.actor; import akka.actor.ActorSystem; import akka.japi.Creator; +import akka.testkit.AkkaSpec; + +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.Test; import static org.junit.Assert.*; public class JavaAPI { - private ActorSystem system = ActorSystem.create(); + private static ActorSystem system; + + @BeforeClass + public static void beforeAll() { + system = ActorSystem.create("JavaAPI", AkkaSpec.testConf()); + } + + @AfterClass + public static void afterAll() { + system.stop(); + system = null; + } @Test - void mustBeAbleToCreateActorRefFromClass() { + public void mustBeAbleToCreateActorRefFromClass() { ActorRef ref = system.actorOf(JavaAPITestActor.class); assertNotNull(ref); } @Test - void mustBeAbleToCreateActorRefFromFactory() { + public void mustBeAbleToCreateActorRefFromFactory() { ActorRef ref = system.actorOf(new Props().withCreator(new Creator() { public Actor create() { return new JavaAPITestActor(); @@ -26,7 +41,7 @@ public class JavaAPI { } @Test - void mustAcceptSingleArgTell() { + public void mustAcceptSingleArgTell() { ActorRef ref = system.actorOf(JavaAPITestActor.class); ref.tell("hallo"); ref.tell("hallo", ref); diff --git a/akka-actor-tests/src/test/java/akka/actor/JavaExtension.java b/akka-actor-tests/src/test/java/akka/actor/JavaExtension.java index 43aade632e..0a994b93d6 100644 --- a/akka-actor-tests/src/test/java/akka/actor/JavaExtension.java +++ b/akka-actor-tests/src/test/java/akka/actor/JavaExtension.java @@ -3,18 +3,23 @@ */ package akka.actor; +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.Test; +import akka.testkit.AkkaSpec; + import com.typesafe.config.ConfigFactory; import com.typesafe.config.Config; -import com.typesafe.config.ConfigParseOptions; import static org.junit.Assert.*; public class JavaExtension { static class Provider implements ExtensionIdProvider { - public ExtensionId lookup() { return defaultInstance; } + public ExtensionId lookup() { + return defaultInstance; + } } public final static TestExtensionId defaultInstance = new TestExtensionId(); @@ -26,10 +31,11 @@ public class JavaExtension { } static class TestExtension implements Extension { - public final ActorSystemImpl system; - public TestExtension(ActorSystemImpl i) { - system = i; - } + public final ActorSystemImpl system; + + public TestExtension(ActorSystemImpl i) { + system = i; + } } static class OtherExtension implements Extension { @@ -41,10 +47,20 @@ public class JavaExtension { } } - private Config c = ConfigFactory.parseString("akka.extensions = [ \"akka.actor.JavaExtension$Provider\" ]", - ConfigParseOptions.defaults()); + private static ActorSystem system; - private ActorSystem system = ActorSystem.create("JavaExtension", c); + @BeforeClass + public static void beforeAll() { + Config c = ConfigFactory.parseString("akka.extensions = [ \"akka.actor.JavaExtension$Provider\" ]").withFallback( + AkkaSpec.testConf()); + system = ActorSystem.create("JavaExtension", c); + } + + @AfterClass + public static void afterAll() { + system.stop(); + system = null; + } @Test public void mustBeAccessible() { diff --git a/akka-actor-tests/src/test/java/akka/dispatch/JavaFutureTests.java b/akka-actor-tests/src/test/java/akka/dispatch/JavaFutureTests.java index 12dbe736d6..d534d87103 100644 --- a/akka-actor-tests/src/test/java/akka/dispatch/JavaFutureTests.java +++ b/akka-actor-tests/src/test/java/akka/dispatch/JavaFutureTests.java @@ -2,6 +2,9 @@ package akka.dispatch; import akka.actor.Timeout; import akka.actor.ActorSystem; + +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.Test; import static org.junit.Assert.*; import java.util.concurrent.Callable; @@ -14,15 +17,30 @@ import akka.japi.Function; import akka.japi.Function2; import akka.japi.Procedure; import akka.japi.Option; +import akka.testkit.AkkaSpec; public class JavaFutureTests { - private final ActorSystem system = ActorSystem.create(); - private final Timeout t = system.settings().ActorTimeout(); - private final FutureFactory ff = new FutureFactory(system.dispatcher(), t); + private static ActorSystem system; + private static FutureFactory ff; + private static Timeout t; + + @BeforeClass + public static void beforeAll() { + system = ActorSystem.create("JavaFutureTests", AkkaSpec.testConf()); + t = system.settings().ActorTimeout(); + ff = new FutureFactory(system.dispatcher(), t); + } + + @AfterClass + public static void afterAll() { + system.stop(); + system = null; + } @Test public void mustBeAbleToMapAFuture() { + Future f1 = ff.future(new Callable() { public String call() { return "Hello"; diff --git a/akka-actor-tests/src/test/java/akka/util/JavaDuration.java b/akka-actor-tests/src/test/java/akka/util/JavaDuration.java index 56e7f68bf6..5a833f5b51 100644 --- a/akka-actor-tests/src/test/java/akka/util/JavaDuration.java +++ b/akka-actor-tests/src/test/java/akka/util/JavaDuration.java @@ -7,7 +7,8 @@ import org.junit.Test; public class JavaDuration { - @Test void testCreation() { + @Test + public void testCreation() { final Duration fivesec = Duration.create(5, "seconds"); final Duration threemillis = Duration.parse("3 millis"); final Duration diff = fivesec.minus(threemillis); diff --git a/akka-actor-tests/src/test/scala/akka/actor/ActorSystemSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/ActorSystemSpec.scala index 3565cde2fb..6f8c364ff8 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/ActorSystemSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/ActorSystemSpec.scala @@ -9,17 +9,15 @@ import com.typesafe.config.ConfigFactory class JavaExtensionSpec extends JavaExtension with JUnitSuite -object ActorSystemSpec { - object TestExtension extends ExtensionId[TestExtension] with ExtensionIdProvider { - def lookup = this - def createExtension(s: ActorSystemImpl) = new TestExtension(s) - } - - class TestExtension(val system: ActorSystemImpl) extends Extension +object TestExtension extends ExtensionId[TestExtension] with ExtensionIdProvider { + def lookup = this + def createExtension(s: ActorSystemImpl) = new TestExtension(s) } -class ActorSystemSpec extends AkkaSpec("""akka.extensions = ["akka.actor.ActorSystemSpec$TestExtension$"]""") { - import ActorSystemSpec._ +// Dont't place inside ActorSystemSpec object, since it will not be garbage collected and reference to system remains +class TestExtension(val system: ActorSystemImpl) extends Extension + +class ActorSystemSpec extends AkkaSpec("""akka.extensions = ["akka.actor.TestExtension$"]""") { "An ActorSystem" must { diff --git a/akka-actor-tests/src/test/scala/akka/actor/Bench.scala b/akka-actor-tests/src/test/scala/akka/actor/Bench.scala index 8e0e3e61fd..52a18e0f3b 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/Bench.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/Bench.scala @@ -107,9 +107,10 @@ object Chameneos { def run { // System.setProperty("akka.config", "akka.conf") Chameneos.start = System.currentTimeMillis - ActorSystem().actorOf(new Mall(1000000, 4)) + val system = ActorSystem().actorOf(new Mall(1000000, 4)) Thread.sleep(10000) println("Elapsed: " + (end - start)) + system.stop() } def main(args: Array[String]): Unit = run diff --git a/akka-actor-tests/src/test/scala/akka/actor/ConsistencySpec.scala b/akka-actor-tests/src/test/scala/akka/actor/ConsistencySpec.scala new file mode 100644 index 0000000000..1118daff1c --- /dev/null +++ b/akka-actor-tests/src/test/scala/akka/actor/ConsistencySpec.scala @@ -0,0 +1,61 @@ +package akka.actor + +import akka.testkit.AkkaSpec +import akka.dispatch.UnboundedMailbox +import akka.util.duration._ + +object ConsistencySpec { + class CacheMisaligned(var value: Long, var padding1: Long, var padding2: Long, var padding3: Int) //Vars, no final fences + + class ConsistencyCheckingActor extends Actor { + var left = new CacheMisaligned(42, 0, 0, 0) //var + var right = new CacheMisaligned(0, 0, 0, 0) //var + var lastStep = -1L + def receive = { + case step: Long ⇒ + + if (lastStep != (step - 1)) + sender.tell("Test failed: Last step %s, this step %s".format(lastStep, step)) + + var shouldBeFortyTwo = left.value + right.value + if (shouldBeFortyTwo != 42) + sender ! "Test failed: 42 failed" + else { + left.value += 1 + right.value -= 1 + } + + lastStep = step + case "done" ⇒ sender ! "done"; self.stop() + } + } +} + +class ConsistencySpec extends AkkaSpec { + import ConsistencySpec._ + "The Akka actor model implementation" must { + "provide memory consistency" in { + val noOfActors = 7 + val dispatcher = system + .dispatcherFactory + .newDispatcher("consistency-dispatcher", 1, UnboundedMailbox()) + .withNewThreadPoolWithArrayBlockingQueueWithCapacityAndFairness(noOfActors, true) + .setCorePoolSize(10) + .setMaxPoolSize(10) + .setKeepAliveTimeInMillis(1) + .setAllowCoreThreadTimeout(true) + .build + + val props = Props[ConsistencyCheckingActor].withDispatcher(dispatcher) + val actors = Vector.fill(noOfActors)(system.actorOf(props)) + + for (i ← 0L until 600000L) { + actors.foreach(_.tell(i, testActor)) + } + + for (a ← actors) { a.tell("done", testActor) } + + for (a ← actors) expectMsg(5 minutes, "done") + } + } +} \ No newline at end of file diff --git a/akka-actor-tests/src/test/scala/akka/actor/DeathWatchSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/DeathWatchSpec.scala index 90e398e4cb..88b31f25d9 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/DeathWatchSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/DeathWatchSpec.scala @@ -11,6 +11,10 @@ import java.util.concurrent.atomic._ @org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) class DeathWatchSpec extends AkkaSpec with BeforeAndAfterEach with ImplicitSender { + def startWatching(target: ActorRef) = actorOf(Props(new Actor { + watch(target) + def receive = { case x ⇒ testActor forward x } + })) "The Death Watch" must { def expectTerminationOf(actorRef: ActorRef) = expectMsgPF(5 seconds, actorRef + ": Stopped or Already terminated when linking") { @@ -19,8 +23,7 @@ class DeathWatchSpec extends AkkaSpec with BeforeAndAfterEach with ImplicitSende "notify with one Terminated message when an Actor is stopped" in { val terminal = actorOf(Props(context ⇒ { case _ ⇒ })) - - testActor startsWatching terminal + startWatching(terminal) testActor ! "ping" expectMsg("ping") @@ -32,11 +35,7 @@ class DeathWatchSpec extends AkkaSpec with BeforeAndAfterEach with ImplicitSende "notify with all monitors with one Terminated message when an Actor is stopped" in { val terminal = actorOf(Props(context ⇒ { case _ ⇒ })) - val monitor1, monitor2, monitor3 = - actorOf(Props(new Actor { - watch(terminal) - def receive = { case t: Terminated ⇒ testActor ! t } - })) + val monitor1, monitor2, monitor3 = startWatching(terminal) terminal ! PoisonPill @@ -51,11 +50,7 @@ class DeathWatchSpec extends AkkaSpec with BeforeAndAfterEach with ImplicitSende "notify with _current_ monitors with one Terminated message when an Actor is stopped" in { val terminal = actorOf(Props(context ⇒ { case _ ⇒ })) - val monitor1, monitor3 = - actorOf(Props(new Actor { - watch(terminal) - def receive = { case t: Terminated ⇒ testActor ! t } - })) + val monitor1, monitor3 = startWatching(terminal) val monitor2 = actorOf(Props(new Actor { watch(terminal) unwatch(terminal) @@ -85,10 +80,7 @@ class DeathWatchSpec extends AkkaSpec with BeforeAndAfterEach with ImplicitSende val terminalProps = Props(context ⇒ { case x ⇒ context.sender ! x }) val terminal = (supervisor ? terminalProps).as[ActorRef].get - val monitor = actorOf(Props(new Actor { - watch(terminal) - def receive = { case t: Terminated ⇒ testActor ! t } - })) + val monitor = startWatching(terminal) terminal ! Kill terminal ! Kill @@ -113,9 +105,13 @@ class DeathWatchSpec extends AkkaSpec with BeforeAndAfterEach with ImplicitSende } })) - val failed, brother = (supervisor ? Props.empty).as[ActorRef].get - brother startsWatching failed - testActor startsWatching brother + val failed = (supervisor ? Props.empty).as[ActorRef].get + val brother = (supervisor ? Props(new Actor { + watch(failed) + def receive = Actor.emptyBehavior + })).as[ActorRef].get + + startWatching(brother) failed ! Kill val result = receiveWhile(3 seconds, messages = 3) { diff --git a/akka-actor-tests/src/test/scala/akka/actor/DeployerSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/DeployerSpec.scala index d9615a5831..a3481e1903 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/DeployerSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/DeployerSpec.scala @@ -160,9 +160,9 @@ class DeployerSpec extends AkkaSpec(DeployerSpec.deployerConf) { nr-of-instances = boom } } - """, ConfigParseOptions.defaults) + """, ConfigParseOptions.defaults).withFallback(AkkaSpec.testConf) - ActorSystem("invalid", invalidDeployerConf) + ActorSystem("invalid", invalidDeployerConf).stop() } } diff --git a/akka-actor-tests/src/test/scala/akka/actor/FSMActorSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/FSMActorSpec.scala index 1fedcd82e4..b73658a352 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/FSMActorSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/FSMActorSpec.scala @@ -195,40 +195,45 @@ class FSMActorSpec extends AkkaSpec(Map("akka.actor.debug.fsm" -> true)) with Im import scala.collection.JavaConverters._ val config = ConfigFactory.parseMap(Map("akka.loglevel" -> "DEBUG", "akka.actor.debug.fsm" -> true).asJava).withFallback(system.settings.config) - new TestKit(ActorSystem("fsmEvent", config)) { - EventFilter.debug(occurrences = 5) intercept { - val fsm = TestActorRef(new Actor with LoggingFSM[Int, Null] { - startWith(1, null) - when(1) { - case Ev("go") ⇒ - setTimer("t", Shutdown, 1.5 seconds, false) - goto(2) + val fsmEventSystem = ActorSystem("fsmEvent", config) + try { + new TestKit(fsmEventSystem) { + EventFilter.debug(occurrences = 5) intercept { + val fsm = TestActorRef(new Actor with LoggingFSM[Int, Null] { + startWith(1, null) + when(1) { + case Ev("go") ⇒ + setTimer("t", Shutdown, 1.5 seconds, false) + goto(2) + } + when(2) { + case Ev("stop") ⇒ + cancelTimer("t") + stop + } + onTermination { + case StopEvent(r, _, _) ⇒ testActor ! r + } + }) + val name = fsm.path.toString + system.eventStream.subscribe(testActor, classOf[Logging.Debug]) + fsm ! "go" + expectMsgPF(1 second, hint = "processing Event(go,null)") { + case Logging.Debug(`name`, s: String) if s.startsWith("processing Event(go,null) from Actor[") ⇒ true } - when(2) { - case Ev("stop") ⇒ - cancelTimer("t") - stop + expectMsg(1 second, Logging.Debug(name, "setting timer 't'/1500 milliseconds: Shutdown")) + expectMsg(1 second, Logging.Debug(name, "transition 1 -> 2")) + fsm ! "stop" + expectMsgPF(1 second, hint = "processing Event(stop,null)") { + case Logging.Debug(`name`, s: String) if s.startsWith("processing Event(stop,null) from Actor[") ⇒ true } - onTermination { - case StopEvent(r, _, _) ⇒ testActor ! r - } - }) - val name = fsm.path.toString - system.eventStream.subscribe(testActor, classOf[Logging.Debug]) - fsm ! "go" - expectMsgPF(1 second, hint = "processing Event(go,null)") { - case Logging.Debug(`name`, s: String) if s.startsWith("processing Event(go,null) from Actor[") ⇒ true + expectMsgAllOf(1 second, Logging.Debug(name, "canceling timer 't'"), Normal) + expectNoMsg(1 second) + system.eventStream.unsubscribe(testActor) } - expectMsg(1 second, Logging.Debug(name, "setting timer 't'/1500 milliseconds: Shutdown")) - expectMsg(1 second, Logging.Debug(name, "transition 1 -> 2")) - fsm ! "stop" - expectMsgPF(1 second, hint = "processing Event(stop,null)") { - case Logging.Debug(`name`, s: String) if s.startsWith("processing Event(stop,null) from Actor[") ⇒ true - } - expectMsgAllOf(1 second, Logging.Debug(name, "canceling timer 't'"), Normal) - expectNoMsg(1 second) - system.eventStream.unsubscribe(testActor) } + } finally { + fsmEventSystem.stop() } } diff --git a/akka-actor-tests/src/test/scala/akka/actor/FSMTransitionSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/FSMTransitionSpec.scala index 917a65ec25..fdeabd2a47 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/FSMTransitionSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/FSMTransitionSpec.scala @@ -58,7 +58,7 @@ class FSMTransitionSpec extends AkkaSpec with ImplicitSender { val forward = actorOf(new Forwarder(testActor)) val fsm = actorOf(new MyFSM(testActor)) val sup = actorOf(Props(new Actor { - self startsWatching fsm + watch(fsm) def receive = { case _ ⇒ } }).withFaultHandler(OneForOneStrategy(List(classOf[Throwable]), None, None))) diff --git a/akka-actor-tests/src/test/scala/akka/actor/JavaAPISpec.scala b/akka-actor-tests/src/test/scala/akka/actor/JavaAPISpec.scala new file mode 100644 index 0000000000..49fcc6d638 --- /dev/null +++ b/akka-actor-tests/src/test/scala/akka/actor/JavaAPISpec.scala @@ -0,0 +1,8 @@ +/** + * Copyright (C) 2009-2011 Typesafe Inc. + */ +package akka.actor + +import org.scalatest.junit.JUnitSuite + +class JavaAPISpec extends JavaAPI with JUnitSuite diff --git a/akka-actor-tests/src/test/scala/akka/actor/RestartStrategySpec.scala b/akka-actor-tests/src/test/scala/akka/actor/RestartStrategySpec.scala index f7ad0d34cb..dd9e9ac79f 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/RestartStrategySpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/RestartStrategySpec.scala @@ -206,7 +206,7 @@ class RestartStrategySpec extends AkkaSpec { val boss = actorOf(Props(new Actor { def receive = { - case p: Props ⇒ sender ! context.actorOf(p) + case p: Props ⇒ sender ! watch(context.actorOf(p)) case t: Terminated ⇒ maxNoOfRestartsLatch.open } }).withFaultHandler(OneForOneStrategy(List(classOf[Throwable]), None, Some(1000)))) @@ -228,8 +228,6 @@ class RestartStrategySpec extends AkkaSpec { }) val slave = (boss ? slaveProps).as[ActorRef].get - boss startsWatching slave - slave ! Ping slave ! Crash slave ! Ping diff --git a/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala b/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala index e3d2bf1eea..7b6299ab69 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/SupervisorHierarchySpec.scala @@ -52,8 +52,7 @@ class SupervisorHierarchySpec extends AkkaSpec { val countDownMessages = new CountDownLatch(1) val countDownMax = new CountDownLatch(1) val boss = actorOf(Props(new Actor { - val crasher = context.actorOf(Props(new CountDownActor(countDownMessages))) - self startsWatching crasher + val crasher = watch(context.actorOf(Props(new CountDownActor(countDownMessages)))) protected def receive = { case "killCrasher" ⇒ crasher ! Kill diff --git a/akka-actor-tests/src/test/scala/akka/actor/dispatch/ActorModelSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/dispatch/ActorModelSpec.scala index e21f965c51..f07ed9dfa1 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/dispatch/ActorModelSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/dispatch/ActorModelSpec.scala @@ -341,9 +341,11 @@ abstract class ActorModelSpec extends AkkaSpec { val cachedMessage = CountDownNStop(new CountDownLatch(num)) val stopLatch = new CountDownLatch(num) val waitTime = (30 seconds).dilated.toMillis - val boss = actorOf(Props(context ⇒ { - case "run" ⇒ for (_ ← 1 to num) (context.self startsWatching context.actorOf(props)) ! cachedMessage - case Terminated(child) ⇒ stopLatch.countDown() + val boss = actorOf(Props(new Actor { + def receive = { + case "run" ⇒ for (_ ← 1 to num) (watch(context.actorOf(props))) ! cachedMessage + case Terminated(child) ⇒ stopLatch.countDown() + } }).withDispatcher(system.dispatcherFactory.newPinnedDispatcher("boss"))) boss ! "run" try { @@ -492,3 +494,39 @@ class BalancingDispatcherModelSpec extends ActorModelSpec { } } } + +@org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) +class FJDispatcherModelSpec extends ActorModelSpec { + import ActorModelSpec._ + + def newInterceptedDispatcher = + (new Dispatcher(system.dispatcherFactory.prerequisites, "foo", system.settings.DispatcherThroughput, + system.settings.DispatcherThroughputDeadlineTime, system.dispatcherFactory.MailboxType, + new ForkJoinPoolConfig(), system.settings.DispatcherDefaultShutdown) with MessageDispatcherInterceptor).asInstanceOf[MessageDispatcherInterceptor] + + def dispatcherType = "FJDispatcher" + + "A " + dispatcherType must { + "process messages in parallel" in { + implicit val dispatcher = newInterceptedDispatcher + val aStart, aStop, bParallel = new CountDownLatch(1) + val a, b = newTestActor(dispatcher) + + a ! Meet(aStart, aStop) + assertCountDown(aStart, 3.seconds.dilated.toMillis, "Should process first message within 3 seconds") + + b ! CountDown(bParallel) + assertCountDown(bParallel, 3.seconds.dilated.toMillis, "Should process other actors in parallel") + + aStop.countDown() + + a.stop + b.stop + + while (!a.isTerminated && !b.isTerminated) {} //Busy wait for termination + + assertRefDefaultZero(a)(registers = 1, unregisters = 1, msgsReceived = 1, msgsProcessed = 1) + assertRefDefaultZero(b)(registers = 1, unregisters = 1, msgsReceived = 1, msgsProcessed = 1) + } + } +} \ No newline at end of file diff --git a/akka-actor-tests/src/test/scala/akka/actor/dispatch/DispatchersSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/dispatch/DispatchersSpec.scala index 53fa4ea5bf..d23bc8ce57 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/dispatch/DispatchersSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/dispatch/DispatchersSpec.scala @@ -9,7 +9,6 @@ import akka.dispatch._ import akka.testkit.AkkaSpec import scala.collection.JavaConverters._ import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigParseOptions @org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) class DispatchersSpec extends AkkaSpec { @@ -41,7 +40,7 @@ class DispatchersSpec extends AkkaSpec { throughput = 17 } } - """, ConfigParseOptions.defaults) + """) lazy val allDispatchers: Map[String, Option[MessageDispatcher]] = { validTypes.map(t ⇒ (t, from(ConfigFactory.parseMap(Map(tipe -> t).asJava).withFallback(defaultDispatcherConfig)))).toMap diff --git a/akka-actor-tests/src/test/scala/akka/config/ConfigSpec.scala b/akka-actor-tests/src/test/scala/akka/config/ConfigSpec.scala index b594ea0f19..80d1ca30e5 100644 --- a/akka-actor-tests/src/test/scala/akka/config/ConfigSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/config/ConfigSpec.scala @@ -6,13 +6,12 @@ package akka.config import akka.testkit.AkkaSpec import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigParseOptions import scala.collection.JavaConverters._ import akka.util.duration._ import akka.util.Duration @org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) -class ConfigSpec extends AkkaSpec(ConfigFactory.parseResource(classOf[ConfigSpec], "/akka-actor-reference.conf", ConfigParseOptions.defaults)) { +class ConfigSpec extends AkkaSpec(ConfigFactory.defaultReference) { "The default configuration file (i.e. akka-actor-reference.conf)" must { "contain all configuration properties for akka-actor that are used in code with their correct defaults" in { diff --git a/akka-actor-tests/src/test/scala/akka/event/EventStreamSpec.scala b/akka-actor-tests/src/test/scala/akka/event/EventStreamSpec.scala index 14158e7454..031fb1ccb3 100644 --- a/akka-actor-tests/src/test/scala/akka/event/EventStreamSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/event/EventStreamSpec.scala @@ -7,7 +7,6 @@ import akka.testkit.AkkaSpec import akka.util.duration._ import akka.actor.{ Actor, ActorRef, ActorSystemImpl } import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigParseOptions import scala.collection.JavaConverters._ import akka.actor.ActorSystem @@ -19,7 +18,7 @@ object EventStreamSpec { loglevel = INFO event-handlers = ["akka.event.EventStreamSpec$MyLog", "%s"] } - """.format(Logging.StandardOutLoggerName), ConfigParseOptions.defaults) + """.format(Logging.StandardOutLoggerName)) case class M(i: Int) diff --git a/akka-actor-tests/src/test/scala/akka/actor/LoggingReceiveSpec.scala b/akka-actor-tests/src/test/scala/akka/event/LoggingReceiveSpec.scala similarity index 93% rename from akka-actor-tests/src/test/scala/akka/actor/LoggingReceiveSpec.scala rename to akka-actor-tests/src/test/scala/akka/event/LoggingReceiveSpec.scala index b7c046a43b..0c3e82e7d0 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/LoggingReceiveSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/event/LoggingReceiveSpec.scala @@ -1,18 +1,26 @@ /** * Copyright (C) 2009-2011 Typesafe Inc. */ -package akka.actor +package akka.event import org.scalatest.{ BeforeAndAfterAll, BeforeAndAfterEach } import akka.util.duration._ import akka.testkit._ import org.scalatest.WordSpec -import akka.event.Logging import akka.util.Duration import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigParseOptions import scala.collection.JavaConverters._ import java.util.Properties +import akka.actor.Actor +import akka.actor.ActorSystem +import akka.actor.HotSwap +import akka.actor.UnhandledMessageException +import akka.actor.PoisonPill +import akka.actor.ActorSystemImpl +import akka.actor.Props +import akka.actor.OneForOneStrategy +import akka.actor.ActorKilledException +import akka.actor.Kill object LoggingReceiveSpec { class TestLogActor extends Actor { @@ -58,7 +66,7 @@ class LoggingReceiveSpec extends WordSpec with BeforeAndAfterEach with BeforeAnd val r: Actor.Receive = { case null ⇒ } - val log = Actor.LoggingReceive("funky", r) + val log = LoggingReceive("funky")(r) log.isDefinedAt("hallo") expectMsg(1 second, Logging.Debug("funky", "received unhandled message hallo")) } @@ -70,7 +78,7 @@ class LoggingReceiveSpec extends WordSpec with BeforeAndAfterEach with BeforeAnd system.eventStream.subscribe(testActor, classOf[Logging.Debug]) system.eventStream.subscribe(testActor, classOf[Logging.Error]) val actor = TestActorRef(new Actor { - def receive = loggable(this) { + def receive = LoggingReceive(this) { case x ⇒ sender ! "x" } @@ -100,7 +108,7 @@ class LoggingReceiveSpec extends WordSpec with BeforeAndAfterEach with BeforeAnd new TestKit(appLogging) with ImplicitSender { system.eventStream.subscribe(testActor, classOf[Logging.Debug]) val actor = TestActorRef(new Actor { - def receive = loggable(this)(loggable(this) { + def receive = LoggingReceive(this)(LoggingReceive(this) { case _ ⇒ sender ! "x" }) }) diff --git a/akka-actor-tests/src/test/scala/akka/performance/microbench/TellThroughput10000PerformanceSpec.scala b/akka-actor-tests/src/test/scala/akka/performance/microbench/TellThroughput10000PerformanceSpec.scala index e47d7987bd..1854a0a32e 100644 --- a/akka-actor-tests/src/test/scala/akka/performance/microbench/TellThroughput10000PerformanceSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/performance/microbench/TellThroughput10000PerformanceSpec.scala @@ -34,7 +34,7 @@ class TellThroughput10000PerformanceSpec extends PerformanceSpec { def createDispatcher(name: String) = ThreadPoolConfigDispatcherBuilder(config ⇒ new Dispatcher(system.dispatcherFactory.prerequisites, name, 10000, - Duration.Zero, UnboundedMailbox(), config, Duration(60, TimeUnit.SECONDS)), ThreadPoolConfig()) + Duration.Zero, UnboundedMailbox(), config, Duration(1, TimeUnit.SECONDS)), ThreadPoolConfig()) .withNewThreadPoolWithLinkedBlockingQueueWithUnboundedCapacity .setCorePoolSize(maxClients * 2) .build diff --git a/akka-actor-tests/src/test/scala/akka/performance/microbench/TellThroughputComputationPerformanceSpec.scala b/akka-actor-tests/src/test/scala/akka/performance/microbench/TellThroughputComputationPerformanceSpec.scala index 52bb3d169b..f5b7b3ae4d 100644 --- a/akka-actor-tests/src/test/scala/akka/performance/microbench/TellThroughputComputationPerformanceSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/performance/microbench/TellThroughputComputationPerformanceSpec.scala @@ -14,7 +14,7 @@ class TellThroughputComputationPerformanceSpec extends PerformanceSpec { def createDispatcher(name: String) = ThreadPoolConfigDispatcherBuilder(config ⇒ new Dispatcher(system.dispatcherFactory.prerequisites, name, 5, - Duration.Zero, UnboundedMailbox(), config, 60 seconds), ThreadPoolConfig()) + Duration.Zero, UnboundedMailbox(), config, 1 seconds), ThreadPoolConfig()) .withNewThreadPoolWithLinkedBlockingQueueWithUnboundedCapacity .setCorePoolSize(maxClients) .build diff --git a/akka-actor-tests/src/test/scala/akka/performance/microbench/TellThroughputPerformanceSpec.scala b/akka-actor-tests/src/test/scala/akka/performance/microbench/TellThroughputPerformanceSpec.scala index a49e837ac4..f2e547ab71 100644 --- a/akka-actor-tests/src/test/scala/akka/performance/microbench/TellThroughputPerformanceSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/performance/microbench/TellThroughputPerformanceSpec.scala @@ -14,7 +14,7 @@ class TellThroughputPerformanceSpec extends PerformanceSpec { def createDispatcher(name: String) = ThreadPoolConfigDispatcherBuilder(config ⇒ new Dispatcher(system.dispatcherFactory.prerequisites, name, 5, - Duration.Zero, UnboundedMailbox(), config, 60 seconds), ThreadPoolConfig()) + Duration.Zero, UnboundedMailbox(), config, 1 seconds), ThreadPoolConfig()) .withNewThreadPoolWithLinkedBlockingQueueWithUnboundedCapacity .setCorePoolSize(maxClients) .build diff --git a/akka-actor-tests/src/test/scala/akka/performance/microbench/TellThroughputSeparateDispatchersPerformanceSpec.scala b/akka-actor-tests/src/test/scala/akka/performance/microbench/TellThroughputSeparateDispatchersPerformanceSpec.scala index 0de1e1be2d..eb0eaffc46 100644 --- a/akka-actor-tests/src/test/scala/akka/performance/microbench/TellThroughputSeparateDispatchersPerformanceSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/performance/microbench/TellThroughputSeparateDispatchersPerformanceSpec.scala @@ -18,7 +18,7 @@ class TellThroughputSeparateDispatchersPerformanceSpec extends PerformanceSpec { def createDispatcher(name: String) = ThreadPoolConfigDispatcherBuilder(config ⇒ new Dispatcher(system.dispatcherFactory.prerequisites, name, 5, - Duration.Zero, UnboundedMailbox(), config, Duration(60, TimeUnit.SECONDS)), ThreadPoolConfig()) + Duration.Zero, UnboundedMailbox(), config, Duration(1, TimeUnit.SECONDS)), ThreadPoolConfig()) .withNewThreadPoolWithLinkedBlockingQueueWithUnboundedCapacity .setCorePoolSize(1) .build diff --git a/akka-actor-tests/src/test/scala/akka/performance/workbench/Report.scala b/akka-actor-tests/src/test/scala/akka/performance/workbench/Report.scala index 764afefe3c..49d3b23b42 100644 --- a/akka-actor-tests/src/test/scala/akka/performance/workbench/Report.scala +++ b/akka-actor-tests/src/test/scala/akka/performance/workbench/Report.scala @@ -222,7 +222,7 @@ class Report( sb.append("Akka version: ").append(system.settings.ConfigVersion) sb.append("\n") sb.append("Akka config:") - for ((key, value) ← system.settings.config.toObject) { + for ((key, value) ← system.settings.config.root) { sb.append("\n ").append(key).append("=").append(value) } diff --git a/akka-actor-tests/src/test/scala/akka/serialization/SerializeSpec.scala b/akka-actor-tests/src/test/scala/akka/serialization/SerializeSpec.scala index 0e2f43a3d8..499d214ff8 100644 --- a/akka-actor-tests/src/test/scala/akka/serialization/SerializeSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/serialization/SerializeSpec.scala @@ -11,7 +11,6 @@ import akka.actor.{ ActorSystem, ActorSystemImpl } import java.io.{ ObjectInputStream, ByteArrayInputStream, ByteArrayOutputStream, ObjectOutputStream } import akka.actor.DeadLetterActorRef import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigParseOptions object SerializeSpec { @@ -32,7 +31,7 @@ object SerializeSpec { } } } - """, ConfigParseOptions.defaults) + """) @BeanInfo case class Address(no: String, street: String, city: String, zip: String) { def this() = this("", "", "", "") } @@ -98,15 +97,19 @@ class SerializeSpec extends AkkaSpec(SerializeSpec.serializationConf) { "serialize DeadLetterActorRef" in { val outbuf = new ByteArrayOutputStream() val out = new ObjectOutputStream(outbuf) - val a = ActorSystem() - out.writeObject(a.deadLetters) - out.flush() - out.close() + val a = ActorSystem("SerializeDeadLeterActorRef", AkkaSpec.testConf) + try { + out.writeObject(a.deadLetters) + out.flush() + out.close() - val in = new ObjectInputStream(new ByteArrayInputStream(outbuf.toByteArray)) - Serialization.currentSystem.withValue(a.asInstanceOf[ActorSystemImpl]) { - val deadLetters = in.readObject().asInstanceOf[DeadLetterActorRef] - (deadLetters eq a.deadLetters) must be(true) + val in = new ObjectInputStream(new ByteArrayInputStream(outbuf.toByteArray)) + Serialization.currentSystem.withValue(a.asInstanceOf[ActorSystemImpl]) { + val deadLetters = in.readObject().asInstanceOf[DeadLetterActorRef] + (deadLetters eq a.deadLetters) must be(true) + } + } finally { + a.stop() } } } diff --git a/akka-actor-tests/src/test/scala/akka/util/JavaDurationSpec.scala b/akka-actor-tests/src/test/scala/akka/util/JavaDurationSpec.scala new file mode 100644 index 0000000000..aafbf3d133 --- /dev/null +++ b/akka-actor-tests/src/test/scala/akka/util/JavaDurationSpec.scala @@ -0,0 +1,8 @@ +/** + * Copyright (C) 2009-2011 Typesafe Inc. + */ +package akka.util + +import org.scalatest.junit.JUnitSuite + +class JavaDurationSpec extends JavaDuration with JUnitSuite diff --git a/akka-actor/src/main/java/akka/dispatch/AbstractMailbox.java b/akka-actor/src/main/java/akka/dispatch/AbstractMailbox.java index 80cc4c9675..dbe87482dc 100644 --- a/akka-actor/src/main/java/akka/dispatch/AbstractMailbox.java +++ b/akka-actor/src/main/java/akka/dispatch/AbstractMailbox.java @@ -12,8 +12,8 @@ final class AbstractMailbox { static { try { - mailboxStatusOffset = Unsafe.instance.objectFieldOffset(Mailbox.class.getDeclaredField("_status")); - systemMessageOffset = Unsafe.instance.objectFieldOffset(Mailbox.class.getDeclaredField("_systemQueue")); + mailboxStatusOffset = Unsafe.instance.objectFieldOffset(Mailbox.class.getDeclaredField("_statusDoNotCallMeDirectly")); + systemMessageOffset = Unsafe.instance.objectFieldOffset(Mailbox.class.getDeclaredField("_systemQueueDoNotCallMeDirectly")); } catch(Throwable t){ throw new ExceptionInInitializerError(t); } diff --git a/akka-actor/src/main/java/com/typesafe/config/Config.java b/akka-actor/src/main/java/com/typesafe/config/Config.java index 3a820033a8..1c7fca50e5 100644 --- a/akka-actor/src/main/java/com/typesafe/config/Config.java +++ b/akka-actor/src/main/java/com/typesafe/config/Config.java @@ -6,69 +6,239 @@ package com.typesafe.config; import java.util.List; /** - * This class represents an immutable map from config paths to config values. It - * also contains some static methods for creating configs. + * An immutable map from config paths to config values. * + *

+ * Contrast with {@link ConfigObject} which is a map from config keys, + * rather than paths, to config values. A {@code Config} contains a tree of + * {@code ConfigObject}, and {@link Config#root()} returns the tree's root + * object. + * + *

* Throughout the API, there is a distinction between "keys" and "paths". A key * is a key in a JSON object; it's just a string that's the key in a map. A * "path" is a parseable expression with a syntax and it refers to a series of - * keys. Path expressions are described in the spec for "HOCON", which can be - * found at https://github.com/havocp/config/blob/master/HOCON.md; in brief, a - * path is period-separated so "a.b.c" looks for key c in object b in object a - * in the root object. Sometimes double quotes are needed around special - * characters in path expressions. + * keys. Path expressions are described in the spec for + * Human-Optimized Config Object Notation. In brief, a path is + * period-separated so "a.b.c" looks for key c in object b in object a in the + * root object. Sometimes double quotes are needed around special characters in + * path expressions. * - * The API for a Config is in terms of path expressions, while the API for a - * ConfigObject is in terms of keys. Conceptually, Config is a one-level map - * from paths to values, while a ConfigObject is a tree of maps from keys to - * values. + *

+ * The API for a {@code Config} is in terms of path expressions, while the API + * for a {@code ConfigObject} is in terms of keys. Conceptually, {@code Config} + * is a one-level map from paths to values, while a + * {@code ConfigObject} is a tree of nested maps from keys to values. * - * Another difference between Config and ConfigObject is that conceptually, - * ConfigValue with valueType() of ConfigValueType.NULL exist in a ConfigObject, - * while a Config treats null values as if they were missing. + *

+ * Another difference between {@code Config} and {@code ConfigObject} is that + * conceptually, {@code ConfigValue}s with a {@link ConfigValue#valueType() + * valueType()} of {@link ConfigValueType#NULL NULL} exist in a + * {@code ConfigObject}, while a {@code Config} treats null values as if they + * were missing. * - * Config is an immutable object and thus safe to use from multiple threads. + *

+ * {@code Config} is an immutable object and thus safe to use from multiple + * threads. There's never a need for "defensive copies." * - * The "getters" on a Config all work in the same way. They never return null, - * nor do they return a ConfigValue with valueType() of ConfigValueType.NULL. - * Instead, they throw ConfigException.Missing if the value is completely absent - * or set to null. If the value is set to null, a subtype of - * ConfigException.Missing called ConfigException.Null will be thrown. - * ConfigException.WrongType will be thrown anytime you ask for a type and the - * value has an incompatible type. Reasonable type conversions are performed for - * you though. + *

+ * The "getters" on a {@code Config} all work in the same way. They never return + * null, nor do they return a {@code ConfigValue} with + * {@link ConfigValue#valueType() valueType()} of {@link ConfigValueType#NULL + * NULL}. Instead, they throw {@link ConfigException.Missing} if the value is + * completely absent or set to null. If the value is set to null, a subtype of + * {@code ConfigException.Missing} called {@link ConfigException.Null} will be + * thrown. {@link ConfigException.WrongType} will be thrown anytime you ask for + * a type and the value has an incompatible type. Reasonable type conversions + * are performed for you though. * - * If you want to iterate over the contents of a Config, you have to get its - * ConfigObject with toObject, and then iterate over the ConfigObject. + *

+ * If you want to iterate over the contents of a {@code Config}, you have to get + * its {@code ConfigObject} with {@link #root()}, and then iterate over the + * {@code ConfigObject}. * + * + *

+ * Do not implement {@code Config}; it should only be implemented by + * the config library. Arbitrary implementations will not work because the + * library internals assume a specific concrete implementation. Also, this + * interface is likely to grow new methods over time, so third-party + * implementations will break. */ public interface Config extends ConfigMergeable { /** - * Gets the config as a tree of ConfigObject. This is a constant-time - * operation (it is not proportional to the number of values in the Config). + * Gets the {@code Config} as a tree of {@link ConfigObject}. This is a + * constant-time operation (it is not proportional to the number of values + * in the {@code Config}). * - * @return + * @return the root object in the configuration */ - ConfigObject toObject(); + ConfigObject root(); + /** + * Gets the origin of the {@code Config}, which may be a file, or a file + * with a line number, or just a descriptive phrase. + * + * @return the origin of the {@code Config} for use in error messages + */ ConfigOrigin origin(); @Override Config withFallback(ConfigMergeable other); - @Override - ConfigObject toValue(); + /** + * Returns a replacement config with all substitutions (the + * ${foo.bar} syntax, see the + * spec) resolved. Substitutions are looked up using this + * Config as the root object, that is, a substitution + * ${foo.bar} will be replaced with the result of + * getValue("foo.bar"). + * + *

+ * This method uses {@link ConfigResolveOptions#defaults()}, there is + * another variant {@link Config#resolve(ConfigResolveOptions)} which lets + * you specify non-default options. + * + *

+ * A given {@link Config} must be resolved before using it to retrieve + * config values, but ideally should be resolved one time for your entire + * stack of fallbacks (see {@link Config#withFallback}). Otherwise, some + * substitutions that could have resolved with all fallbacks available may + * not resolve, which will be a user-visible oddity. + * + *

+ * resolve() should be invoked on root config objects, rather + * than on a subtree (a subtree is the result of something like + * config.getConfig("foo")). The problem with + * resolve() on a subtree is that substitutions are relative to + * the root of the config and the subtree will have no way to get values + * from the root. For example, if you did + * config.getConfig("foo").resolve() on the below config file, + * it would not work: + * + *

+     *   common-value = 10
+     *   foo {
+     *      whatever = ${common-value}
+     *   }
+     * 
+ * + * @return an immutable object with substitutions resolved + * @throws ConfigException.UnresolvedSubstitution + * if any substitutions refer to nonexistent paths + * @throws ConfigException + * some other config exception if there are other problems + */ + Config resolve(); + + /** + * Like {@link Config#resolve()} but allows you to specify non-default + * options. + * + * @param options + * resolve options + * @return the resolved Config + */ + Config resolve(ConfigResolveOptions options); + + /** + * Validates this config against a reference config, throwing an exception + * if it is invalid. The purpose of this method is to "fail early" with a + * comprehensive list of problems; in general, anything this method can find + * would be detected later when trying to use the config, but it's often + * more user-friendly to fail right away when loading the config. + * + *

+ * Using this method is always optional, since you can "fail late" instead. + * + *

+ * You must restrict validation to paths you "own" (those whose meaning are + * defined by your code module). If you validate globally, you may trigger + * errors about paths that happen to be in the config but have nothing to do + * with your module. It's best to allow the modules owning those paths to + * validate them. Also, if every module validates only its own stuff, there + * isn't as much redundant work being done. + * + *

+ * If no paths are specified in checkValid()'s parameter list, + * validation is for the entire config. + * + *

+ * If you specify paths that are not in the reference config, those paths + * are ignored. (There's nothing to validate.) + * + *

+ * Here's what validation involves: + * + *

    + *
  • All paths found in the reference config must be present in this + * config or an exception will be thrown. + *
  • + * Some changes in type from the reference config to this config will cause + * an exception to be thrown. Not all potential type problems are detected, + * in particular it's assumed that strings are compatible with everything + * except objects and lists. This is because string types are often "really" + * some other type (system properties always start out as strings, or a + * string like "5ms" could be used with {@link #getMilliseconds}). Also, + * it's allowed to set any type to null or override null with any type. + *
  • + * Any unresolved substitutions in this config will cause a validation + * failure; both the reference config and this config should be resolved + * before validation. If the reference config is unresolved, it's a bug in + * the caller of this method. + *
+ * + *

+ * If you want to allow a certain setting to have a flexible type (or + * otherwise want validation to be looser for some settings), you could + * either remove the problematic setting from the reference config provided + * to this method, or you could intercept the validation exception and + * screen out certain problems. Of course, this will only work if all other + * callers of this method are careful to restrict validation to their own + * paths, as they should be. + * + *

+ * If validation fails, the thrown exception contains a list of all problems + * found. See {@link ConfigException.ValidationFailed#problems}. The + * exception's getMessage() will have all the problems + * concatenated into one huge string, as well. + * + *

+ * Again, checkValid() can't guess every domain-specific way a + * setting can be invalid, so some problems may arise later when attempting + * to use the config. checkValid() is limited to reporting + * generic, but common, problems such as missing settings and blatant type + * incompatibilities. + * + * @param reference + * a reference configuration + * @param restrictToPaths + * only validate values underneath these paths that your code + * module owns and understands + * @throws ConfigException.ValidationFailed + * if there are any validation issues + * @throws ConfigException.NotResolved + * if this config is not resolved + * @throws ConfigException.BugOrBroken + * if the reference config is unresolved or caller otherwise + * misuses the API + */ + void checkValid(Config reference, String... restrictToPaths); /** * Checks whether a value is present and non-null at the given path. This - * differs in two ways from ConfigObject.containsKey(): it looks for a path - * expression, not a key; and it returns false for null values, while - * containsKey() returns true indicating that the object contains a null - * value for the key. + * differs in two ways from {@code Map.containsKey()} as implemented by + * {@link ConfigObject}: it looks for a path expression, not a key; and it + * returns false for null values, while {@code containsKey()} returns true + * indicating that the object contains a null value for the key. * - * If a path exists according to hasPath(), then getValue() will never throw - * an exception. However, the typed getters, such as getInt(), will still - * throw if the value is not convertible to the requested type. + *

+ * If a path exists according to {@link #hasPath(String)}, then + * {@link #getValue(String)} will never throw an exception. However, the + * typed getters, such as {@link #getInt(String)}, will still throw if the + * value is not convertible to the requested type. * * @param path * the path expression @@ -78,12 +248,19 @@ public interface Config extends ConfigMergeable { */ boolean hasPath(String path); + /** + * Returns true if the {@code Config}'s root object contains no key-value + * pairs. + * + * @return true if the configuration is empty + */ boolean isEmpty(); /** * * @param path - * @return + * path expression + * @return the boolean value at the requested path * @throws ConfigException.Missing * if value is absent or null * @throws ConfigException.WrongType @@ -93,7 +270,8 @@ public interface Config extends ConfigMergeable { /** * @param path - * @return + * path expression + * @return the numeric value at the requested path * @throws ConfigException.Missing * if value is absent or null * @throws ConfigException.WrongType @@ -103,17 +281,20 @@ public interface Config extends ConfigMergeable { /** * @param path - * @return + * path expression + * @return the 32-bit integer value at the requested path * @throws ConfigException.Missing * if value is absent or null * @throws ConfigException.WrongType - * if value is not convertible to an int + * if value is not convertible to an int (for example it is out + * of range, or it's a boolean value) */ int getInt(String path); /** * @param path - * @return + * path expression + * @return the 64-bit long value at the requested path * @throws ConfigException.Missing * if value is absent or null * @throws ConfigException.WrongType @@ -123,7 +304,8 @@ public interface Config extends ConfigMergeable { /** * @param path - * @return + * path expression + * @return the floating-point value at the requested path * @throws ConfigException.Missing * if value is absent or null * @throws ConfigException.WrongType @@ -133,7 +315,8 @@ public interface Config extends ConfigMergeable { /** * @param path - * @return + * path expression + * @return the string value at the requested path * @throws ConfigException.Missing * if value is absent or null * @throws ConfigException.WrongType @@ -143,7 +326,8 @@ public interface Config extends ConfigMergeable { /** * @param path - * @return + * path expression + * @return the {@link ConfigObject} value at the requested path * @throws ConfigException.Missing * if value is absent or null * @throws ConfigException.WrongType @@ -153,7 +337,8 @@ public interface Config extends ConfigMergeable { /** * @param path - * @return + * path expression + * @return the nested {@code Config} value at the requested path * @throws ConfigException.Missing * if value is absent or null * @throws ConfigException.WrongType @@ -162,9 +347,13 @@ public interface Config extends ConfigMergeable { Config getConfig(String path); /** - * Gets the value at the path as an unwrapped Java boxed value (Boolean, - * Integer, Long, etc.) + * Gets the value at the path as an unwrapped Java boxed value ( + * {@link java.lang.Boolean Boolean}, {@link java.lang.Integer Integer}, and + * so on - see {@link ConfigValue#unwrapped()}). * + * @param path + * path expression + * @return the unwrapped value at the requested path * @throws ConfigException.Missing * if value is absent or null */ @@ -172,35 +361,47 @@ public interface Config extends ConfigMergeable { /** * Gets the value at the given path, unless the value is a null value or - * missing, in which case it throws just like the other getters. Use get() - * from the Map interface if you want an unprocessed value. + * missing, in which case it throws just like the other getters. Use + * {@code get()} from the {@link java.util.Map Map} interface if you want an + * unprocessed value. * * @param path - * @return + * path expression + * @return the value at the requested path * @throws ConfigException.Missing * if value is absent or null */ ConfigValue getValue(String path); /** - * Get value as a size in bytes (parses special strings like "128M"). The - * size units are interpreted as for memory, not as for disk space, so they - * are in powers of two. + * Gets a value as a size in bytes (parses special strings like "128M"). If + * the value is already a number, then it's left alone; if it's a string, + * it's parsed understanding unit suffixes such as "128K", as documented in + * the the + * spec. * + * @param path + * path expression + * @return the value at the requested path, in bytes * @throws ConfigException.Missing * if value is absent or null * @throws ConfigException.WrongType * if value is not convertible to Long or String * @throws ConfigException.BadValue - * if value cannot be parsed as a memory size + * if value cannot be parsed as a size in bytes */ - Long getMemorySizeInBytes(String path); + Long getBytes(String path); /** * Get value as a duration in milliseconds. If the value is already a * number, then it's left alone; if it's a string, it's parsed understanding - * units suffixes like "10m" or "5ns" + * units suffixes like "10m" or "5ns" as documented in the the + * spec. * + * @param path + * path expression + * @return the duration value at the requested path, in milliseconds * @throws ConfigException.Missing * if value is absent or null * @throws ConfigException.WrongType @@ -213,8 +414,12 @@ public interface Config extends ConfigMergeable { /** * Get value as a duration in nanoseconds. If the value is already a number * it's taken as milliseconds and converted to nanoseconds. If it's a - * string, it's parsed understanding unit suffixes. + * string, it's parsed understanding unit suffixes, as for + * {@link #getMilliseconds(String)}. * + * @param path + * path expression + * @return the duration value at the requested path, in nanoseconds * @throws ConfigException.Missing * if value is absent or null * @throws ConfigException.WrongType @@ -225,13 +430,13 @@ public interface Config extends ConfigMergeable { Long getNanoseconds(String path); /** - * Gets a list value (with any element type) as a ConfigList, which - * implements java.util.List. Throws if the path is unset or - * null. + * Gets a list value (with any element type) as a {@link ConfigList}, which + * implements {@code java.util.List}. Throws if the path is + * unset or null. * * @param path * the path to the list value. - * @return the ConfigList at the path + * @return the {@link ConfigList} at the path * @throws ConfigException.Missing * if value is absent or null * @throws ConfigException.WrongType @@ -257,7 +462,7 @@ public interface Config extends ConfigMergeable { List getAnyRefList(String path); - List getMemorySizeInBytesList(String path); + List getBytesList(String path); List getMillisecondsList(String path); diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigException.java b/akka-actor/src/main/java/com/typesafe/config/ConfigException.java index 7763231108..8c23d09533 100644 --- a/akka-actor/src/main/java/com/typesafe/config/ConfigException.java +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigException.java @@ -3,15 +3,19 @@ */ package com.typesafe.config; + /** * All exceptions thrown by the library are subclasses of ConfigException. */ -public class ConfigException extends RuntimeException { +public abstract class ConfigException extends RuntimeException { private static final long serialVersionUID = 1L; + final private ConfigOrigin origin; + protected ConfigException(ConfigOrigin origin, String message, Throwable cause) { super(origin.description() + ": " + message, cause); + this.origin = origin; } protected ConfigException(ConfigOrigin origin, String message) { @@ -20,12 +24,26 @@ public class ConfigException extends RuntimeException { protected ConfigException(String message, Throwable cause) { super(message, cause); + this.origin = null; } protected ConfigException(String message) { this(message, null); } + /** + * Returns an "origin" (such as a filename and line number) for the + * exception, or null if none is available. If there's no sensible origin + * for a given exception, or the kind of exception doesn't meaningfully + * relate to a particular origin file, this returns null. Never assume this + * will return non-null, it can always return null. + * + * @return origin of the problem, or null if unknown/inapplicable + */ + public ConfigOrigin origin() { + return origin; + } + /** * Exception indicating that the type of a value does not match the type you * requested. @@ -134,6 +152,11 @@ public class ConfigException extends RuntimeException { } } + /** + * Exception indicating that a path expression was invalid. Try putting + * double quotes around path elements that contain "special" characters. + * + */ public static class BadPath extends ConfigException { private static final long serialVersionUID = 1L; @@ -163,10 +186,11 @@ public class ConfigException extends RuntimeException { } /** - * Exception indicating that there's a bug in something or the runtime - * environment is broken. This exception should never be handled; instead, - * something should be fixed to keep the exception from occurring. - * + * Exception indicating that there's a bug in something (possibly the + * library itself) or the runtime environment is broken. This exception + * should never be handled; instead, something should be fixed to keep the + * exception from occurring. This exception can be thrown by any method in + * the library. */ public static class BugOrBroken extends ConfigException { private static final long serialVersionUID = 1L; @@ -212,12 +236,29 @@ public class ConfigException extends RuntimeException { } } + /** + * Exception indicating that a substitution did not resolve to anything. + * Thrown by {@link Config#resolve}. + */ + public static class UnresolvedSubstitution extends Parse { + private static final long serialVersionUID = 1L; + + public UnresolvedSubstitution(ConfigOrigin origin, String expression, Throwable cause) { + super(origin, "Could not resolve substitution to a value: " + expression, cause); + } + + public UnresolvedSubstitution(ConfigOrigin origin, String expression) { + this(origin, expression, null); + } + } + /** * Exception indicating that you tried to use a function that requires - * substitutions to be resolved, but substitutions have not been resolved. - * This is always a bug in either application code or the library; it's - * wrong to write a handler for this exception because you should be able to - * fix the code to avoid it. + * substitutions to be resolved, but substitutions have not been resolved + * (that is, {@link Config#resolve} was not called). This is always a bug in + * either application code or the library; it's wrong to write a handler for + * this exception because you should be able to fix the code to avoid it by + * adding calls to {@link Config#resolve}. */ public static class NotResolved extends BugOrBroken { private static final long serialVersionUID = 1L; @@ -230,4 +271,92 @@ public class ConfigException extends RuntimeException { this(message, null); } } + + /** + * Information about a problem that occurred in {@link Config#checkValid}. A + * {@link ConfigException.ValidationFailed} exception thrown from + * checkValid() includes a list of problems encountered. + */ + public static class ValidationProblem { + + final private String path; + final private ConfigOrigin origin; + final private String problem; + + public ValidationProblem(String path, ConfigOrigin origin, String problem) { + this.path = path; + this.origin = origin; + this.problem = problem; + } + + /** Returns the config setting causing the problem. */ + public String path() { + return path; + } + + /** + * Returns where the problem occurred (origin may include info on the + * file, line number, etc.). + */ + public ConfigOrigin origin() { + return origin; + } + + /** Returns a description of the problem. */ + public String problem() { + return problem; + } + } + + /** + * Exception indicating that {@link Config#checkValid} found validity + * problems. The problems are available via the {@link #problems()} method. + * The getMessage() of this exception is a potentially very + * long string listing all the problems found. + */ + public static class ValidationFailed extends ConfigException { + private static final long serialVersionUID = 1L; + + final private Iterable problems; + + public ValidationFailed(Iterable problems) { + super(makeMessage(problems), null); + this.problems = problems; + } + + public Iterable problems() { + return problems; + } + + private static String makeMessage(Iterable problems) { + StringBuilder sb = new StringBuilder(); + for (ValidationProblem p : problems) { + sb.append(p.origin().description()); + sb.append(": "); + sb.append(p.path()); + sb.append(": "); + sb.append(p.problem()); + sb.append(", "); + } + sb.setLength(sb.length() - 2); // chop comma and space + + return sb.toString(); + } + } + + /** + * Exception that doesn't fall into any other category. + */ + public static class Generic extends ConfigException { + private static final long serialVersionUID = 1L; + + public Generic(String message, Throwable cause) { + super(message, cause); + } + + public Generic(String message) { + this(message, null); + } + } + } diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigFactory.java b/akka-actor/src/main/java/com/typesafe/config/ConfigFactory.java index b55c9abdf8..9251b3fb45 100644 --- a/akka-actor/src/main/java/com/typesafe/config/ConfigFactory.java +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigFactory.java @@ -5,211 +5,484 @@ package com.typesafe.config; import java.io.File; import java.io.Reader; +import java.net.MalformedURLException; import java.net.URL; import java.util.Map; import java.util.Properties; import com.typesafe.config.impl.ConfigImpl; +import com.typesafe.config.impl.ConfigUtil; import com.typesafe.config.impl.Parseable; /** - * This class contains static methods for creating Config objects. + * Contains static methods for creating {@link Config} instances. * + *

+ * See also {@link ConfigValueFactory} which contains static methods for + * converting Java values into a {@link ConfigObject}. You can then convert a + * {@code ConfigObject} into a {@code Config} with {@link ConfigObject#toConfig}. + * + *

* The static methods with "load" in the name do some sort of higher-level * operation potentially parsing multiple resources and resolving substitutions, - * while the ones with "parse" in the name just create a ConfigValue from a - * resource and nothing else. + * while the ones with "parse" in the name just create a {@link ConfigValue} + * from a resource and nothing else. */ public final class ConfigFactory { + private ConfigFactory() { + } + /** - * Loads a configuration for the given root path in a "standard" way. - * Oversimplified, if your root path is foo.bar then this will load files - * from the classpath: foo-bar.conf, foo-bar.json, foo-bar.properties, - * foo-bar-reference.conf, foo-bar-reference.json, - * foo-bar-reference.properties. It will override all those files with any - * system properties that begin with "foo.bar.", as well. - * - * The root path should be a path expression, usually just a single short - * word, that scopes the package being configured; typically it's the - * package name or something similar. System properties overriding values in - * the configuration will have to be prefixed with the root path. The root - * path may have periods in it if you like but other punctuation or - * whitespace will probably cause you headaches. Example root paths: "akka", - * "sbt", "jsoup", "heroku", "mongo", etc. + * Loads an application's configuration from the given classpath resource or + * classpath resource basename, sandwiches it between default reference + * config and default overrides, and then resolves it. The classpath + * resource is "raw" (it should have no "/" prefix, and is not made relative + * to any package, so it's like {@link ClassLoader#getResource} not + * {@link Class#getResource}). * + *

* The loaded object will already be resolved (substitutions have already * been processed). As a result, if you add more fallbacks then they won't * be seen by substitutions. Substitutions are the "${foo.bar}" syntax. If * you want to parse additional files or something then you need to use - * loadWithoutResolving(). + * {@link #load(Config)}. * - * @param rootPath - * the configuration "domain" - * @return configuration object for the requested root path + * @param resourceBasename + * name (optionally without extension) of a resource on classpath + * @return configuration for an application */ - public static ConfigRoot load(String rootPath) { - return loadWithoutResolving(rootPath).resolve(); - } - - public static ConfigRoot load(String rootPath, - ConfigParseOptions parseOptions, ConfigResolveOptions resolveOptions) { - return loadWithoutResolving(rootPath, parseOptions).resolve( - resolveOptions); + public static Config load(String resourceBasename) { + return load(resourceBasename, ConfigParseOptions.defaults(), + ConfigResolveOptions.defaults()); } /** - * Like load() but does not resolve the object, so you can go ahead and add - * more fallbacks and stuff and have them seen by substitutions when you do - * call {@link ConfigRoot.resolve()}. - * - * @param rootPath - * @return + * Like {@link #load(String)} but allows you to specify parse and resolve + * options. + * @param resourceBasename + * the classpath resource name with optional extension + * @param parseOptions + * options to use when parsing the resource + * @param resolveOptions + * options to use when resolving the stack + * @return configuration for an application */ - public static ConfigRoot loadWithoutResolving(String rootPath) { - return loadWithoutResolving(rootPath, ConfigParseOptions.defaults()); + public static Config load(String resourceBasename, ConfigParseOptions parseOptions, + ConfigResolveOptions resolveOptions) { + Config appConfig = ConfigFactory.parseResourcesAnySyntax(ConfigFactory.class, "/" + + resourceBasename, parseOptions); + return load(appConfig, resolveOptions); } - public static ConfigRoot loadWithoutResolving(String rootPath, - ConfigParseOptions options) { - ConfigRoot system = systemPropertiesRoot(rootPath); - - Config mainFiles = parseResourcesForPath(rootPath, options); - Config referenceFiles = parseResourcesForPath(rootPath + ".reference", - options); - - return system.withFallback(mainFiles).withFallback(referenceFiles); + /** + * Assembles a standard configuration using a custom Config + * object rather than loading "application.conf". The Config + * object will be sandwiched between the default reference config and + * default overrides and then resolved. + * + * @param config + * the application's portion of the configuration + * @return resolved configuration with overrides and fallbacks added + */ + public static Config load(Config config) { + return load(config, ConfigResolveOptions.defaults()); } - public static ConfigRoot emptyRoot(String rootPath) { - return emptyRoot(rootPath, null); + /** + * Like {@link #load(Config)} but allows you to specify + * {@link ConfigResolveOptions}. + * + * @param config + * the application's portion of the configuration + * @param resolveOptions + * options for resolving the assembled config stack + * @return resolved configuration with overrides and fallbacks added + */ + public static Config load(Config config, ConfigResolveOptions resolveOptions) { + return defaultOverrides().withFallback(config).withFallback(defaultReference()) + .resolve(resolveOptions); } + private static class DefaultConfigHolder { + + private static Config loadDefaultConfig() { + int specified = 0; + + // override application.conf with config.file, config.resource, + // config.url if requested. + String resource = System.getProperty("config.resource"); + if (resource != null) + specified += 1; + String file = System.getProperty("config.file"); + if (file != null) + specified += 1; + String url = System.getProperty("config.url"); + if (url != null) + specified += 1; + + if (specified == 0) { + return load("application"); + } else if (specified > 1) { + throw new ConfigException.Generic("You set more than one of config.file='" + file + + "', config.url='" + url + "', config.resource='" + resource + + "'; don't know which one to use!"); + } else { + if (resource != null) { + // this deliberately does not parseResourcesAnySyntax; if + // people want that they can use an include statement. + return load(parseResources(ConfigFactory.class, resource)); + } else if (file != null) { + return load(parseFile(new File(file))); + } else { + try { + return load(parseURL(new URL(url))); + } catch (MalformedURLException e) { + throw new ConfigException.Generic( + "Bad URL in config.url system property: '" + url + "': " + + e.getMessage(), e); + } + } + } + } + + static final Config defaultConfig = loadDefaultConfig(); + } + + /** + * Loads a default configuration, equivalent to {@link #load(String) + * load("application")} in most cases. This configuration should be used by + * libraries and frameworks unless an application provides a different one. + *

+ * This method may return a cached singleton. + *

+ * If the system properties config.resource, + * config.file, or config.url are set, then the + * classpath resource, file, or URL specified in those properties will be + * used rather than the default + * application.{conf,json,properties} classpath resources. + * These system properties should not be set in code (after all, you can + * just parse whatever you want manually and then use {@link #load(Config) + * if you don't want to use application.conf}). The properties + * are intended for use by the person or script launching the application. + * For example someone might have a production.conf that + * include application.conf but then change a couple of values. + * When launching the app they could specify + * -Dconfig.resource=production.conf to get production mode. + *

+ * If no system properties are set to change the location of the default + * configuration, ConfigFactory.load() is equivalent to + * ConfigFactory.load("application"). + * + * @return configuration for an application + */ + public static Config load() { + try { + return DefaultConfigHolder.defaultConfig; + } catch (ExceptionInInitializerError e) { + throw ConfigUtil.extractInitializerError(e); + } + } + + /** + * Obtains the default reference configuration, which is currently created + * by merging all resources "reference.conf" found on the classpath and + * overriding the result with system properties. The returned reference + * configuration will already have substitutions resolved. + * + *

+ * Libraries and frameworks should ship with a "reference.conf" in their + * jar. + * + *

+ * The {@link #load()} methods merge this configuration for you + * automatically. + * + *

+ * Future versions may look for reference configuration in more places. It + * is not guaranteed that this method only looks at + * "reference.conf". + * + * @return the default reference config + */ + public static Config defaultReference() { + return ConfigImpl.defaultReference(); + } + + /** + * Obtains the default override configuration, which currently consists of + * system properties. The returned override configuration will already have + * substitutions resolved. + * + *

+ * The {@link #load()} methods merge this configuration for you + * automatically. + * + *

+ * Future versions may get overrides in more places. It is not guaranteed + * that this method only uses system properties. + * + * @return the default override configuration + */ + public static Config defaultOverrides() { + return systemProperties(); + } + + /** + * Gets an empty configuration. See also {@link #empty(String)} to create an + * empty configuration with a description, which may improve user-visible + * error messages. + * + * @return an empty configuration + */ public static Config empty() { return empty(null); } - public static ConfigRoot emptyRoot(String rootPath, String originDescription) { - return ConfigImpl.emptyRoot(rootPath, originDescription); - } - + /** + * Gets an empty configuration with a description to be used to create a + * {@link ConfigOrigin} for this Config. The description should + * be very short and say what the configuration is, like "default settings" + * or "foo settings" or something. (Presumably you will merge some actual + * settings into this empty config using {@link Config#withFallback}, making + * the description more useful.) + * + * @param originDescription + * description of the config + * @return an empty configuration + */ public static Config empty(String originDescription) { return ConfigImpl.emptyConfig(originDescription); } - public static ConfigRoot systemPropertiesRoot(String rootPath) { - return ConfigImpl.systemPropertiesRoot(rootPath); - } - + /** + * Gets a Config containing the system properties from + * {@link java.lang.System#getProperties()}, parsed and converted as with + * {@link #parseProperties}. This method can return a global immutable + * singleton, so it's preferred over parsing system properties yourself. + * + *

+ * {@link #load} will include the system properties as overrides already, as + * will {@link #defaultReference} and {@link #defaultOverrides}. + * + *

+ * Because this returns a singleton, it will not notice changes to system + * properties made after the first time this method is called. + * + * @return system properties parsed into a Config + */ public static Config systemProperties() { return ConfigImpl.systemPropertiesAsConfig(); } + /** + * Gets a Config containing the system's environment variables. + * This method can return a global immutable singleton. + * + *

+ * Environment variables are used as fallbacks when resolving substitutions + * whether or not this object is included in the config being resolved, so + * you probably don't need to use this method for most purposes. It can be a + * nicer API for accessing environment variables than raw + * {@link java.lang.System#getenv(String)} though, since you can use methods + * such as {@link Config#getInt}. + * + * @return system environment variables parsed into a Config + */ public static Config systemEnvironment() { return ConfigImpl.envVariablesAsConfig(); } /** - * Converts a Java Properties object to a ConfigObject using the rules - * documented in https://github.com/havocp/config/blob/master/HOCON.md The - * keys in the Properties object are split on the period character '.' and - * treated as paths. The values will all end up as string values. If you - * have both "a=foo" and "a.b=bar" in your properties file, so "a" is both - * the object containing "b" and the string "foo", then the string value is - * dropped. + * Converts a Java {@link java.util.Properties} object to a + * {@link ConfigObject} using the rules documented in the HOCON + * spec. The keys in the Properties object are split on the + * period character '.' and treated as paths. The values will all end up as + * string values. If you have both "a=foo" and "a.b=bar" in your properties + * file, so "a" is both the object containing "b" and the string "foo", then + * the string value is dropped. * - * If you want to get System.getProperties() as a ConfigObject, it's better - * to use the systemProperties() or systemPropertiesRoot() methods. Those - * methods are able to use a cached global singleton ConfigObject for the - * system properties. + *

+ * If you want to have System.getProperties() as a + * ConfigObject, it's better to use the {@link #systemProperties()} method + * which returns a cached global singleton. * * @param properties * a Java Properties object * @param options - * @return + * @return the parsed configuration */ public static Config parseProperties(Properties properties, ConfigParseOptions options) { return Parseable.newProperties(properties, options).parse().toConfig(); } + public static Config parseProperties(Properties properties) { + return parseProperties(properties, ConfigParseOptions.defaults()); + } + public static Config parseReader(Reader reader, ConfigParseOptions options) { return Parseable.newReader(reader, options).parse().toConfig(); } + public static Config parseReader(Reader reader) { + return parseReader(reader, ConfigParseOptions.defaults()); + } + public static Config parseURL(URL url, ConfigParseOptions options) { return Parseable.newURL(url, options).parse().toConfig(); } + public static Config parseURL(URL url) { + return parseURL(url, ConfigParseOptions.defaults()); + } + public static Config parseFile(File file, ConfigParseOptions options) { return Parseable.newFile(file, options).parse().toConfig(); } + public static Config parseFile(File file) { + return parseFile(file, ConfigParseOptions.defaults()); + } + /** - * Parses a file. If the fileBasename already ends in a known extension, - * just parses it according to that extension. If the fileBasename does not - * end in an extension, then parse all known extensions and merge whatever - * is found. If options force a specific syntax, only parse files with an - * extension matching that syntax. If options.getAllowMissing() is true, - * then no files have to exist; if false, then at least one file has to - * exist. + * Parses a file with a flexible extension. If the fileBasename + * already ends in a known extension, this method parses it according to + * that extension (the file's syntax must match its extension). If the + * fileBasename does not end in an extension, it parses files + * with all known extensions and merges whatever is found. + * + *

+ * In the current implementation, the extension ".conf" forces + * {@link ConfigSyntax#CONF}, ".json" forces {@link ConfigSyntax#JSON}, and + * ".properties" forces {@link ConfigSyntax#PROPERTIES}. When merging files, + * ".conf" falls back to ".json" falls back to ".properties". + * + *

+ * Future versions of the implementation may add additional syntaxes or + * additional extensions. However, the ordering (fallback priority) of the + * three current extensions will remain the same. + * + *

+ * If options forces a specific syntax, this method only parses + * files with an extension matching that syntax. + * + *

+ * If {@link ConfigParseOptions#getAllowMissing options.getAllowMissing()} + * is true, then no files have to exist; if false, then at least one file + * has to exist. * * @param fileBasename + * a filename with or without extension * @param options - * @return + * parse options + * @return the parsed configuration */ public static Config parseFileAnySyntax(File fileBasename, ConfigParseOptions options) { return ConfigImpl.parseFileAnySyntax(fileBasename, options).toConfig(); } - public static Config parseResource(Class klass, String resource, - ConfigParseOptions options) { - return Parseable.newResource(klass, resource, options).parse() - .toConfig(); + public static Config parseFileAnySyntax(File fileBasename) { + return parseFileAnySyntax(fileBasename, ConfigParseOptions.defaults()); } /** - * Same behavior as parseFileAnySyntax() but for classpath resources - * instead. + * Parses all resources on the classpath with the given name and merges them + * into a single Config. + * + *

+ * If the resource name does not begin with a "/", it will have the supplied + * class's package added to it, in the same way as + * {@link java.lang.Class#getResource}. + * + *

+ * Duplicate resources with the same name are merged such that ones returned + * earlier from {@link ClassLoader#getResources} fall back to (have higher + * priority than) the ones returned later. This implies that resources + * earlier in the classpath override those later in the classpath when they + * configure the same setting. However, in practice real applications may + * not be consistent about classpath ordering, so be careful. It may be best + * to avoid assuming too much. * * @param klass - * @param resourceBasename + * klass.getClassLoader() will be used to load + * resources, and non-absolute resource names will have this + * class's package added + * @param resource + * resource to look up, relative to klass's package + * or absolute starting with a "/" * @param options - * @return + * parse options + * @return the parsed configuration */ - public static Config parseResourceAnySyntax(Class klass, String resourceBasename, + public static Config parseResources(Class klass, String resource, ConfigParseOptions options) { - return ConfigImpl.parseResourceAnySyntax(klass, resourceBasename, + return Parseable.newResources(klass, resource, options).parse() + .toConfig(); + } + + public static Config parseResources(Class klass, String resource) { + return parseResources(klass, resource, ConfigParseOptions.defaults()); + } + + /** + * Parses classpath resources with a flexible extension. In general, this + * method has the same behavior as + * {@link #parseFileAnySyntax(File,ConfigParseOptions)} but for classpath + * resources instead, as in {@link #parseResources}. + * + *

+ * There is a thorny problem with this method, which is that + * {@link java.lang.ClassLoader#getResources} must be called separately for + * each possible extension. The implementation ends up with separate lists + * of resources called "basename.conf" and "basename.json" for example. As a + * result, the ideal ordering between two files with different extensions is + * unknown; there is no way to figure out how to merge the two lists in + * classpath order. To keep it simple, the lists are simply concatenated, + * with the same syntax priorities as + * {@link #parseFileAnySyntax(File,ConfigParseOptions) parseFileAnySyntax()} + * - all ".conf" resources are ahead of all ".json" resources which are + * ahead of all ".properties" resources. + * + * @param klass + * class which determines the ClassLoader and the + * package for relative resource names + * @param resourceBasename + * a resource name as in {@link java.lang.Class#getResource}, + * with or without extension + * @param options + * parse options + * @return the parsed configuration + */ + public static Config parseResourcesAnySyntax(Class klass, String resourceBasename, + ConfigParseOptions options) { + return ConfigImpl.parseResourcesAnySyntax(klass, resourceBasename, options).toConfig(); } + public static Config parseResourcesAnySyntax(Class klass, String resourceBasename) { + return parseResourcesAnySyntax(klass, resourceBasename, ConfigParseOptions.defaults()); + } + public static Config parseString(String s, ConfigParseOptions options) { return Parseable.newString(s, options).parse().toConfig(); } - /** - * Parses classpath resources corresponding to this path expression. - * Essentially if the path is "foo.bar" then the resources are - * "/foo-bar.conf", "/foo-bar.json", and "/foo-bar.properties". If more than - * one of those exists, they are merged. - * - * @param path - * @param options - * @return - */ - public static Config parseResourcesForPath(String rootPath, - ConfigParseOptions options) { - // null originDescription is allowed in parseResourcesForPath - return ConfigImpl.parseResourcesForPath(rootPath, options).toConfig(); + public static Config parseString(String s) { + return parseString(s, ConfigParseOptions.defaults()); } /** - * Similar to ConfigValueFactory.fromMap(), but the keys in the map are path - * expressions, rather than keys; and correspondingly it returns a Config - * instead of a ConfigObject. This is more convenient if you are writing - * literal maps in code, and less convenient if you are getting your maps - * from some data source such as a parser. + * Creates a {@code Config} based on a {@link java.util.Map} from paths to + * plain Java values. Similar to + * {@link ConfigValueFactory#fromMap(Map,String)}, except the keys in the + * map are path expressions, rather than keys; and correspondingly it + * returns a {@code Config} instead of a {@code ConfigObject}. This is more + * convenient if you are writing literal maps in code, and less convenient + * if you are getting your maps from some data source such as a parser. * + *

* An exception will be thrown (and it is a bug in the caller of the method) * if a path is both an object and a value, for example if you had both * "a=foo" and "a.b=bar", then "a" is both the string "foo" and the parent @@ -221,7 +494,7 @@ public final class ConfigFactory { * description of what this map represents, like a filename, or * "default settings" (origin description is used in error * messages) - * @return + * @return the map converted to a {@code Config} */ public static Config parseMap(Map values, String originDescription) { @@ -229,11 +502,11 @@ public final class ConfigFactory { } /** - * See the other overload of parseMap() for details, this one just uses a - * default origin description. + * See the other overload of {@link #parseMap(Map, String)} for details, + * this one just uses a default origin description. * * @param values - * @return + * @return the map converted to a {@code Config} */ public static Config parseMap(Map values) { return parseMap(values, null); diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigIncludeContext.java b/akka-actor/src/main/java/com/typesafe/config/ConfigIncludeContext.java index a770250171..2be0abff34 100644 --- a/akka-actor/src/main/java/com/typesafe/config/ConfigIncludeContext.java +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigIncludeContext.java @@ -5,8 +5,9 @@ package com.typesafe.config; /** - * A ConfigIncludeContext is passed to a ConfigIncluder. This interface is not - * intended for apps to implement. + * Context provided to a {@link ConfigIncluder}; this interface is only useful + * inside a {@code ConfigIncluder} implementation, and is not intended for apps + * to implement. */ public interface ConfigIncludeContext { /** @@ -15,7 +16,7 @@ public interface ConfigIncludeContext { * null if it can't meaningfully create a relative name. The returned * parseable may not exist; this function is not required to do any IO, just * compute what the name would be. - * + * * The passed-in filename has to be a complete name (with extension), not * just a basename. (Include statements in config files are allowed to give * just a basename.) diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigIncluder.java b/akka-actor/src/main/java/com/typesafe/config/ConfigIncluder.java index 364b8c5e30..1ac6f3383d 100644 --- a/akka-actor/src/main/java/com/typesafe/config/ConfigIncluder.java +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigIncluder.java @@ -4,8 +4,9 @@ package com.typesafe.config; /** - * Interface you have to implement to customize "include" statements in config - * files. + * Implement this interface and provide an instance to + * {@link ConfigParseOptions#setIncluder ConfigParseOptions.setIncluder()} to + * customize handling of {@code include} statements in config files. */ public interface ConfigIncluder { /** @@ -29,7 +30,7 @@ public interface ConfigIncluder { * Parses another item to be included. The returned object typically would * not have substitutions resolved. You can throw a ConfigException here to * abort parsing, or return an empty object, but may not return null. - * + * * @param context * some info about the include context * @param what diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigList.java b/akka-actor/src/main/java/com/typesafe/config/ConfigList.java index 0688c29abe..2024efe744 100644 --- a/akka-actor/src/main/java/com/typesafe/config/ConfigList.java +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigList.java @@ -6,9 +6,30 @@ package com.typesafe.config; import java.util.List; /** - * A list (aka array) value corresponding to ConfigValueType.LIST or JSON's - * "[1,2,3]" value. Implements java.util.List so you can use it - * like a regular Java list. + * Subtype of {@link ConfigValue} representing a list value, as in JSON's + * {@code [1,2,3]} syntax. + * + *

+ * {@code ConfigList} implements {@code java.util.List} so you can + * use it like a regular Java list. Or call {@link #unwrapped()} to unwrap the + * list elements into plain Java values. + * + *

+ * Like all {@link ConfigValue} subtypes, {@code ConfigList} is immutable. This + * makes it threadsafe and you never have to create "defensive copies." The + * mutator methods from {@link java.util.List} all throw + * {@link java.lang.UnsupportedOperationException}. + * + *

+ * The {@link ConfigValue#valueType} method on a list returns + * {@link ConfigValueType#LIST}. + * + *

+ * Do not implement {@code ConfigList}; it should only be implemented + * by the config library. Arbitrary implementations will not work because the + * library internals assume a specific concrete implementation. Also, this + * interface is likely to grow new methods over time, so third-party + * implementations will break. * */ public interface ConfigList extends List, ConfigValue { diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigMergeable.java b/akka-actor/src/main/java/com/typesafe/config/ConfigMergeable.java index deec42bec1..c4280e93ea 100644 --- a/akka-actor/src/main/java/com/typesafe/config/ConfigMergeable.java +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigMergeable.java @@ -4,33 +4,39 @@ package com.typesafe.config; /** - * This is a marker for types that can be merged as a fallback into a Config or - * a ConfigValue. Both Config and ConfigValue are mergeable. + * Marker for types whose instances can be merged, that is {@link Config} and + * {@link ConfigValue}. Instances of {@code Config} and {@code ConfigValue} can + * be combined into a single new instance using the + * {@link ConfigMergeable#withFallback withFallback()} method. + * + *

+ * Do not implement this interface; it should only be implemented by + * the config library. Arbitrary implementations will not work because the + * library internals assume a specific concrete implementation. Also, this + * interface is likely to grow new methods over time, so third-party + * implementations will break. */ public interface ConfigMergeable { - /** - * Converts the mergeable to a ConfigValue to be merged. - * - * @return - */ - ConfigValue toValue(); - /** * Returns a new value computed by merging this value with another, with - * keys in this value "winning" over the other one. Only ConfigObject and - * Config instances do anything in this method (they need to merge the - * fallback keys into themselves). All other values just return the original - * value, since they automatically override any fallback. - * - * The semantics of merging are described in - * https://github.com/havocp/config/blob/master/HOCON.md - * - * Note that objects do not merge "across" non-objects; if you do + * keys in this value "winning" over the other one. Only + * {@link ConfigObject} and {@link Config} instances do anything in this + * method (they need to merge the fallback keys into themselves). All other + * values just return the original value, since they automatically override + * any fallback. + * + *

+ * The semantics of merging are described in the spec for + * HOCON. + * + *

+ * Note that objects do not merge "across" non-objects; if you write * object.withFallback(nonObject).withFallback(otherObject), * then otherObject will simply be ignored. This is an * intentional part of how merging works. Both non-objects, and any object * which has fallen back to a non-object, block subsequent fallbacks. - * + * * @param other * an object whose keys should be used if the keys are not * present in this one diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigObject.java b/akka-actor/src/main/java/com/typesafe/config/ConfigObject.java index c559f9e4ee..8613840223 100644 --- a/akka-actor/src/main/java/com/typesafe/config/ConfigObject.java +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigObject.java @@ -6,54 +6,67 @@ package com.typesafe.config; import java.util.Map; /** - * A ConfigObject is a read-only configuration object, which may have nested - * child objects. Implementations of ConfigObject should be immutable (at least - * from the perspective of anyone using this interface) and thus thread-safe. + * Subtype of {@link ConfigValue} representing an object (dictionary, map) + * value, as in JSON's { "a" : 42 } syntax. * - * In most cases you want to use the Config interface rather than this one. Call - * toConfig() to convert a ConfigObject to a config. + *

+ * {@code ConfigObject} implements {@code java.util.Map} so + * you can use it like a regular Java map. Or call {@link #unwrapped()} to + * unwrap the map to a map with plain Java values rather than + * {@code ConfigValue}. * - * The API for a ConfigObject is in terms of keys, while the API for a Config is - * in terms of path expressions. Conceptually, ConfigObject is a tree of maps - * from keys to values, while a ConfigObject is a one-level map from paths to - * values. + *

+ * Like all {@link ConfigValue} subtypes, {@code ConfigObject} is immutable. + * This makes it threadsafe and you never have to create "defensive copies." The + * mutator methods from {@link java.util.Map} all throw + * {@link java.lang.UnsupportedOperationException}. * - * Throughout the API, there is a distinction between "keys" and "paths". A key - * is a key in a JSON object; it's just a string that's the key in a map. A - * "path" is a parseable expression with a syntax and it refers to a series of - * keys. A path is used to traverse nested ConfigObject by looking up each key - * in the path. Path expressions are described in the spec for "HOCON", which - * can be found at https://github.com/havocp/config/blob/master/HOCON.md; in - * brief, a path is period-separated so "a.b.c" looks for key c in object b in - * object a in the root object. Sometimes double quotes are needed around - * special characters in path expressions. + *

+ * The {@link ConfigValue#valueType} method on an object returns + * {@link ConfigValueType#OBJECT}. * - * ConfigObject implements java.util.Map and all methods - * work with keys, not path expressions. + *

+ * In most cases you want to use the {@link Config} interface rather than this + * one. Call {@link #toConfig()} to convert a {@code ConfigObject} to a + * {@code Config}. * - * While ConfigObject implements the standard Java Map interface, the mutator - * methods all throw UnsupportedOperationException. This Map is immutable. + *

+ * The API for a {@code ConfigObject} is in terms of keys, while the API for a + * {@link Config} is in terms of path expressions. Conceptually, + * {@code ConfigObject} is a tree of maps from keys to values, while a + * {@code ConfigObject} is a one-level map from paths to values. * - * The Map may contain null values, which will have ConfigValue.valueType() == - * ConfigValueType.NULL. If get() returns Java's null then the key was not - * present in the parsed file (or wherever this value tree came from). If get() - * returns a ConfigValue with type ConfigValueType.NULL then the key was set to - * null explicitly. + *

+ * A {@code ConfigObject} may contain null values, which will have + * {@link ConfigValue#valueType()} equal to {@link ConfigValueType#NULL}. If + * {@code get()} returns Java's null then the key was not present in the parsed + * file (or wherever this value tree came from). If {@code get()} returns a + * {@link ConfigValue} with type {@code ConfigValueType#NULL} then the key was + * set to null explicitly in the config file. + * + *

+ * Do not implement {@code ConfigObject}; it should only be implemented + * by the config library. Arbitrary implementations will not work because the + * library internals assume a specific concrete implementation. Also, this + * interface is likely to grow new methods over time, so third-party + * implementations will break. */ public interface ConfigObject extends ConfigValue, Map { /** - * Converts this object to a Config instance, enabling you to use path - * expressions to find values in the object. This is a constant-time + * Converts this object to a {@link Config} instance, enabling you to use + * path expressions to find values in the object. This is a constant-time * operation (it is not proportional to the size of the object). * - * @return + * @return a {@link Config} with this object as its root */ Config toConfig(); /** * Recursively unwraps the object, returning a map from String to whatever * plain Java values are unwrapped from the object's values. + * + * @return a {@link java.util.Map} containing plain Java objects */ @Override Map unwrapped(); @@ -62,10 +75,15 @@ public interface ConfigObject extends ConfigValue, Map { ConfigObject withFallback(ConfigMergeable other); /** - * Gets a ConfigValue at the given key, or returns null if there is no - * value. The returned ConfigValue may have ConfigValueType.NULL or any - * other type, and the passed-in key must be a key in this object, rather - * than a path expression. + * Gets a {@link ConfigValue} at the given key, or returns null if there is + * no value. The returned {@link ConfigValue} may have + * {@link ConfigValueType#NULL} or any other type, and the passed-in key + * must be a key in this object, rather than a path expression. + * + * @param key + * key to look up + * + * @return the value at the key or null if none */ @Override ConfigValue get(Object key); diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigOrigin.java b/akka-actor/src/main/java/com/typesafe/config/ConfigOrigin.java index 73240736e5..013d91eb9e 100644 --- a/akka-actor/src/main/java/com/typesafe/config/ConfigOrigin.java +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigOrigin.java @@ -3,10 +3,67 @@ */ package com.typesafe.config; +import java.net.URL; + + /** - * ConfigOrigin is used to track the origin (such as filename and line number) - * of a ConfigValue or other object. The origin is used in error messages. + * Represents the origin (such as filename and line number) of a + * {@link ConfigValue} for use in error messages. Obtain the origin of a value + * with {@link ConfigValue#origin}. Exceptions may have an origin, see + * {@link ConfigException#origin}, but be careful because + * ConfigException.origin() may return null. + * + *

+ * It's best to use this interface only for debugging; its accuracy is + * "best effort" rather than guaranteed, and a potentially-noticeable amount of + * memory could probably be saved if origins were not kept around, so in the + * future there might be some option to discard origins. + * + *

+ * Do not implement this interface; it should only be implemented by + * the config library. Arbitrary implementations will not work because the + * library internals assume a specific concrete implementation. Also, this + * interface is likely to grow new methods over time, so third-party + * implementations will break. */ public interface ConfigOrigin { + /** + * Returns a string describing the origin of a value or exception. This will + * never return null. + * + * @return string describing the origin + */ public String description(); + + /** + * Returns a filename describing the origin. This will return null if the + * origin was not a file. + * + * @return filename of the origin or null + */ + public String filename(); + + /** + * Returns a URL describing the origin. This will return null if the origin + * has no meaningful URL. + * + * @return url of the origin or null + */ + public URL url(); + + /** + * Returns a classpath resource name describing the origin. This will return + * null if the origin was not a classpath resource. + * + * @return resource name of the origin or null + */ + public String resource(); + + /** + * Returns a line number where the value or exception originated. This will + * return -1 if there's no meaningful line number. + * + * @return line number or -1 if none is available + */ + public int lineNumber(); } diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigParseOptions.java b/akka-actor/src/main/java/com/typesafe/config/ConfigParseOptions.java index ac0c8f3974..f3765a5479 100644 --- a/akka-actor/src/main/java/com/typesafe/config/ConfigParseOptions.java +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigParseOptions.java @@ -4,6 +4,22 @@ package com.typesafe.config; +/** + * A set of options related to parsing. + * + *

+ * This object is immutable, so the "setters" return a new object. + * + *

+ * Here is an example of creating a custom {@code ConfigParseOptions}: + * + *

+ *     ConfigParseOptions options = ConfigParseOptions.defaults()
+ *         .setSyntax(ConfigSyntax.JSON)
+ *         .setAllowMissing(false)
+ * 
+ * + */ public final class ConfigParseOptions { final ConfigSyntax syntax; final String originDescription; @@ -24,10 +40,11 @@ public final class ConfigParseOptions { /** * Set the file format. If set to null, try to guess from any available - * filename extension; if guessing fails, assume ConfigSyntax.CONF. - * + * filename extension; if guessing fails, assume {@link ConfigSyntax#CONF}. + * * @param syntax - * @return + * a syntax or {@code null} for best guess + * @return options with the syntax set */ public ConfigParseOptions setSyntax(ConfigSyntax syntax) { if (this.syntax == syntax) @@ -45,10 +62,11 @@ public final class ConfigParseOptions { * Set a description for the thing being parsed. In most cases this will be * set up for you to something like the filename, but if you provide just an * input stream you might want to improve on it. Set to null to allow the - * library to come up with something automatically. + * library to come up with something automatically. This description is the + * basis for the {@link ConfigOrigin} of the parsed values. * * @param originDescription - * @return + * @return options with the origin description set */ public ConfigParseOptions setOriginDescription(String originDescription) { if (this.originDescription == originDescription) @@ -79,7 +97,7 @@ public final class ConfigParseOptions { * case. * * @param allowMissing - * @return + * @return options with the "allow missing" flag set */ public ConfigParseOptions setAllowMissing(boolean allowMissing) { if (this.allowMissing == allowMissing) diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigParseable.java b/akka-actor/src/main/java/com/typesafe/config/ConfigParseable.java index 4c19b5451a..1cc39614ca 100644 --- a/akka-actor/src/main/java/com/typesafe/config/ConfigParseable.java +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigParseable.java @@ -3,21 +3,40 @@ */ package com.typesafe.config; -import java.net.URL; -/** An opaque handle to something that can be parsed. */ +/** + * An opaque handle to something that can be parsed, obtained from + * {@link ConfigIncludeContext}. + * + *

+ * Do not implement this interface; it should only be implemented by + * the config library. Arbitrary implementations will not work because the + * library internals assume a specific concrete implementation. Also, this + * interface is likely to grow new methods over time, so third-party + * implementations will break. + */ public interface ConfigParseable { /** - * Parse whatever it is. - * + * Parse whatever it is. The options should come from + * {@link ConfigParseable#options options()} but you could tweak them if you + * like. + * * @param options - * parse options, should be based on the ones from options() + * parse options, should be based on the ones from + * {@link ConfigParseable#options options()} */ ConfigObject parse(ConfigParseOptions options); - /** Possibly return a URL representing the resource; this may return null. */ - URL url(); + /** + * Returns a {@link ConfigOrigin} describing the origin of the parseable + * item. + */ + ConfigOrigin origin(); - /** Get the initial options, which can be modified then passed to parse(). */ + /** + * Get the initial options, which can be modified then passed to parse(). + * These options will have the right description, includer, and other + * parameters already set up. + */ ConfigParseOptions options(); } diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigResolveOptions.java b/akka-actor/src/main/java/com/typesafe/config/ConfigResolveOptions.java index 6f572a84e8..37c7b36d5b 100644 --- a/akka-actor/src/main/java/com/typesafe/config/ConfigResolveOptions.java +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigResolveOptions.java @@ -3,36 +3,70 @@ */ package com.typesafe.config; +/** + * A set of options related to resolving substitutions. Substitutions use the + * ${foo.bar} syntax and are documented in the HOCON spec. + *

+ * This object is immutable, so the "setters" return a new object. + *

+ * Here is an example of creating a custom {@code ConfigResolveOptions}: + *

+ *     ConfigResolveOptions options = ConfigResolveOptions.defaults()
+ *         .setUseSystemEnvironment(false)
+ * 
+ *

+ * In addition to {@link ConfigResolveOptions#defaults}, there's a prebuilt + * {@link ConfigResolveOptions#noSystem} which avoids looking at any system + * environment variables or other external system information. (Right now, + * environment variables are the only example.) + */ public final class ConfigResolveOptions { - private final boolean useSystemProperties; private final boolean useSystemEnvironment; - private ConfigResolveOptions(boolean useSystemProperties, - boolean useSystemEnvironment) { - this.useSystemProperties = useSystemProperties; + private ConfigResolveOptions(boolean useSystemEnvironment) { this.useSystemEnvironment = useSystemEnvironment; } + /** + * Returns the default resolve options. + * + * @return the default resolve options + */ public static ConfigResolveOptions defaults() { - return new ConfigResolveOptions(true, true); + return new ConfigResolveOptions(true); } + /** + * Returns resolve options that disable any reference to "system" data + * (currently, this means environment variables). + * + * @return the resolve options with env variables disabled + */ public static ConfigResolveOptions noSystem() { - return new ConfigResolveOptions(false, false); - } - - public ConfigResolveOptions setUseSystemProperties(boolean value) { - return new ConfigResolveOptions(value, useSystemEnvironment); + return defaults().setUseSystemEnvironment(false); } + /** + * Returns options with use of environment variables set to the given value. + * + * @param value + * true to resolve substitutions falling back to environment + * variables. + * @return options with requested setting for use of environment variables + */ + @SuppressWarnings("static-method") public ConfigResolveOptions setUseSystemEnvironment(boolean value) { - return new ConfigResolveOptions(useSystemProperties, value); - } - - public boolean getUseSystemProperties() { - return useSystemProperties; + return new ConfigResolveOptions(value); } + /** + * Returns whether the options enable use of system environment variables. + * This method is mostly used by the config lib internally, not by + * applications. + * + * @return true if environment variables should be used + */ public boolean getUseSystemEnvironment() { return useSystemEnvironment; } diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigRoot.java b/akka-actor/src/main/java/com/typesafe/config/ConfigRoot.java deleted file mode 100644 index d8f25e89d6..0000000000 --- a/akka-actor/src/main/java/com/typesafe/config/ConfigRoot.java +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (C) 2011 Typesafe Inc. - */ -package com.typesafe.config; - -/** - * A root object. The only special thing about a root object is that you can - * resolve substitutions against it. So it can have a resolve() method that - * doesn't require you to pass in an object to resolve against. - */ -public interface ConfigRoot extends Config { - /** - * Returns a replacement root object with all substitutions (the - * "${foo.bar}" syntax) resolved. Substitutions are looked up in this root - * object. A configuration value tree must be resolved before you can use - * it. This method uses ConfigResolveOptions.defaults(). - * - * @return an immutable object with substitutions resolved - */ - ConfigRoot resolve(); - - ConfigRoot resolve(ConfigResolveOptions options); - - @Override - ConfigRoot withFallback(ConfigMergeable fallback); - - /** - * Gets the global app name that this root represents. - * - * @return the app's root config path - */ - String rootPath(); -} diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigSyntax.java b/akka-actor/src/main/java/com/typesafe/config/ConfigSyntax.java index 4f43fe7365..58e7fc020b 100644 --- a/akka-actor/src/main/java/com/typesafe/config/ConfigSyntax.java +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigSyntax.java @@ -3,6 +3,30 @@ */ package com.typesafe.config; +/** + * The syntax of a character stream, JSON, HOCON aka + * ".conf", or Java properties. + * + */ public enum ConfigSyntax { - JSON, CONF, PROPERTIES; + /** + * Pedantically strict JSON format; no + * comments, no unexpected commas, no duplicate keys in the same object. + */ + JSON, + /** + * The JSON-superset HOCON + * format. + */ + CONF, + /** + * Standard Java properties format. + */ + PROPERTIES; } diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigValue.java b/akka-actor/src/main/java/com/typesafe/config/ConfigValue.java index 8b50f7b205..b636c6f4cd 100644 --- a/akka-actor/src/main/java/com/typesafe/config/ConfigValue.java +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigValue.java @@ -4,32 +4,57 @@ package com.typesafe.config; /** - * Interface implemented by any configuration value. From the perspective of - * users of this interface, the object is immutable. It is therefore safe to use - * from multiple threads. + * An immutable value, following the JSON type + * schema. + * + *

+ * Because this object is immutable, it is safe to use from multiple threads and + * there's no need for "defensive copies." + * + *

+ * Do not implement {@code ConfigValue}; it should only be implemented + * by the config library. Arbitrary implementations will not work because the + * library internals assume a specific concrete implementation. Also, this + * interface is likely to grow new methods over time, so third-party + * implementations will break. */ public interface ConfigValue extends ConfigMergeable { /** - * The origin of the value, for debugging and error messages. + * The origin of the value (file, line number, etc.), for debugging and + * error messages. * * @return where the value came from */ ConfigOrigin origin(); /** - * The type of the value; matches the JSON type schema. + * The {@link ConfigValueType} of the value; matches the JSON type schema. * * @return value's type */ ConfigValueType valueType(); /** - * Returns the config value as a plain Java boxed value, should be a String, - * Number, etc. matching the valueType() of the ConfigValue. If the value is - * a ConfigObject or ConfigList, it is recursively unwrapped. + * Returns the value as a plain Java boxed value, that is, a {@code String}, + * {@code Number}, {@code Boolean}, {@code Map}, + * {@code List}, or {@code null}, matching the {@link #valueType()} + * of this {@code ConfigValue}. If the value is a {@link ConfigObject} or + * {@link ConfigList}, it is recursively unwrapped. */ Object unwrapped(); + /** + * Renders the config value as a HOCON string. This method is primarily + * intended for debugging, so it tries to add helpful comments and + * whitespace. If the config value has not been resolved (see + * {@link Config#resolve}), it's possible that it can't be rendered as valid + * HOCON. In that case the rendering should still be useful for debugging + * but you might not be able to parse it. + * + * @return the rendered value + */ + String render(); + @Override ConfigValue withFallback(ConfigMergeable other); } diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigValueFactory.java b/akka-actor/src/main/java/com/typesafe/config/ConfigValueFactory.java index fd63a6ffe5..2f381f9ad1 100644 --- a/akka-actor/src/main/java/com/typesafe/config/ConfigValueFactory.java +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigValueFactory.java @@ -3,7 +3,6 @@ */ package com.typesafe.config; -import java.util.Collection; import java.util.Map; import com.typesafe.config.impl.ConfigImpl; @@ -14,6 +13,9 @@ import com.typesafe.config.impl.ConfigImpl; * data structures. */ public final class ConfigValueFactory { + private ConfigValueFactory() { + } + /** * Creates a ConfigValue from a plain Java boxed value, which may be a * Boolean, Number, String, Map, Iterable, or null. A Map must be a Map from @@ -23,23 +25,27 @@ public final class ConfigValueFactory { * the Iterable is not an ordered collection, results could be strange, * since ConfigList is ordered. * + *

* In a Map passed to fromAnyRef(), the map's keys are plain keys, not path * expressions. So if your Map has a key "foo.bar" then you will get one * object with a key called "foo.bar", rather than an object with a key * "foo" containing another object with a key "bar". * + *

* The originDescription will be used to set the origin() field on the * ConfigValue. It should normally be the name of the file the values came * from, or something short describing the value such as "default settings". * The originDescription is prefixed to error messages so users can tell * where problematic values are coming from. * + *

* Supplying the result of ConfigValue.unwrapped() to this function is * guaranteed to work and should give you back a ConfigValue that matches * the one you unwrapped. The re-wrapped ConfigValue will lose some * information that was present in the original such as its origin, but it * will have matching values. * + *

* This function throws if you supply a value that cannot be converted to a * ConfigValue, but supplying such a value is a bug in your program, so you * should never handle the exception. Just fix your program (or report a bug @@ -57,23 +63,25 @@ public final class ConfigValueFactory { /** * See the fromAnyRef() documentation for details. This is a typesafe - * wrapper that only works on Map and returns ConfigObject rather than - * ConfigValue. + * wrapper that only works on {@link java.util.Map} and returns + * {@link ConfigObject} rather than {@link ConfigValue}. * + *

* If your Map has a key "foo.bar" then you will get one object with a key * called "foo.bar", rather than an object with a key "foo" containing * another object with a key "bar". The keys in the map are keys; not path * expressions. That is, the Map corresponds exactly to a single - * ConfigObject. The keys will not be parsed or modified, and the values are - * wrapped in ConfigValue. To get nested ConfigObject, some of the values in - * the map would have to be more maps. + * {@code ConfigObject}. The keys will not be parsed or modified, and the + * values are wrapped in ConfigValue. To get nested {@code ConfigObject}, + * some of the values in the map would have to be more maps. * - * There is a separate fromPathMap() that interprets the keys in the map as - * path expressions. + *

+ * See also {@link ConfigFactory#parseMap(Map,String)} which interprets the + * keys in the map as path expressions. * * @param values * @param originDescription - * @return + * @return a new {@link ConfigObject} value */ public static ConfigObject fromMap(Map values, String originDescription) { @@ -82,12 +90,12 @@ public final class ConfigValueFactory { /** * See the fromAnyRef() documentation for details. This is a typesafe - * wrapper that only works on Iterable and returns ConfigList rather than - * ConfigValue. + * wrapper that only works on {@link java.util.Iterable} and returns + * {@link ConfigList} rather than {@link ConfigValue}. * * @param values * @param originDescription - * @return + * @return a new {@link ConfigList} value */ public static ConfigList fromIterable(Iterable values, String originDescription) { @@ -95,35 +103,39 @@ public final class ConfigValueFactory { } /** - * See the other overload of fromAnyRef() for details, this one just uses a - * default origin description. + * See the other overload {@link #fromAnyRef(Object,String)} for details, + * this one just uses a default origin description. * * @param object - * @return + * @return a new {@link ConfigValue} */ public static ConfigValue fromAnyRef(Object object) { return fromAnyRef(object, null); } /** - * See the other overload of fromMap() for details, this one just uses a - * default origin description. + * See the other overload {@link #fromMap(Map,String)} for details, this one + * just uses a default origin description. + * + *

+ * See also {@link ConfigFactory#parseMap(Map)} which interprets the keys in + * the map as path expressions. * * @param values - * @return + * @return a new {@link ConfigObject} */ public static ConfigObject fromMap(Map values) { return fromMap(values, null); } /** - * See the other overload of fromIterable() for details, this one just uses - * a default origin description. + * See the other overload of {@link #fromIterable(Iterable, String)} for + * details, this one just uses a default origin description. * * @param values - * @return + * @return a new {@link ConfigList} */ - public static ConfigList fromIterable(Collection values) { + public static ConfigList fromIterable(Iterable values) { return fromIterable(values, null); } } diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigValueType.java b/akka-actor/src/main/java/com/typesafe/config/ConfigValueType.java index 0ead8e1965..981cb6d189 100644 --- a/akka-actor/src/main/java/com/typesafe/config/ConfigValueType.java +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigValueType.java @@ -4,7 +4,8 @@ package com.typesafe.config; /** - * The type of a configuration value. Value types follow the JSON type schema. + * The type of a configuration value (following the JSON type schema). */ public enum ConfigValueType { OBJECT, LIST, NUMBER, BOOLEAN, NULL, STRING diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java b/akka-actor/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java index d9d41e3f04..428a7b2b3f 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java @@ -35,6 +35,11 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements return config; } + @Override + public AbstractConfigObject toFallbackValue() { + return this; + } + /** * This looks up the key with no transformation or type conversion of any * kind, and returns null if the key is not present. @@ -61,19 +66,23 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements * (just returns null if path not found). Does however resolve the path, if * resolver != null. */ - protected ConfigValue peekPath(Path path, SubstitutionResolver resolver, + protected AbstractConfigValue peekPath(Path path, SubstitutionResolver resolver, int depth, ConfigResolveOptions options) { return peekPath(this, path, resolver, depth, options); } - private static ConfigValue peekPath(AbstractConfigObject self, Path path, + AbstractConfigValue peekPath(Path path) { + return peekPath(this, path, null, 0, null); + } + + private static AbstractConfigValue peekPath(AbstractConfigObject self, Path path, SubstitutionResolver resolver, int depth, ConfigResolveOptions options) { String key = path.first(); Path next = path.remainder(); if (next == null) { - ConfigValue v = self.peek(key, resolver, depth, options); + AbstractConfigValue v = self.peek(key, resolver, depth, options); return v; } else { // it's important to ONLY resolve substitutions here, not @@ -131,6 +140,7 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements if (ignoresFallbacks()) throw new ConfigException.BugOrBroken("should not be reached"); + boolean changed = false; boolean allResolved = true; Map merged = new HashMap(); Set allKeys = new HashSet(); @@ -146,12 +156,26 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements kept = first; else kept = first.withFallback(second); + merged.put(key, kept); + + if (first != kept) + changed = true; + if (kept.resolveStatus() == ResolveStatus.UNRESOLVED) allResolved = false; } - return new SimpleConfigObject(mergeOrigins(this, fallback), merged, - ResolveStatus.fromBoolean(allResolved), fallback.ignoresFallbacks()); + + ResolveStatus newResolveStatus = ResolveStatus.fromBoolean(allResolved); + boolean newIgnoresFallbacks = fallback.ignoresFallbacks(); + + if (changed) + return new SimpleConfigObject(mergeOrigins(this, fallback), merged, newResolveStatus, + newIgnoresFallbacks); + else if (newResolveStatus != resolveStatus() || newIgnoresFallbacks != ignoresFallbacks()) + return newCopy(newResolveStatus, newIgnoresFallbacks); + else + return this; } @Override @@ -164,18 +188,13 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements if (stack.isEmpty()) throw new ConfigException.BugOrBroken( "can't merge origins on empty list"); - final String prefix = "merge of "; - StringBuilder sb = new StringBuilder(); + List origins = new ArrayList(); ConfigOrigin firstOrigin = null; int numMerged = 0; for (AbstractConfigValue v : stack) { if (firstOrigin == null) firstOrigin = v.origin(); - String desc = v.origin().description(); - if (desc.startsWith(prefix)) - desc = desc.substring(prefix.length()); - if (v instanceof AbstractConfigObject && ((AbstractConfigObject) v).resolveStatus() == ResolveStatus.RESOLVED && ((ConfigObject) v).isEmpty()) { @@ -183,22 +202,17 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements // config in the description, since they are // likely to be "implementation details" } else { - sb.append(desc); - sb.append(","); + origins.add(v.origin()); numMerged += 1; } } - if (numMerged > 0) { - sb.setLength(sb.length() - 1); // chop comma - if (numMerged > 1) { - return new SimpleConfigOrigin(prefix + sb.toString()); - } else { - return new SimpleConfigOrigin(sb.toString()); - } - } else { - // the configs were all empty. - return firstOrigin; + + if (numMerged == 0) { + // the configs were all empty, so just use the first one + origins.add(firstOrigin); } + + return SimpleConfigOrigin.mergeOrigins(origins); } static ConfigOrigin mergeOrigins(AbstractConfigObject... stack) { @@ -210,6 +224,8 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements Map changes = null; for (String k : keySet()) { AbstractConfigValue v = peek(k); + // "modified" may be null, which means remove the child; + // to do that we put null in the "changes" map. AbstractConfigValue modified = modifier.modifyChild(v); if (modified != v) { if (changes == null) @@ -223,7 +239,12 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements Map modified = new HashMap(); for (String k : keySet()) { if (changes.containsKey(k)) { - modified.put(k, changes.get(k)); + AbstractConfigValue newValue = changes.get(k); + if (newValue != null) { + modified.put(k, newValue); + } else { + // remove this child; don't put it in the new map. + } } else { modified.put(k, peek(k)); } @@ -271,20 +292,36 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements } @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(valueType().name()); - sb.append("("); - for (String k : keySet()) { - sb.append(k); - sb.append("->"); - sb.append(peek(k).toString()); - sb.append(","); + protected void render(StringBuilder sb, int indent, boolean formatted) { + if (isEmpty()) { + sb.append("{}"); + } else { + sb.append("{"); + if (formatted) + sb.append('\n'); + for (String k : keySet()) { + AbstractConfigValue v = peek(k); + if (formatted) { + indent(sb, indent + 1); + sb.append("# "); + sb.append(v.origin().description()); + sb.append("\n"); + indent(sb, indent + 1); + } + v.render(sb, indent + 1, k, formatted); + sb.append(","); + if (formatted) + sb.append('\n'); + } + // chop comma or newline + sb.setLength(sb.length() - 1); + if (formatted) { + sb.setLength(sb.length() - 1); // also chop comma + sb.append("\n"); // put a newline back + indent(sb, indent); + } + sb.append("}"); } - if (!keySet().isEmpty()) - sb.setLength(sb.length() - 1); // chop comma - sb.append(")"); - return sb.toString(); } private static boolean mapEquals(Map a, diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java b/akka-actor/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java index 2125cf723b..1bec6ec536 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java @@ -16,7 +16,7 @@ import com.typesafe.config.ConfigValue; * improperly-factored and non-modular code. Please don't add parent(). * */ -abstract class AbstractConfigValue implements ConfigValue { +abstract class AbstractConfigValue implements ConfigValue, MergeableValue { final private ConfigOrigin origin; @@ -72,7 +72,7 @@ abstract class AbstractConfigValue implements ConfigValue { } @Override - public AbstractConfigValue toValue() { + public AbstractConfigValue toFallbackValue() { return this; } @@ -110,7 +110,7 @@ abstract class AbstractConfigValue implements ConfigValue { if (ignoresFallbacks()) { return this; } else { - ConfigValue other = mergeable.toValue(); + ConfigValue other = ((MergeableValue) mergeable).toFallbackValue(); if (other instanceof Unmergeable) { return mergedWithTheUnmergeable((Unmergeable) other); @@ -162,8 +162,39 @@ abstract class AbstractConfigValue implements ConfigValue { } @Override - public String toString() { - return valueType().name() + "(" + unwrapped() + ")"; + public final String toString() { + StringBuilder sb = new StringBuilder(); + render(sb, 0, null /* atKey */, false /* formatted */); + return getClass().getSimpleName() + "(" + sb.toString() + ")"; + } + + protected static void indent(StringBuilder sb, int indent) { + int remaining = indent; + while (remaining > 0) { + sb.append(" "); + --remaining; + } + } + + protected void render(StringBuilder sb, int indent, String atKey, boolean formatted) { + if (atKey != null) { + sb.append(ConfigUtil.renderJsonString(atKey)); + sb.append(" : "); + } + render(sb, indent, formatted); + } + + protected void render(StringBuilder sb, int indent, boolean formatted) { + Object u = unwrapped(); + sb.append(u.toString()); + } + + + @Override + public final String render() { + StringBuilder sb = new StringBuilder(); + render(sb, 0, null, true /* formatted */); + return sb.toString(); } // toString() is a debugging-oriented string but this is defined diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java index 0c20aa5701..b916d9a0a7 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java @@ -5,6 +5,7 @@ package com.typesafe.config.impl; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import com.typesafe.config.ConfigException; @@ -76,10 +77,12 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements AbstractConfigValue merged = null; for (AbstractConfigValue v : stack) { AbstractConfigValue resolved = resolver.resolve(v, depth, options); - if (merged == null) - merged = resolved; - else - merged = merged.withFallback(resolved); + if (resolved != null) { + if (merged == null) + merged = resolved; + else + merged = merged.withFallback(resolved); + } } return merged; @@ -160,16 +163,58 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements } @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("DELAYED_MERGE"); - sb.append("("); - for (Object s : stack) { - sb.append(s.toString()); - sb.append(","); + protected void render(StringBuilder sb, int indent, String atKey, boolean formatted) { + render(stack, sb, indent, atKey, formatted); + } + + // static method also used by ConfigDelayedMergeObject. + static void render(List stack, StringBuilder sb, int indent, String atKey, + boolean formatted) { + if (formatted) { + sb.append("# unresolved merge of " + stack.size() + " values follows (\n"); + if (atKey == null) { + indent(sb, indent); + sb.append("# this unresolved merge will not be parseable because it's at the root of the object\n"); + sb.append("# the HOCON format has no way to list multiple root objects in a single file\n"); + } + } + + List reversed = new ArrayList(); + reversed.addAll(stack); + Collections.reverse(reversed); + + int i = 0; + for (AbstractConfigValue v : reversed) { + if (formatted) { + indent(sb, indent); + if (atKey != null) { + sb.append("# unmerged value " + i + " for key " + + ConfigUtil.renderJsonString(atKey) + " from "); + } else { + sb.append("# unmerged value " + i + " from "); + } + i += 1; + sb.append(v.origin().description()); + sb.append("\n"); + indent(sb, indent); + } + + if (atKey != null) { + sb.append(ConfigUtil.renderJsonString(atKey)); + sb.append(" : "); + } + v.render(sb, indent, formatted); + sb.append(","); + if (formatted) + sb.append('\n'); + } + // chop comma or newline + sb.setLength(sb.length() - 1); + if (formatted) { + sb.setLength(sb.length() - 1); // also chop comma + sb.append("\n"); // put a newline back + indent(sb, indent); + sb.append("# ) end of unresolved merge\n"); } - sb.setLength(sb.length() - 1); // chop comma - sb.append(")"); - return sb.toString(); } } diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigDelayedMergeObject.java b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigDelayedMergeObject.java index 6f381afc48..5669f62f34 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigDelayedMergeObject.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigDelayedMergeObject.java @@ -139,17 +139,8 @@ class ConfigDelayedMergeObject extends AbstractConfigObject implements } @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("DELAYED_MERGE_OBJECT"); - sb.append("("); - for (Object s : stack) { - sb.append(s.toString()); - sb.append(","); - } - sb.setLength(sb.length() - 1); // chop comma - sb.append(")"); - return sb.toString(); + protected void render(StringBuilder sb, int indent, String atKey, boolean formatted) { + ConfigDelayedMerge.render(stack, sb, indent, atKey, formatted); } private static ConfigException notResolved() { diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigImpl.java b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigImpl.java index 8c2bd01ec1..8c016d6f98 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigImpl.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigImpl.java @@ -19,7 +19,6 @@ import com.typesafe.config.ConfigObject; import com.typesafe.config.ConfigOrigin; import com.typesafe.config.ConfigParseOptions; import com.typesafe.config.ConfigParseable; -import com.typesafe.config.ConfigRoot; import com.typesafe.config.ConfigSyntax; import com.typesafe.config.ConfigValue; @@ -45,8 +44,7 @@ public class ConfigImpl { obj = p.parse(p.options().setAllowMissing( options.getAllowMissing())); } else { - obj = SimpleConfigObject.emptyMissing(new SimpleConfigOrigin( - name)); + obj = SimpleConfigObject.emptyMissing(SimpleConfigOrigin.newSimple(name)); } } else { ConfigParseable confHandle = source.nameToParseable(name + ".conf"); @@ -56,13 +54,13 @@ public class ConfigImpl { if (!options.getAllowMissing() && confHandle == null && jsonHandle == null && propsHandle == null) { - throw new ConfigException.IO(new SimpleConfigOrigin(name), + throw new ConfigException.IO(SimpleConfigOrigin.newSimple(name), "No config files {.conf,.json,.properties} found"); } ConfigSyntax syntax = options.getSyntax(); - obj = SimpleConfigObject.empty(new SimpleConfigOrigin(name)); + obj = SimpleConfigObject.empty(SimpleConfigOrigin.newSimple(name)); if (confHandle != null && (syntax == null || syntax == ConfigSyntax.CONF)) { obj = confHandle.parse(confHandle.options() @@ -89,39 +87,13 @@ public class ConfigImpl { return obj; } - private static String makeResourceBasename(Path path) { - StringBuilder sb = new StringBuilder("/"); - String next = path.first(); - Path remaining = path.remainder(); - while (next != null) { - sb.append(next); - sb.append('-'); - - if (remaining == null) - break; - - next = remaining.first(); - remaining = remaining.remainder(); - } - sb.setLength(sb.length() - 1); // chop extra hyphen - return sb.toString(); - } - /** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */ - public static ConfigObject parseResourcesForPath(String expression, - final ConfigParseOptions baseOptions) { - Path path = Parser.parsePath(expression); - String basename = makeResourceBasename(path); - return parseResourceAnySyntax(ConfigImpl.class, basename, baseOptions); - } - - /** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */ - public static ConfigObject parseResourceAnySyntax(final Class klass, + public static ConfigObject parseResourcesAnySyntax(final Class klass, String resourceBasename, final ConfigParseOptions baseOptions) { NameSource source = new NameSource() { @Override public ConfigParseable nameToParseable(String name) { - return Parseable.newResource(klass, name, baseOptions); + return Parseable.newResources(klass, name, baseOptions); } }; return fromBasename(source, resourceBasename, baseOptions); @@ -139,16 +111,9 @@ public class ConfigImpl { return fromBasename(source, basename.getPath(), baseOptions); } - /** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */ - public static ConfigRoot emptyRoot(String rootPath, String originDescription) { - String desc = originDescription != null ? originDescription : rootPath; - return emptyObject(desc).toConfig().asRoot( - Path.newPath(rootPath)); - } - static AbstractConfigObject emptyObject(String originDescription) { - ConfigOrigin origin = originDescription != null ? new SimpleConfigOrigin( - originDescription) : null; + ConfigOrigin origin = originDescription != null ? SimpleConfigOrigin + .newSimple(originDescription) : null; return emptyObject(origin); } @@ -162,8 +127,8 @@ public class ConfigImpl { } // default origin for values created with fromAnyRef and no origin specified - final private static ConfigOrigin defaultValueOrigin = new SimpleConfigOrigin( - "hardcoded value"); + final private static ConfigOrigin defaultValueOrigin = SimpleConfigOrigin + .newSimple("hardcoded value"); final private static ConfigBoolean defaultTrueValue = new ConfigBoolean( defaultValueOrigin, true); final private static ConfigBoolean defaultFalseValue = new ConfigBoolean( @@ -196,7 +161,7 @@ public class ConfigImpl { if (originDescription == null) return defaultValueOrigin; else - return new SimpleConfigOrigin(originDescription); + return SimpleConfigOrigin.newSimple(originDescription); } /** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */ @@ -290,17 +255,6 @@ public class ConfigImpl { } } - /** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */ - public static ConfigRoot systemPropertiesRoot(String rootPath) { - Path path = Parser.parsePath(rootPath); - try { - return systemPropertiesAsConfigObject().toConfig().getConfig(rootPath) - .asRoot(path); - } catch (ConfigException.Missing e) { - return emptyObject("system properties").toConfig().asRoot(path); - } - } - private static class SimpleIncluder implements ConfigIncluder { private ConfigIncluder fallback; @@ -346,29 +300,34 @@ public class ConfigImpl { } } - private static ConfigIncluder defaultIncluder = null; - - synchronized static ConfigIncluder defaultIncluder() { - if (defaultIncluder == null) { - defaultIncluder = new SimpleIncluder(null); - } - return defaultIncluder; + private static class DefaultIncluderHolder { + static final ConfigIncluder defaultIncluder = new SimpleIncluder(null); } - private static AbstractConfigObject systemProperties = null; - - synchronized static AbstractConfigObject systemPropertiesAsConfigObject() { - if (systemProperties == null) { - systemProperties = loadSystemProperties(); + static ConfigIncluder defaultIncluder() { + try { + return DefaultIncluderHolder.defaultIncluder; + } catch (ExceptionInInitializerError e) { + throw ConfigUtil.extractInitializerError(e); } - return systemProperties; } private static AbstractConfigObject loadSystemProperties() { - return (AbstractConfigObject) Parseable.newProperties( - System.getProperties(), - ConfigParseOptions.defaults().setOriginDescription( - "system properties")).parse(); + return (AbstractConfigObject) Parseable.newProperties(System.getProperties(), + ConfigParseOptions.defaults().setOriginDescription("system properties")).parse(); + } + + private static class SystemPropertiesHolder { + // this isn't final due to the reloadSystemPropertiesConfig() hack below + static AbstractConfigObject systemProperties = loadSystemProperties(); + } + + static AbstractConfigObject systemPropertiesAsConfigObject() { + try { + return SystemPropertiesHolder.systemProperties; + } catch (ExceptionInInitializerError e) { + throw ConfigUtil.extractInitializerError(e); + } } /** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */ @@ -376,18 +335,10 @@ public class ConfigImpl { return systemPropertiesAsConfigObject().toConfig(); } - // this is a hack to let us set system props in the test suite - synchronized static void dropSystemPropertiesConfig() { - systemProperties = null; - } - - private static AbstractConfigObject envVariables = null; - - synchronized static AbstractConfigObject envVariablesAsConfigObject() { - if (envVariables == null) { - envVariables = loadEnvVariables(); - } - return envVariables; + // this is a hack to let us set system props in the test suite. + // obviously not thread-safe. + static void reloadSystemPropertiesConfig() { + SystemPropertiesHolder.systemProperties = loadSystemProperties(); } private static AbstractConfigObject loadEnvVariables() { @@ -395,15 +346,45 @@ public class ConfigImpl { Map m = new HashMap(); for (Map.Entry entry : env.entrySet()) { String key = entry.getKey(); - m.put(key, new ConfigString( - new SimpleConfigOrigin("env var " + key), entry.getValue())); + m.put(key, + new ConfigString(SimpleConfigOrigin.newSimple("env var " + key), entry + .getValue())); } - return new SimpleConfigObject(new SimpleConfigOrigin("env variables"), + return new SimpleConfigObject(SimpleConfigOrigin.newSimple("env variables"), m, ResolveStatus.RESOLVED, false /* ignoresFallbacks */); } + private static class EnvVariablesHolder { + static final AbstractConfigObject envVariables = loadEnvVariables(); + } + + static AbstractConfigObject envVariablesAsConfigObject() { + try { + return EnvVariablesHolder.envVariables; + } catch (ExceptionInInitializerError e) { + throw ConfigUtil.extractInitializerError(e); + } + } + /** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */ public static Config envVariablesAsConfig() { return envVariablesAsConfigObject().toConfig(); } + + private static class ReferenceHolder { + private static final Config unresolvedResources = Parseable + .newResources(ConfigImpl.class, "/reference.conf", ConfigParseOptions.defaults()) + .parse().toConfig(); + static final Config referenceConfig = systemPropertiesAsConfig().withFallback( + unresolvedResources).resolve(); + } + + /** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */ + public static Config defaultReference() { + try { + return ReferenceHolder.referenceConfig; + } catch (ExceptionInInitializerError e) { + throw ConfigUtil.extractInitializerError(e); + } + } } diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigNull.java b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigNull.java index ea8976e340..a45d2dbc40 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigNull.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigNull.java @@ -34,4 +34,9 @@ final class ConfigNull extends AbstractConfigValue { String transformToString() { return "null"; } + + @Override + protected void render(StringBuilder sb, int indent, boolean formatted) { + sb.append("null"); + } } diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigString.java b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigString.java index e054b6c91f..dd8a5fa3b0 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigString.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigString.java @@ -29,4 +29,9 @@ final class ConfigString extends AbstractConfigValue { String transformToString() { return value; } + + @Override + protected void render(StringBuilder sb, int indent, boolean formatted) { + sb.append(ConfigUtil.renderJsonString(value)); + } } diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigSubstitution.java b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigSubstitution.java index 2c5531e74f..8f1b43571c 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigSubstitution.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigSubstitution.java @@ -22,8 +22,8 @@ import com.typesafe.config.ConfigValueType; final class ConfigSubstitution extends AbstractConfigValue implements Unmergeable { - // this is a list of String and Path where the Path - // have to be resolved to values, then if there's more + // this is a list of String and SubstitutionExpression where the + // SubstitutionExpression has to be resolved to values, then if there's more // than one piece everything is stringified and concatenated final private List pieces; // the length of any prefixes added with relativized() @@ -40,19 +40,24 @@ final class ConfigSubstitution extends AbstractConfigValue implements this.pieces = pieces; this.prefixLength = prefixLength; this.ignoresFallbacks = ignoresFallbacks; + for (Object p : pieces) { + if (p instanceof Path) + throw new RuntimeException("broken here"); + } } @Override public ConfigValueType valueType() { throw new ConfigException.NotResolved( - "tried to get value type on an unresolved substitution: " + "need to call resolve() on root config; tried to get value type on an unresolved substitution: " + this); } @Override public Object unwrapped() { throw new ConfigException.NotResolved( - "tried to unwrap an unresolved substitution: " + this); + "need to call resolve() on root config; tried to unwrap an unresolved substitution: " + + this); } @Override @@ -124,28 +129,27 @@ final class ConfigSubstitution extends AbstractConfigValue implements return result; } - private ConfigValue resolve(SubstitutionResolver resolver, Path subst, + private ConfigValue resolve(SubstitutionResolver resolver, SubstitutionExpression subst, int depth, ConfigResolveOptions options) { - ConfigValue result = findInObject(resolver.root(), resolver, subst, + // First we look up the full path, which means relative to the + // included file if we were not a root file + ConfigValue result = findInObject(resolver.root(), resolver, subst.path(), depth, options); - // when looking up system props and env variables, - // we don't want the prefix that was added when - // we were included in another file. - Path unprefixed = subst.subPath(prefixLength); - - if (result == null && options.getUseSystemProperties()) { - result = findInObject(ConfigImpl.systemPropertiesAsConfigObject(), null, - unprefixed, depth, options); - } - - if (result == null && options.getUseSystemEnvironment()) { - result = findInObject(ConfigImpl.envVariablesAsConfigObject(), null, - unprefixed, depth, options); - } - if (result == null) { - result = new ConfigNull(origin()); + // Then we want to check relative to the root file. We don't + // want the prefix we were included at to be used when looking up + // env variables either. + Path unprefixed = subst.path().subPath(prefixLength); + + if (result == null && prefixLength > 0) { + result = findInObject(resolver.root(), resolver, unprefixed, depth, options); + } + + if (result == null && options.getUseSystemEnvironment()) { + result = findInObject(ConfigImpl.envVariablesAsConfigObject(), null, unprefixed, + depth, options); + } } return result; @@ -160,28 +164,40 @@ final class ConfigSubstitution extends AbstractConfigValue implements if (p instanceof String) { sb.append((String) p); } else { - ConfigValue v = resolve(resolver, (Path) p, depth, options); - switch (v.valueType()) { - case NULL: - // nothing; becomes empty string - break; - case LIST: - case OBJECT: - // cannot substitute lists and objects into strings - throw new ConfigException.WrongType(v.origin(), - ((Path) p).render(), - "not a list or object", v.valueType().name()); - default: - sb.append(((AbstractConfigValue) v).transformToString()); + SubstitutionExpression exp = (SubstitutionExpression) p; + ConfigValue v = resolve(resolver, exp, depth, options); + + if (v == null) { + if (exp.optional()) { + // append nothing to StringBuilder + } else { + throw new ConfigException.UnresolvedSubstitution(origin(), + exp.toString()); + } + } else { + switch (v.valueType()) { + case LIST: + case OBJECT: + // cannot substitute lists and objects into strings + throw new ConfigException.WrongType(v.origin(), exp.path().render(), + "not a list or object", v.valueType().name()); + default: + sb.append(((AbstractConfigValue) v).transformToString()); + } } } } return new ConfigString(origin(), sb.toString()); } else { - if (!(pieces.get(0) instanceof Path)) + if (!(pieces.get(0) instanceof SubstitutionExpression)) throw new ConfigException.BugOrBroken( "ConfigSubstitution should never contain a single String piece"); - return resolve(resolver, (Path) pieces.get(0), depth, options); + SubstitutionExpression exp = (SubstitutionExpression) pieces.get(0); + ConfigValue v = resolve(resolver, exp, depth, options); + if (v == null && !exp.optional()) { + throw new ConfigException.UnresolvedSubstitution(origin(), exp.toString()); + } + return v; } } @@ -210,8 +226,10 @@ final class ConfigSubstitution extends AbstractConfigValue implements ConfigSubstitution relativized(Path prefix) { List newPieces = new ArrayList(); for (Object p : pieces) { - if (p instanceof Path) { - newPieces.add(((Path) p).prepend(prefix)); + if (p instanceof SubstitutionExpression) { + SubstitutionExpression exp = (SubstitutionExpression) p; + + newPieces.add(exp.changePath(exp.path().prepend(prefix))); } else { newPieces.add(p); } @@ -243,16 +261,13 @@ final class ConfigSubstitution extends AbstractConfigValue implements } @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("SUBST"); - sb.append("("); + protected void render(StringBuilder sb, int indent, boolean formatted) { for (Object p : pieces) { - sb.append(p.toString()); - sb.append(","); + if (p instanceof SubstitutionExpression) { + sb.append(p.toString()); + } else { + sb.append(ConfigUtil.renderJsonString((String) p)); + } } - sb.setLength(sb.length() - 1); // chop comma - sb.append(")"); - return sb.toString(); } } diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigUtil.java b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigUtil.java index bfd8f05521..6f7b2c5aaa 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigUtil.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigUtil.java @@ -3,6 +3,12 @@ */ package com.typesafe.config.impl; +import java.io.File; +import java.net.URISyntaxException; +import java.net.URL; + +import com.typesafe.config.ConfigException; + /** This is public just for the "config" package to use, don't touch it */ final public class ConfigUtil { @@ -118,4 +124,26 @@ final public class ConfigUtil { } return s.substring(start, end); } + + /** This is public just for the "config" package to use, don't touch it! */ + public static ConfigException extractInitializerError(ExceptionInInitializerError e) { + Throwable cause = e.getCause(); + if (cause != null && cause instanceof ConfigException) { + return (ConfigException) cause; + } else { + throw e; + } + } + + static File urlToFile(URL url) { + // this isn't really right, clearly, but not sure what to do. + try { + // this will properly handle hex escapes, etc. + return new File(url.toURI()); + } catch (URISyntaxException e) { + // this handles some stuff like file:///c:/Whatever/ + // apparently but mangles handling of hex escapes + return new File(url.getPath()); + } + } } diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/MergeableValue.java b/akka-actor/src/main/java/com/typesafe/config/impl/MergeableValue.java new file mode 100644 index 0000000000..38dba70cc2 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/MergeableValue.java @@ -0,0 +1,9 @@ +package com.typesafe.config.impl; + +import com.typesafe.config.ConfigMergeable; +import com.typesafe.config.ConfigValue; + +interface MergeableValue extends ConfigMergeable { + // converts a Config to its root object and a ConfigValue to itself + ConfigValue toFallbackValue(); +} diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/OriginType.java b/akka-actor/src/main/java/com/typesafe/config/impl/OriginType.java new file mode 100644 index 0000000000..b8e990c091 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/OriginType.java @@ -0,0 +1,8 @@ +package com.typesafe.config.impl; + +enum OriginType { + GENERIC, + FILE, + URL, + RESOURCE +} diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/Parseable.java b/akka-actor/src/main/java/com/typesafe/config/impl/Parseable.java index eef9e75b2a..e5b67540de 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/Parseable.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/Parseable.java @@ -17,6 +17,7 @@ import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.util.Enumeration; import java.util.Iterator; import java.util.Properties; @@ -39,6 +40,7 @@ import com.typesafe.config.ConfigValue; public abstract class Parseable implements ConfigParseable { private ConfigIncludeContext includeContext; private ConfigParseOptions initialOptions; + private ConfigOrigin initialOrigin; protected Parseable() { @@ -54,9 +56,6 @@ public abstract class Parseable implements ConfigParseable { } ConfigParseOptions modified = baseOptions.setSyntax(syntax); - if (modified.getOriginDescription() == null) - modified = modified.setOriginDescription(originDescription()); - modified = modified.appendIncluder(ConfigImpl.defaultIncluder()); return modified; @@ -71,6 +70,11 @@ public abstract class Parseable implements ConfigParseable { return Parseable.this.relativeTo(filename); } }; + + if (initialOptions.getOriginDescription() != null) + initialOrigin = SimpleConfigOrigin.newSimple(initialOptions.getOriginDescription()); + else + initialOrigin = createOrigin(); } // the general idea is that any work should be in here, not in the @@ -108,33 +112,26 @@ public abstract class Parseable implements ConfigParseable { return forceParsedToObject(parseValue(baseOptions)); } - AbstractConfigValue parseValue(ConfigParseOptions baseOptions) { - // note that we are NOT using our "options" and "origin" fields, + final AbstractConfigValue parseValue(ConfigParseOptions baseOptions) { + // note that we are NOT using our "initialOptions", // but using the ones from the passed-in options. The idea is that // callers can get our original options and then parse with different // ones if they want. ConfigParseOptions options = fixupOptions(baseOptions); - ConfigOrigin origin = new SimpleConfigOrigin( - options.getOriginDescription()); + + // passed-in options can override origin + ConfigOrigin origin; + if (options.getOriginDescription() != null) + origin = SimpleConfigOrigin.newSimple(options.getOriginDescription()); + else + origin = initialOrigin; return parseValue(origin, options); } - protected AbstractConfigValue parseValue(ConfigOrigin origin, + final private AbstractConfigValue parseValue(ConfigOrigin origin, ConfigParseOptions finalOptions) { try { - Reader reader = reader(); - try { - if (finalOptions.getSyntax() == ConfigSyntax.PROPERTIES) { - return PropertiesParser.parse(reader, origin); - } else { - Iterator tokens = Tokenizer.tokenize(origin, reader, - finalOptions.getSyntax()); - return Parser.parse(tokens, origin, finalOptions, - includeContext()); - } - } finally { - reader.close(); - } + return rawParseValue(origin, finalOptions); } catch (IOException e) { if (finalOptions.getAllowMissing()) { return SimpleConfigObject.emptyMissing(origin); @@ -144,6 +141,28 @@ public abstract class Parseable implements ConfigParseable { } } + // this is parseValue without post-processing the IOException or handling + // options.getAllowMissing() + protected AbstractConfigValue rawParseValue(ConfigOrigin origin, ConfigParseOptions finalOptions) + throws IOException { + Reader reader = reader(); + try { + return rawParseValue(reader, origin, finalOptions); + } finally { + reader.close(); + } + } + + protected AbstractConfigValue rawParseValue(Reader reader, ConfigOrigin origin, + ConfigParseOptions finalOptions) throws IOException { + if (finalOptions.getSyntax() == ConfigSyntax.PROPERTIES) { + return PropertiesParser.parse(reader, origin); + } else { + Iterator tokens = Tokenizer.tokenize(origin, reader, finalOptions.getSyntax()); + return Parser.parse(tokens, origin, finalOptions, includeContext()); + } + } + public ConfigObject parse() { return forceParsedToObject(parseValue(options())); } @@ -152,13 +171,13 @@ public abstract class Parseable implements ConfigParseable { return parseValue(options()); } - abstract String originDescription(); - @Override - public URL url() { - return null; + public final ConfigOrigin origin() { + return initialOrigin; } + protected abstract ConfigOrigin createOrigin(); + @Override public ConfigParseOptions options() { return initialOptions; @@ -228,32 +247,18 @@ public abstract class Parseable implements ConfigParseable { } } - private final static class ParseableInputStream extends Parseable { - final private InputStream input; + static File relativeTo(File file, String filename) { + File child = new File(filename); - ParseableInputStream(InputStream input, ConfigParseOptions options) { - this.input = input; - postConstruct(options); - } + if (child.isAbsolute()) + return null; - @Override - protected Reader reader() { - return doNotClose(readerFromStream(input)); - } + File parent = file.getParentFile(); - @Override - String originDescription() { - return "InputStream"; - } - } - - /** - * note that we will never close this stream; you have to do it when parsing - * is complete. - */ - public static Parseable newInputStream(InputStream input, - ConfigParseOptions options) { - return new ParseableInputStream(input, options); + if (parent == null) + return null; + else + return new File(parent, filename); } private final static class ParseableReader extends Parseable { @@ -270,8 +275,8 @@ public abstract class Parseable implements ConfigParseable { } @Override - String originDescription() { - return "Reader"; + protected ConfigOrigin createOrigin() { + return SimpleConfigOrigin.newSimple("Reader"); } } @@ -297,8 +302,8 @@ public abstract class Parseable implements ConfigParseable { } @Override - String originDescription() { - return "String"; + protected ConfigOrigin createOrigin() { + return SimpleConfigOrigin.newSimple("String"); } } @@ -335,13 +340,8 @@ public abstract class Parseable implements ConfigParseable { } @Override - String originDescription() { - return input.toExternalForm(); - } - - @Override - public URL url() { - return input; + protected ConfigOrigin createOrigin() { + return SimpleConfigOrigin.newURL(input); } @Override @@ -352,7 +352,13 @@ public abstract class Parseable implements ConfigParseable { } public static Parseable newURL(URL input, ConfigParseOptions options) { - return new ParseableURL(input, options); + // we want file: URLs and files to always behave the same, so switch + // to a file if it's a file: URL + if (input.getProtocol().equals("file")) { + return newFile(ConfigUtil.urlToFile(input), options); + } else { + return new ParseableURL(input, options); + } } private final static class ParseableFile extends Parseable { @@ -376,28 +382,33 @@ public abstract class Parseable implements ConfigParseable { @Override ConfigParseable relativeTo(String filename) { - try { - URL url = relativeTo(input.toURI().toURL(), filename); - if (url == null) - return null; - return newURL(url, options().setOriginDescription(null)); - } catch (MalformedURLException e) { + File sibling; + if ((new File(filename)).isAbsolute()) { + sibling = new File(filename); + } else { + // this may return null + sibling = relativeTo(input, filename); + } + if (sibling == null) return null; + if (sibling.exists()) { + return newFile(sibling, options().setOriginDescription(null)); + } else { + // fall back to classpath; we treat the "filename" as absolute + // (don't add a package name in front), + // if it starts with "/" then remove the "/", for consistency + // with ParseableResources.relativeTo + String resource = filename; + if (filename.startsWith("/")) + resource = filename.substring(1); + return newResources(this.getClass().getClassLoader(), resource, options() + .setOriginDescription(null)); } } @Override - String originDescription() { - return input.getPath(); - } - - @Override - public URL url() { - try { - return input.toURI().toURL(); - } catch (MalformedURLException e) { - return null; - } + protected ConfigOrigin createOrigin() { + return SimpleConfigOrigin.newFile(input.getPath()); } @Override @@ -410,25 +421,61 @@ public abstract class Parseable implements ConfigParseable { return new ParseableFile(input, options); } - private final static class ParseableResource extends Parseable { - final private Class klass; + private final static class ParseableResources extends Parseable { + final private ClassLoader loader; final private String resource; - ParseableResource(Class klass, String resource, + ParseableResources(ClassLoader loader, String resource, ConfigParseOptions options) { - this.klass = klass; + this.loader = loader; this.resource = resource; postConstruct(options); } @Override protected Reader reader() throws IOException { - InputStream stream = klass.getResourceAsStream(resource); - if (stream == null) { - throw new IOException("resource not found on classpath: " - + resource); + throw new ConfigException.BugOrBroken( + "reader() should not be called on resources"); + } + + @Override + protected AbstractConfigObject rawParseValue(ConfigOrigin origin, + ConfigParseOptions finalOptions) throws IOException { + Enumeration e = loader.getResources(resource); + if (!e.hasMoreElements()) { + throw new IOException("resource not found on classpath: " + resource); } - return readerFromStream(stream); + AbstractConfigObject merged = SimpleConfigObject.empty(origin); + while (e.hasMoreElements()) { + URL url = e.nextElement(); + + ConfigOrigin elementOrigin = ((SimpleConfigOrigin) origin).addURL(url); + + AbstractConfigValue v; + + // it's tempting to use ParseableURL here but it would be wrong + // because the wrong relativeTo() would be used for includes. + InputStream stream = url.openStream(); + try { + Reader reader = readerFromStream(stream); + stream = null; // reader now owns it + try { + // parse in "raw" mode which will throw any IOException + // from here. + v = rawParseValue(reader, elementOrigin, finalOptions); + } finally { + reader.close(); + } + } finally { + // stream is null if the reader owns it + if (stream != null) + stream.close(); + } + + merged = merged.withFallback(v); + } + + return merged; } @Override @@ -436,48 +483,86 @@ public abstract class Parseable implements ConfigParseable { return syntaxFromExtension(resource); } - @Override - ConfigParseable relativeTo(String filename) { - // not using File.isAbsolute because resource paths always use '/' - // (?) - if (filename.startsWith("/")) + static String parent(String resource) { + // the "resource" is not supposed to begin with a "/" + // because it's supposed to be the raw resource + // (ClassLoader#getResource), not the + // resource "syntax" (Class#getResource) + int i = resource.lastIndexOf('/'); + if (i < 0) { return null; + } else { + return resource.substring(0, i); + } + } - // here we want to build a new resource name and let - // the class loader have it, rather than getting the - // url with getResource() and relativizing to that url. - // This is needed in case the class loader is going to - // search a classpath. - File parent = new File(resource).getParentFile(); - if (parent == null) - return newResource(klass, "/" + filename, options() - .setOriginDescription(null)); - else - return newResource(klass, new File(parent, filename).getPath(), + @Override + ConfigParseable relativeTo(String sibling) { + if (sibling.startsWith("/")) { + // if it starts with "/" then don't make it relative to + // the including resource + return newResources(loader, sibling.substring(1), options().setOriginDescription(null)); + } else { + // here we want to build a new resource name and let + // the class loader have it, rather than getting the + // url with getResource() and relativizing to that url. + // This is needed in case the class loader is going to + // search a classpath. + String parent = parent(resource); + if (parent == null) + return newResources(loader, sibling, options().setOriginDescription(null)); + else + return newResources(loader, parent + "/" + sibling, options() + .setOriginDescription(null)); + } } @Override - String originDescription() { - return resource + " on classpath"; - } - - @Override - public URL url() { - return klass.getResource(resource); + protected ConfigOrigin createOrigin() { + return SimpleConfigOrigin.newResource(resource); } @Override public String toString() { return getClass().getSimpleName() + "(" + resource + "," - + klass.getName() - + ")"; + + loader.getClass().getSimpleName() + ")"; } } - public static Parseable newResource(Class klass, String resource, + public static Parseable newResources(Class klass, String resource, ConfigParseOptions options) { - return new ParseableResource(klass, resource, options); + return newResources(klass.getClassLoader(), convertResourceName(klass, resource), options); + } + + // this function is supposed to emulate the difference + // between Class.getResource and ClassLoader.getResource + // (unfortunately there doesn't seem to be public API for it). + // We're using it because the Class API is more limited, + // for example it lacks getResources(). So we want to be able to + // use ClassLoader directly. + private static String convertResourceName(Class klass, String resource) { + if (resource.startsWith("/")) { + // "absolute" resource, chop the slash + return resource.substring(1); + } else { + String className = klass.getName(); + int i = className.lastIndexOf('.'); + if (i < 0) { + // no package + return resource; + } else { + // need to be relative to the package + String packageName = className.substring(0, i); + String packagePath = packageName.replace('.', '/'); + return packagePath + "/" + resource; + } + } + } + + public static Parseable newResources(ClassLoader loader, String resource, + ConfigParseOptions options) { + return new ParseableResources(loader, resource, options); } private final static class ParseableProperties extends Parseable { @@ -495,7 +580,7 @@ public abstract class Parseable implements ConfigParseable { } @Override - protected AbstractConfigObject parseValue(ConfigOrigin origin, + protected AbstractConfigObject rawParseValue(ConfigOrigin origin, ConfigParseOptions finalOptions) { return PropertiesParser.fromProperties(origin, props); } @@ -506,13 +591,8 @@ public abstract class Parseable implements ConfigParseable { } @Override - String originDescription() { - return "properties"; - } - - @Override - public URL url() { - return null; + protected ConfigOrigin createOrigin() { + return SimpleConfigOrigin.newSimple("properties"); } @Override diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/Parser.java b/akka-actor/src/main/java/com/typesafe/config/impl/Parser.java index d168e8d57a..8c1434b566 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/Parser.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/Parser.java @@ -110,7 +110,8 @@ final class Parser { Token t = nextToken(); while (true) { if (Tokens.isNewline(t)) { - lineNumber = Tokens.getLineNumber(t); + // newline number is the line just ended, so add one + lineNumber = Tokens.getLineNumber(t) + 1; sawSeparatorOrNewline = true; // we want to continue to also eat // a comma if there is one. @@ -155,7 +156,7 @@ final class Parser { return; } - // this will be a list of String and Path + // this will be a list of String and SubstitutionExpression List minimized = new ArrayList(); // we have multiple value tokens or one unquoted text token; @@ -187,7 +188,9 @@ final class Parser { .getSubstitutionPathExpression(valueToken); Path path = parsePathExpression(expression.iterator(), Tokens.getSubstitutionOrigin(valueToken)); - minimized.add(path); + boolean optional = Tokens.getSubstitutionOptional(valueToken); + + minimized.add(new SubstitutionExpression(path, optional)); } else { throw new ConfigException.BugOrBroken( "should not be trying to consolidate token: " @@ -219,8 +222,7 @@ final class Parser { } private ConfigOrigin lineOrigin() { - return new SimpleConfigOrigin(baseOrigin.description() + ": line " - + lineNumber); + return ((SimpleConfigOrigin) baseOrigin).setLineNumber(lineNumber); } private ConfigException parseError(String message) { @@ -681,7 +683,7 @@ final class Parser { return pb.result(); } - static ConfigOrigin apiOrigin = new SimpleConfigOrigin("path parameter"); + static ConfigOrigin apiOrigin = SimpleConfigOrigin.newSimple("path parameter"); static Path parsePath(String path) { Path speculated = speculativeFastParsePath(path); diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/Path.java b/akka-actor/src/main/java/com/typesafe/config/impl/Path.java index b0434f0f14..f19552c890 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/Path.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/Path.java @@ -139,11 +139,25 @@ final class Path { } // this doesn't have a very precise meaning, just to reduce - // noise from quotes in the rendered path + // noise from quotes in the rendered path for average cases static boolean hasFunkyChars(String s) { - for (int i = 0; i < s.length(); ++i) { + int length = s.length(); + + if (length == 0) + return false; + + // if the path starts with something that could be a number, + // we need to quote it because the number could be invalid, + // for example it could be a hyphen with no digit afterward + // or the exponent "e" notation could be mangled. + char first = s.charAt(0); + if (!(Character.isLetter(first))) + return true; + + for (int i = 1; i < length; ++i) { char c = s.charAt(i); - if (Character.isLetterOrDigit(c) || c == ' ') + + if (Character.isLetterOrDigit(c) || c == '-' || c == '_') continue; else return true; @@ -152,7 +166,7 @@ final class Path { } private void appendToStringBuilder(StringBuilder sb) { - if (hasFunkyChars(first)) + if (hasFunkyChars(first) || first.isEmpty()) sb.append(ConfigUtil.renderJsonString(first)); else sb.append(first); diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/RootConfig.java b/akka-actor/src/main/java/com/typesafe/config/impl/RootConfig.java deleted file mode 100644 index 723b9fad89..0000000000 --- a/akka-actor/src/main/java/com/typesafe/config/impl/RootConfig.java +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (C) 2011 Typesafe Inc. - */ -package com.typesafe.config.impl; - -import com.typesafe.config.ConfigMergeable; -import com.typesafe.config.ConfigResolveOptions; -import com.typesafe.config.ConfigRoot; - -final class RootConfig extends SimpleConfig implements ConfigRoot { - - final private Path rootPath; - - RootConfig(AbstractConfigObject underlying, Path rootPath) { - super(underlying); - this.rootPath = rootPath; - } - - @Override - protected RootConfig asRoot(AbstractConfigObject underlying, - Path newRootPath) { - if (newRootPath.equals(this.rootPath)) - return this; - else - return new RootConfig(underlying, newRootPath); - } - - @Override - public RootConfig resolve() { - return resolve(ConfigResolveOptions.defaults()); - } - - @Override - public RootConfig resolve(ConfigResolveOptions options) { - // if the object is already resolved then we should end up returning - // "this" here, since asRoot() should return this if the path - // is unchanged. - AbstractConfigObject resolved = resolvedObject(options); - return newRootIfObjectChanged(this, resolved); - } - - @Override - public RootConfig withFallback(ConfigMergeable value) { - // this can return "this" if the withFallback does nothing - return newRootIfObjectChanged(this, super.withFallback(value).toObject()); - } - - Path rootPathObject() { - return rootPath; - } - - @Override - public String rootPath() { - return rootPath.render(); - } - - @Override - public String toString() { - return "Root" + super.toString(); - } -} diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfig.java b/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfig.java index d64ce4625a..127a98a05b 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfig.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfig.java @@ -4,7 +4,9 @@ package com.typesafe.config.impl; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import com.typesafe.config.Config; @@ -25,16 +27,16 @@ import com.typesafe.config.ConfigValueType; * key-value pairs would be all the tree's leaf values, in a big flat list with * their full paths. */ -class SimpleConfig implements Config { +final class SimpleConfig implements Config, MergeableValue { - AbstractConfigObject object; + final private AbstractConfigObject object; SimpleConfig(AbstractConfigObject object) { this.object = object; } @Override - public AbstractConfigObject toObject() { + public AbstractConfigObject root() { return object; } @@ -43,35 +45,22 @@ class SimpleConfig implements Config { return object.origin(); } - /** - * Returns a version of this config that implements the ConfigRoot - * interface. - * - * @return a config root - */ - RootConfig asRoot(Path rootPath) { - return asRoot(object, rootPath); + @Override + public SimpleConfig resolve() { + return resolve(ConfigResolveOptions.defaults()); } - // RootConfig overrides this to avoid a new object on unchanged path. - protected RootConfig asRoot(AbstractConfigObject underlying, - Path newRootPath) { - return new RootConfig(underlying, newRootPath); - } - - static protected RootConfig newRootIfObjectChanged(RootConfig self, AbstractConfigObject underlying) { - if (underlying == self.object) - return self; - else - return new RootConfig(underlying, self.rootPathObject()); - } - - protected AbstractConfigObject resolvedObject(ConfigResolveOptions options) { + @Override + public SimpleConfig resolve(ConfigResolveOptions options) { AbstractConfigValue resolved = SubstitutionResolver.resolve(object, object, options); - return (AbstractConfigObject) resolved; + if (resolved == object) + return this; + else + return new SimpleConfig((AbstractConfigObject) resolved); } + @Override public boolean hasPath(String pathExpression) { Path path = Path.newPath(pathExpression); @@ -196,13 +185,13 @@ class SimpleConfig implements Config { } @Override - public Long getMemorySizeInBytes(String path) { + public Long getBytes(String path) { Long size = null; try { size = getLong(path); } catch (ConfigException.WrongType e) { ConfigValue v = find(path, ConfigValueType.STRING, path); - size = parseMemorySizeInBytes((String) v.unwrapped(), + size = parseBytes((String) v.unwrapped(), v.origin(), path); } return size; @@ -338,7 +327,7 @@ class SimpleConfig implements Config { } @Override - public List getMemorySizeInBytesList(String path) { + public List getBytesList(String path) { List l = new ArrayList(); List list = getList(path); for (ConfigValue v : list) { @@ -346,7 +335,7 @@ class SimpleConfig implements Config { l.add(((Number) v.unwrapped()).longValue()); } else if (v.valueType() == ConfigValueType.STRING) { String s = (String) v.unwrapped(); - Long n = parseMemorySizeInBytes(s, v.origin(), path); + Long n = parseBytes(s, v.origin(), path); l.add(n); } else { throw new ConfigException.WrongType(v.origin(), path, @@ -389,7 +378,7 @@ class SimpleConfig implements Config { } @Override - public AbstractConfigObject toValue() { + public AbstractConfigObject toFallbackValue() { return object; } @@ -509,23 +498,87 @@ class SimpleConfig implements Config { } private static enum MemoryUnit { - BYTES(1), KILOBYTES(1024), MEGABYTES(1024 * 1024), GIGABYTES( - 1024 * 1024 * 1024), TERABYTES(1024 * 1024 * 1024 * 1024); + BYTES("", 1024, 0), - int bytes; + KILOBYTES("kilo", 1000, 1), + MEGABYTES("mega", 1000, 2), + GIGABYTES("giga", 1000, 3), + TERABYTES("tera", 1000, 4), + PETABYTES("peta", 1000, 5), + EXABYTES("exa", 1000, 6), + ZETTABYTES("zetta", 1000, 7), + YOTTABYTES("yotta", 1000, 8), - MemoryUnit(int bytes) { + KIBIBYTES("kibi", 1024, 1), + MEBIBYTES("mebi", 1024, 2), + GIBIBYTES("gibi", 1024, 3), + TEBIBYTES("tebi", 1024, 4), + PEBIBYTES("pebi", 1024, 5), + EXBIBYTES("exbi", 1024, 6), + ZEBIBYTES("zebi", 1024, 7), + YOBIBYTES("yobi", 1024, 8); + + final String prefix; + final int powerOf; + final int power; + final long bytes; + + MemoryUnit(String prefix, int powerOf, int power) { + this.prefix = prefix; + this.powerOf = powerOf; + this.power = power; + int i = power; + long bytes = 1; + while (i > 0) { + bytes *= powerOf; + --i; + } this.bytes = bytes; } + + private static Map makeUnitsMap() { + Map map = new HashMap(); + for (MemoryUnit unit : MemoryUnit.values()) { + map.put(unit.prefix + "byte", unit); + map.put(unit.prefix + "bytes", unit); + if (unit.prefix.length() == 0) { + map.put("b", unit); + map.put("B", unit); + map.put("", unit); // no unit specified means bytes + } else { + String first = unit.prefix.substring(0, 1); + String firstUpper = first.toUpperCase(); + if (unit.powerOf == 1024) { + map.put(first, unit); // 512m + map.put(firstUpper, unit); // 512M + map.put(firstUpper + "i", unit); // 512Mi + map.put(firstUpper + "iB", unit); // 512MiB + } else if (unit.powerOf == 1000) { + if (unit.power == 1) { + map.put(first + "B", unit); // 512kB + } else { + map.put(firstUpper + "B", unit); // 512MB + } + } else { + throw new RuntimeException("broken MemoryUnit enum"); + } + } + } + return map; + } + + private static Map unitsMap = makeUnitsMap(); + + static MemoryUnit parseUnit(String unit) { + return unitsMap.get(unit); + } } /** - * Parses a memory-size string. If no units are specified in the string, it - * is assumed to be in bytes. The returned value is in bytes. The purpose of - * this function is to implement the memory-size-related methods in the - * ConfigObject interface. The units parsed are interpreted as powers of - * two, that is, the convention for memory rather than the convention for - * disk space. + * Parses a size-in-bytes string. If no units are specified in the string, + * it is assumed to be in bytes. The returned value is in bytes. The purpose + * of this function is to implement the size-in-bytes-related methods in the + * Config interface. * * @param input * the string to parse @@ -537,19 +590,12 @@ class SimpleConfig implements Config { * @throws ConfigException * if string is invalid */ - public static long parseMemorySizeInBytes(String input, - ConfigOrigin originForException, String pathForException) { + public static long parseBytes(String input, ConfigOrigin originForException, + String pathForException) { String s = ConfigUtil.unicodeTrim(input); - String unitStringMaybePlural = getUnits(s); - String unitString; - if (unitStringMaybePlural.endsWith("s")) - unitString = unitStringMaybePlural.substring(0, - unitStringMaybePlural.length() - 1); - else - unitString = unitStringMaybePlural; - String unitStringLower = unitString.toLowerCase(); - String numberString = ConfigUtil.unicodeTrim(s.substring(0, s.length() - - unitStringMaybePlural.length())); + String unitString = getUnits(s); + String numberString = ConfigUtil.unicodeTrim(s.substring(0, + s.length() - unitString.length())); // this would be caught later anyway, but the error message // is more helpful if we check it here. @@ -558,40 +604,197 @@ class SimpleConfig implements Config { pathForException, "No number in size-in-bytes value '" + input + "'"); - MemoryUnit units = null; + MemoryUnit units = MemoryUnit.parseUnit(unitString); - // the short abbreviations are case-insensitive but you can't write the - // long form words in all caps. - if (unitString.equals("") || unitStringLower.equals("b") - || unitString.equals("byte")) { - units = MemoryUnit.BYTES; - } else if (unitStringLower.equals("k") || unitString.equals("kilobyte")) { - units = MemoryUnit.KILOBYTES; - } else if (unitStringLower.equals("m") || unitString.equals("megabyte")) { - units = MemoryUnit.MEGABYTES; - } else if (unitStringLower.equals("g") || unitString.equals("gigabyte")) { - units = MemoryUnit.GIGABYTES; - } else if (unitStringLower.equals("t") || unitString.equals("terabyte")) { - units = MemoryUnit.TERABYTES; - } else { - throw new ConfigException.BadValue(originForException, - pathForException, "Could not parse size unit '" - + unitStringMaybePlural + "' (try b, k, m, g, t)"); + if (units == null) { + throw new ConfigException.BadValue(originForException, pathForException, + "Could not parse size-in-bytes unit '" + unitString + + "' (try k, K, kB, KiB, kilobytes, kibibytes)"); } try { // if the string is purely digits, parse as an integer to avoid - // possible precision loss; - // otherwise as a double. + // possible precision loss; otherwise as a double. if (numberString.matches("[0-9]+")) { return Long.parseLong(numberString) * units.bytes; } else { return (long) (Double.parseDouble(numberString) * units.bytes); } } catch (NumberFormatException e) { - throw new ConfigException.BadValue(originForException, - pathForException, "Could not parse memory size number '" - + numberString + "'"); + throw new ConfigException.BadValue(originForException, pathForException, + "Could not parse size-in-bytes number '" + numberString + "'"); + } + } + + private AbstractConfigValue peekPath(Path path) { + return root().peekPath(path); + } + + private static void addProblem(List accumulator, Path path, + ConfigOrigin origin, String problem) { + accumulator.add(new ConfigException.ValidationProblem(path.render(), origin, problem)); + } + + private static String getDesc(ConfigValue refValue) { + if (refValue instanceof AbstractConfigObject) { + AbstractConfigObject obj = (AbstractConfigObject) refValue; + if (obj.isEmpty()) + return "object"; + else + return "object with keys " + obj.keySet(); + } else if (refValue instanceof SimpleConfigList) { + return "list"; + } else { + return refValue.valueType().name().toLowerCase(); + } + } + + private static void addMissing(List accumulator, + ConfigValue refValue, Path path, ConfigOrigin origin) { + addProblem(accumulator, path, origin, "No setting at '" + path.render() + "', expecting: " + + getDesc(refValue)); + } + + private static void addWrongType(List accumulator, + ConfigValue refValue, AbstractConfigValue actual, Path path) { + addProblem(accumulator, path, actual.origin(), "Wrong value type at '" + path.render() + + "', expecting: " + getDesc(refValue) + " but got: " + + getDesc(actual)); + } + + private static boolean couldBeNull(AbstractConfigValue v) { + return DefaultTransformer.transform(v, ConfigValueType.NULL) + .valueType() == ConfigValueType.NULL; + } + + private static boolean haveCompatibleTypes(ConfigValue reference, AbstractConfigValue value) { + if (couldBeNull((AbstractConfigValue) reference) || couldBeNull(value)) { + // we allow any setting to be null + return true; + } else if (reference instanceof AbstractConfigObject) { + if (value instanceof AbstractConfigObject) { + return true; + } else { + return false; + } + } else if (reference instanceof SimpleConfigList) { + if (value instanceof SimpleConfigList) { + return true; + } else { + return false; + } + } else if (reference instanceof ConfigString) { + // assume a string could be gotten as any non-collection type; + // allows things like getMilliseconds including domain-specific + // interpretations of strings + return true; + } else if (value instanceof ConfigString) { + // assume a string could be gotten as any non-collection type + return true; + } else { + if (reference.valueType() == value.valueType()) { + return true; + } else { + return false; + } + } + } + + // path is null if we're at the root + private static void checkValidObject(Path path, AbstractConfigObject reference, + AbstractConfigObject value, + List accumulator) { + for (Map.Entry entry : reference.entrySet()) { + String key = entry.getKey(); + + Path childPath; + if (path != null) + childPath = Path.newKey(key).prepend(path); + else + childPath = Path.newKey(key); + + AbstractConfigValue v = value.get(key); + if (v == null) { + addMissing(accumulator, entry.getValue(), childPath, value.origin()); + } else { + checkValid(childPath, entry.getValue(), v, accumulator); + } + } + } + + private static void checkValid(Path path, ConfigValue reference, AbstractConfigValue value, + List accumulator) { + // Unmergeable is supposed to be impossible to encounter in here + // because we check for resolve status up front. + + if (haveCompatibleTypes(reference, value)) { + if (reference instanceof AbstractConfigObject && value instanceof AbstractConfigObject) { + checkValidObject(path, (AbstractConfigObject) reference, + (AbstractConfigObject) value, accumulator); + } else if (reference instanceof SimpleConfigList && value instanceof SimpleConfigList) { + SimpleConfigList listRef = (SimpleConfigList) reference; + SimpleConfigList listValue = (SimpleConfigList) value; + if (listRef.isEmpty() || listValue.isEmpty()) { + // can't verify type, leave alone + } else { + AbstractConfigValue refElement = listRef.get(0); + for (ConfigValue elem : listValue) { + AbstractConfigValue e = (AbstractConfigValue) elem; + if (!haveCompatibleTypes(refElement, e)) { + addProblem(accumulator, path, e.origin(), "List at '" + path.render() + + "' contains wrong value type, expecting list of " + + getDesc(refElement) + " but got element of type " + + getDesc(e)); + // don't add a problem for every last array element + break; + } + } + } + } + } else { + addWrongType(accumulator, reference, value, path); + } + } + + @Override + public void checkValid(Config reference, String... restrictToPaths) { + SimpleConfig ref = (SimpleConfig) reference; + + // unresolved reference config is a bug in the caller of checkValid + if (ref.root().resolveStatus() != ResolveStatus.RESOLVED) + throw new ConfigException.BugOrBroken( + "do not call checkValid() with an unresolved reference config, call Config.resolve()"); + + // unresolved config under validation is probably a bug in something, + // but our whole goal here is to check for bugs in this config, so + // BugOrBroken is not the appropriate exception. + if (root().resolveStatus() != ResolveStatus.RESOLVED) + throw new ConfigException.NotResolved( + "config has unresolved substitutions; must call Config.resolve()"); + + // Now we know that both reference and this config are resolved + + List problems = new ArrayList(); + + if (restrictToPaths.length == 0) { + checkValidObject(null, ref.root(), root(), problems); + } else { + for (String p : restrictToPaths) { + Path path = Path.newPath(p); + AbstractConfigValue refValue = ref.peekPath(path); + if (refValue != null) { + AbstractConfigValue child = peekPath(path); + if (child != null) { + checkValid(path, refValue, child, problems); + } else { + addMissing(problems, refValue, path, origin()); + } + } + } + } + + if (!problems.isEmpty()) { + throw new ConfigException.ValidationFailed(problems); } } } diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfigList.java b/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfigList.java index 9e610858c9..6703540040 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfigList.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfigList.java @@ -9,7 +9,6 @@ import java.util.Iterator; import java.util.List; import java.util.ListIterator; -import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigList; import com.typesafe.config.ConfigOrigin; import com.typesafe.config.ConfigResolveOptions; @@ -68,8 +67,9 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList { } // once the new list is created, all elements - // have to go in it. - if (changed != null) { + // have to go in it. if modifyChild returned + // null, we drop that element. + if (changed != null && modified != null) { changed.add(modified); } @@ -77,9 +77,6 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList { } if (changed != null) { - if (changed.size() != value.size()) - throw new ConfigException.BugOrBroken( - "substituted list's size doesn't match"); return new SimpleConfigList(origin(), changed, newResolveStatus); } else { return this; @@ -135,18 +132,34 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList { } @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(valueType().name()); - sb.append("("); - for (ConfigValue e : value) { - sb.append(e.toString()); - sb.append(","); + protected void render(StringBuilder sb, int indent, boolean formatted) { + if (value.isEmpty()) { + sb.append("[]"); + } else { + sb.append("["); + if (formatted) + sb.append('\n'); + for (AbstractConfigValue v : value) { + if (formatted) { + indent(sb, indent + 1); + sb.append("# "); + sb.append(v.origin().description()); + sb.append("\n"); + indent(sb, indent + 1); + } + v.render(sb, indent + 1, formatted); + sb.append(","); + if (formatted) + sb.append('\n'); + } + sb.setLength(sb.length() - 1); // chop or newline + if (formatted) { + sb.setLength(sb.length() - 1); // also chop comma + sb.append('\n'); + indent(sb, indent); + } + sb.append("]"); } - if (!value.isEmpty()) - sb.setLength(sb.length() - 1); // chop comma - sb.append(")"); - return sb.toString(); } @Override @@ -160,7 +173,7 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList { } @Override - public ConfigValue get(int index) { + public AbstractConfigValue get(int index) { return value.get(index); } diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java b/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java index 88b2b9090f..0c855ba879 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java @@ -112,8 +112,8 @@ final class SimpleConfigObject extends AbstractConfigObject { } final private static String EMPTY_NAME = "empty config"; - final private static SimpleConfigObject emptyInstance = empty(new SimpleConfigOrigin( - EMPTY_NAME)); + final private static SimpleConfigObject emptyInstance = empty(SimpleConfigOrigin + .newSimple(EMPTY_NAME)); final static SimpleConfigObject empty() { return emptyInstance; @@ -128,7 +128,7 @@ final class SimpleConfigObject extends AbstractConfigObject { } final static SimpleConfigObject emptyMissing(ConfigOrigin baseOrigin) { - return new SimpleConfigObject(new SimpleConfigOrigin( + return new SimpleConfigObject(SimpleConfigOrigin.newSimple( baseOrigin.description() + " (not found)"), Collections. emptyMap()); } diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfigOrigin.java b/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfigOrigin.java index 6e37756638..1ae914c0e4 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfigOrigin.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfigOrigin.java @@ -3,26 +3,101 @@ */ package com.typesafe.config.impl; +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigOrigin; +// it would be cleaner to have a class hierarchy for various origin types, +// but was hoping this would be enough simpler to be a little messy. eh. final class SimpleConfigOrigin implements ConfigOrigin { - final private String description; + final private int lineNumber; + final private int endLineNumber; + final private OriginType originType; + final private String urlOrNull; - SimpleConfigOrigin(String description) { + protected SimpleConfigOrigin(String description, int lineNumber, int endLineNumber, + OriginType originType, + String urlOrNull) { this.description = description; + this.lineNumber = lineNumber; + this.endLineNumber = endLineNumber; + this.originType = originType; + this.urlOrNull = urlOrNull; + } + + static SimpleConfigOrigin newSimple(String description) { + return new SimpleConfigOrigin(description, -1, -1, OriginType.GENERIC, null); + } + + static SimpleConfigOrigin newFile(String filename) { + String url; + try { + url = (new File(filename)).toURI().toURL().toExternalForm(); + } catch (MalformedURLException e) { + url = null; + } + return new SimpleConfigOrigin(filename, -1, -1, OriginType.FILE, url); + } + + static SimpleConfigOrigin newURL(URL url) { + String u = url.toExternalForm(); + return new SimpleConfigOrigin(u, -1, -1, OriginType.URL, u); + } + + static SimpleConfigOrigin newResource(String resource, URL url) { + return new SimpleConfigOrigin(resource, -1, -1, OriginType.RESOURCE, + url != null ? url.toExternalForm() : null); + } + + static SimpleConfigOrigin newResource(String resource) { + return newResource(resource, null); + } + + SimpleConfigOrigin setLineNumber(int lineNumber) { + if (lineNumber == this.lineNumber && lineNumber == this.endLineNumber) { + return this; + } else { + return new SimpleConfigOrigin(this.description, lineNumber, lineNumber, + this.originType, this.urlOrNull); + } + } + + SimpleConfigOrigin addURL(URL url) { + return new SimpleConfigOrigin(this.description, this.lineNumber, this.endLineNumber, this.originType, + url != null ? url.toExternalForm() : null); } @Override public String description() { - return description; + // not putting the URL in here for files and resources, because people + // parsing "file: line" syntax would hit the ":" in the URL. + if (lineNumber < 0) { + return description; + } else if (endLineNumber == lineNumber) { + return description + ": " + lineNumber; + } else { + return description + ": " + lineNumber + "-" + endLineNumber; + } } @Override public boolean equals(Object other) { if (other instanceof SimpleConfigOrigin) { - return this.description - .equals(((SimpleConfigOrigin) other).description); + SimpleConfigOrigin otherOrigin = (SimpleConfigOrigin) other; + + return this.description.equals(otherOrigin.description) + && this.lineNumber == otherOrigin.lineNumber + && this.endLineNumber == otherOrigin.endLineNumber + && this.originType == otherOrigin.originType + && ConfigUtil.equalsHandlingNull(this.urlOrNull, otherOrigin.urlOrNull); } else { return false; } @@ -30,11 +105,201 @@ final class SimpleConfigOrigin implements ConfigOrigin { @Override public int hashCode() { - return description.hashCode(); + int h = 41 * (41 + description.hashCode()); + h = 41 * (h + lineNumber); + h = 41 * (h + endLineNumber); + h = 41 * (h + originType.hashCode()); + if (urlOrNull != null) + h = 41 * (h + urlOrNull.hashCode()); + return h; } @Override public String toString() { - return "ConfigOrigin(" + description + ")"; + // the url is only really useful on top of description for resources + if (originType == OriginType.RESOURCE && urlOrNull != null) { + return "ConfigOrigin(" + description + "," + urlOrNull + ")"; + } else { + return "ConfigOrigin(" + description + ")"; + } + } + + @Override + public String filename() { + if (originType == OriginType.FILE) { + return description; + } else if (urlOrNull != null) { + URL url; + try { + url = new URL(urlOrNull); + } catch (MalformedURLException e) { + return null; + } + if (url.getProtocol().equals("file")) { + return url.getFile(); + } else { + return null; + } + } else { + return null; + } + } + + @Override + public URL url() { + if (urlOrNull == null) { + return null; + } else { + try { + return new URL(urlOrNull); + } catch (MalformedURLException e) { + return null; + } + } + } + + @Override + public String resource() { + if (originType == OriginType.RESOURCE) { + return description; + } else { + return null; + } + } + + @Override + public int lineNumber() { + return lineNumber; + } + + static final String MERGE_OF_PREFIX = "merge of "; + + private static SimpleConfigOrigin mergeTwo(SimpleConfigOrigin a, SimpleConfigOrigin b) { + String mergedDesc; + int mergedStartLine; + int mergedEndLine; + + OriginType mergedType; + if (a.originType == b.originType) { + mergedType = a.originType; + } else { + mergedType = OriginType.GENERIC; + } + + // first use the "description" field which has no line numbers + // cluttering it. + String aDesc = a.description; + String bDesc = b.description; + if (aDesc.startsWith(MERGE_OF_PREFIX)) + aDesc = aDesc.substring(MERGE_OF_PREFIX.length()); + if (bDesc.startsWith(MERGE_OF_PREFIX)) + bDesc = bDesc.substring(MERGE_OF_PREFIX.length()); + + if (aDesc.equals(bDesc)) { + mergedDesc = aDesc; + + if (a.lineNumber < 0) + mergedStartLine = b.lineNumber; + else if (b.lineNumber < 0) + mergedStartLine = a.lineNumber; + else + mergedStartLine = Math.min(a.lineNumber, b.lineNumber); + + mergedEndLine = Math.max(a.endLineNumber, b.endLineNumber); + } else { + // this whole merge song-and-dance was intended to avoid this case + // whenever possible, but we've lost. Now we have to lose some + // structured information and cram into a string. + + // description() method includes line numbers, so use it instead + // of description field. + String aFull = a.description(); + String bFull = b.description(); + if (aFull.startsWith(MERGE_OF_PREFIX)) + aFull = aFull.substring(MERGE_OF_PREFIX.length()); + if (bFull.startsWith(MERGE_OF_PREFIX)) + bFull = bFull.substring(MERGE_OF_PREFIX.length()); + + mergedDesc = MERGE_OF_PREFIX + aFull + "," + bFull; + + mergedStartLine = -1; + mergedEndLine = -1; + } + + String mergedURL; + if (ConfigUtil.equalsHandlingNull(a.urlOrNull, b.urlOrNull)) { + mergedURL = a.urlOrNull; + } else { + mergedURL = null; + } + + return new SimpleConfigOrigin(mergedDesc, mergedStartLine, mergedEndLine, mergedType, + mergedURL); + } + + private static int similarity(SimpleConfigOrigin a, SimpleConfigOrigin b) { + int count = 0; + + if (a.originType == b.originType) + count += 1; + + if (a.description.equals(b.description)) { + count += 1; + + // only count these if the description field (which is the file + // or resource name) also matches. + if (a.lineNumber == b.lineNumber) + count += 1; + if (a.endLineNumber == b.endLineNumber) + count += 1; + if (ConfigUtil.equalsHandlingNull(a.urlOrNull, b.urlOrNull)) + count += 1; + } + + return count; + } + + // this picks the best pair to merge, because the pair has the most in + // common. we want to merge two lines in the same file rather than something + // else with one of the lines; because two lines in the same file can be + // better consolidated. + private static SimpleConfigOrigin mergeThree(SimpleConfigOrigin a, SimpleConfigOrigin b, + SimpleConfigOrigin c) { + if (similarity(a, b) >= similarity(b, c)) { + return mergeTwo(mergeTwo(a, b), c); + } else { + return mergeTwo(a, mergeTwo(b, c)); + } + } + + static ConfigOrigin mergeOrigins(Collection stack) { + if (stack.isEmpty()) { + throw new ConfigException.BugOrBroken("can't merge empty list of origins"); + } else if (stack.size() == 1) { + return stack.iterator().next(); + } else if (stack.size() == 2) { + Iterator i = stack.iterator(); + return mergeTwo((SimpleConfigOrigin) i.next(), (SimpleConfigOrigin) i.next()); + } else { + List remaining = new ArrayList(); + for (ConfigOrigin o : stack) { + remaining.add((SimpleConfigOrigin) o); + } + while (remaining.size() > 2) { + SimpleConfigOrigin c = remaining.get(remaining.size() - 1); + remaining.remove(remaining.size() - 1); + SimpleConfigOrigin b = remaining.get(remaining.size() - 1); + remaining.remove(remaining.size() - 1); + SimpleConfigOrigin a = remaining.get(remaining.size() - 1); + remaining.remove(remaining.size() - 1); + + SimpleConfigOrigin merged = mergeThree(a, b, c); + + remaining.add(merged); + } + + // should be down to either 1 or 2 + return mergeOrigins(remaining); + } } } diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/SubstitutionExpression.java b/akka-actor/src/main/java/com/typesafe/config/impl/SubstitutionExpression.java new file mode 100644 index 0000000000..abc1c74a39 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/SubstitutionExpression.java @@ -0,0 +1,46 @@ +package com.typesafe.config.impl; + +final class SubstitutionExpression { + + final private Path path; + final private boolean optional; + + SubstitutionExpression(Path path, boolean optional) { + this.path = path; + this.optional = optional; + } + + Path path() { + return path; + } + + boolean optional() { + return optional; + } + + SubstitutionExpression changePath(Path newPath) { + return new SubstitutionExpression(newPath, optional); + } + + @Override + public String toString() { + return "${" + (optional ? "?" : "") + path.render() + "}"; + } + + @Override + public boolean equals(Object other) { + if (other instanceof SubstitutionExpression) { + SubstitutionExpression otherExp = (SubstitutionExpression) other; + return otherExp.path.equals(this.path) && otherExp.optional == this.optional; + } else { + return false; + } + } + + @Override + public int hashCode() { + int h = 41 * (41 + path.hashCode()); + h = 41 * (h + (optional ? 1 : 0)); + return h; + } +} diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/SubstitutionResolver.java b/akka-actor/src/main/java/com/typesafe/config/impl/SubstitutionResolver.java index 7f77570d02..65a2b30900 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/SubstitutionResolver.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/SubstitutionResolver.java @@ -16,6 +16,8 @@ import com.typesafe.config.ConfigResolveOptions; */ final class SubstitutionResolver { final private AbstractConfigObject root; + // note that we can resolve things to undefined (represented as Java null, + // rather than ConfigNull) so this map can have null values. final private Map memos; SubstitutionResolver(AbstractConfigObject root) { @@ -31,9 +33,11 @@ final class SubstitutionResolver { } else { AbstractConfigValue resolved = original.resolveSubstitutions(this, depth, options); - if (resolved.resolveStatus() != ResolveStatus.RESOLVED) - throw new ConfigException.BugOrBroken( - "resolveSubstitutions() did not give us a resolved object"); + if (resolved != null) { + if (resolved.resolveStatus() != ResolveStatus.RESOLVED) + throw new ConfigException.BugOrBroken( + "resolveSubstitutions() did not give us a resolved object"); + } memos.put(original, resolved); return resolved; } diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/Tokenizer.java b/akka-actor/src/main/java/com/typesafe/config/impl/Tokenizer.java index 147917bf40..4965b2a619 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/Tokenizer.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/Tokenizer.java @@ -219,8 +219,7 @@ final class Tokenizer { private static ConfigOrigin lineOrigin(ConfigOrigin baseOrigin, int lineNumber) { - return new SimpleConfigOrigin(baseOrigin.description() + ": line " - + lineNumber); + return ((SimpleConfigOrigin) baseOrigin).setLineNumber(lineNumber); } // chars JSON allows a number to start with @@ -228,7 +227,7 @@ final class Tokenizer { // chars JSON allows to be part of a number static final String numberChars = "0123456789eE+-."; // chars that stop an unquoted string - static final String notInUnquotedText = "$\"{}[]:=,\\+#"; + static final String notInUnquotedText = "$\"{}[]:=,+#`^?!@*&\\"; // The rules here are intended to maximize convenience while // avoiding confusion with real valid JSON. Basically anything @@ -404,6 +403,14 @@ final class Tokenizer { throw parseError("'$' not followed by {"); } + boolean optional = false; + c = nextCharSkippingComments(); + if (c == '?') { + optional = true; + } else { + putBack(c); + } + WhitespaceSaver saver = new WhitespaceSaver(); List expression = new ArrayList(); @@ -428,7 +435,7 @@ final class Tokenizer { } } while (true); - return Tokens.newSubstitution(origin, expression); + return Tokens.newSubstitution(origin, optional, expression); } private Token pullNextToken(WhitespaceSaver saver) { diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/Tokens.java b/akka-actor/src/main/java/com/typesafe/config/impl/Tokens.java index 9ec73a819c..f36527d738 100644 --- a/akka-actor/src/main/java/com/typesafe/config/impl/Tokens.java +++ b/akka-actor/src/main/java/com/typesafe/config/impl/Tokens.java @@ -125,11 +125,13 @@ final class Tokens { // This is not a Value, because it requires special processing static private class Substitution extends Token { final private ConfigOrigin origin; + final private boolean optional; final private List value; - Substitution(ConfigOrigin origin, List expression) { + Substitution(ConfigOrigin origin, boolean optional, List expression) { super(TokenType.SUBSTITUTION); this.origin = origin; + this.optional = optional; this.value = expression; } @@ -137,6 +139,10 @@ final class Tokens { return origin; } + boolean optional() { + return optional; + } + List value() { return value; } @@ -237,6 +243,15 @@ final class Tokens { } } + static boolean getSubstitutionOptional(Token token) { + if (token instanceof Substitution) { + return ((Substitution) token).optional(); + } else { + throw new ConfigException.BugOrBroken("tried to get substitution optionality from " + + token); + } + } + final static Token START = new Token(TokenType.START); final static Token END = new Token(TokenType.END); final static Token COMMA = new Token(TokenType.COMMA); @@ -255,8 +270,8 @@ final class Tokens { return new UnquotedText(origin, s); } - static Token newSubstitution(ConfigOrigin origin, List expression) { - return new Substitution(origin, expression); + static Token newSubstitution(ConfigOrigin origin, boolean optional, List expression) { + return new Substitution(origin, optional, expression); } static Token newValue(AbstractConfigValue value) { diff --git a/akka-actor/src/main/java/com/typesafe/config/package.html b/akka-actor/src/main/java/com/typesafe/config/package.html new file mode 100644 index 0000000000..1e1c78bfb2 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/package.html @@ -0,0 +1,42 @@ + + + + + + + + +

+An API for loading and using configuration files, see the project site +for more information. +

+ +

+Typically you would load configuration with a static method from {@link com.typesafe.config.ConfigFactory} and then use +it with methods in the {@link com.typesafe.config.Config} interface. +

+ +

+An application can simply call {@link com.typesafe.config.ConfigFactory#load()} and place +its configuration in "application.conf" on the classpath. +If you use the default configuration from {@link com.typesafe.config.ConfigFactory#load()} +there's no need to pass a configuration to your libraries +and frameworks, as long as they all default to this same default, which they should. +

+ +

+A library or framework should ship a file "reference.conf" in its jar, and allow an application to pass in a +{@link com.typesafe.config.Config} to be used for the library. If no {@link com.typesafe.config.Config} is provided, +call {@link com.typesafe.config.ConfigFactory#load()} +to get the default one. Typically a library might offer two constructors, one with a Config parameter +and one which uses {@link com.typesafe.config.ConfigFactory#load()}. +

+ +

+You can find an example app and library on GitHub. +

+ + + diff --git a/akka-actor/src/main/resources/akka-serialization-reference.conf b/akka-actor/src/main/resources/akka-serialization-reference.conf deleted file mode 100644 index fb6f134b93..0000000000 --- a/akka-actor/src/main/resources/akka-serialization-reference.conf +++ /dev/null @@ -1,32 +0,0 @@ -############################################ -# Akka Serialization Reference Config File # -############################################ - -# This the reference config file has all the default settings. -# Make your edits/overrides in your akka.conf. - -akka { - actor { - - # Entries for pluggable serializers and their bindings. If a binding for a specific class is not found, - # then the default serializer (Java serialization) is used. - # - serializers { - # java = "akka.serialization.JavaSerializer" - # proto = "akka.testing.ProtobufSerializer" - # sjson = "akka.testing.SJSONSerializer" - default = "akka.serialization.JavaSerializer" - } - - # serialization-bindings { - # java = ["akka.serialization.SerializeSpec$Address", - # "akka.serialization.MyJavaSerializableActor", - # "akka.serialization.MyStatelessActorWithMessagesInMailbox", - # "akka.serialization.MyActorWithProtobufMessagesInMailbox"] - # sjson = ["akka.serialization.SerializeSpec$Person"] - # proto = ["com.google.protobuf.Message", - # "akka.actor.ProtobufProtocol$MyMessage"] - # } - } - -} diff --git a/akka-actor/src/main/resources/akka-actor-reference.conf b/akka-actor/src/main/resources/reference.conf similarity index 90% rename from akka-actor/src/main/resources/akka-actor-reference.conf rename to akka-actor/src/main/resources/reference.conf index fbfd7d7e9c..1d00474462 100644 --- a/akka-actor/src/main/resources/akka-actor-reference.conf +++ b/akka-actor/src/main/resources/reference.conf @@ -16,7 +16,7 @@ akka { loglevel = "INFO" # Options: ERROR, WARNING, INFO, DEBUG # this level is used by the configured loggers (see "event-handlers") as soon # as they have been started; before that, see "stdout-loglevel" - stdout-loglevel = "INFO" # Loglevel for the very basic logger activated during AkkaApplication startup + stdout-loglevel = "WARNING" # Loglevel for the very basic logger activated during AkkaApplication startup # FIXME: Is there any sensible reason why we have 2 different log levels? logConfigOnStart = off # Log the complete configuration at INFO level when the actor system is started. @@ -81,7 +81,7 @@ akka { # optional replication { # use replication or not? only makes sense for a stateful actor - # FIXME should we have this config option here? If so, implement it all through. + # serialize-mailbox not implemented, ticket #1412 serialize-mailbox = off # should the actor mailbox be part of the serialized snapshot? # default is 'off' @@ -125,6 +125,26 @@ akka { fsm = off # enable DEBUG logging of all LoggingFSMs for events, transitions and timers event-stream = off # enable DEBUG logging of subscription changes on the eventStream } + + # Entries for pluggable serializers and their bindings. If a binding for a specific class is not found, + # then the default serializer (Java serialization) is used. + # + serializers { + # java = "akka.serialization.JavaSerializer" + # proto = "akka.testing.ProtobufSerializer" + # sjson = "akka.testing.SJSONSerializer" + default = "akka.serialization.JavaSerializer" + } + + # serialization-bindings { + # java = ["akka.serialization.SerializeSpec$Address", + # "akka.serialization.MyJavaSerializableActor", + # "akka.serialization.MyStatelessActorWithMessagesInMailbox", + # "akka.serialization.MyActorWithProtobufMessagesInMailbox"] + # sjson = ["akka.serialization.SerializeSpec$Person"] + # proto = ["com.google.protobuf.Message", + # "akka.actor.ProtobufProtocol$MyMessage"] + # } } @@ -142,6 +162,5 @@ akka { tickDuration = 100ms ticksPerWheel = 512 } - - + } diff --git a/akka-actor/src/main/scala/akka/actor/Actor.scala b/akka-actor/src/main/scala/akka/actor/Actor.scala index efaedcde49..e741e774f4 100644 --- a/akka-actor/src/main/scala/akka/actor/Actor.scala +++ b/akka-actor/src/main/scala/akka/actor/Actor.scala @@ -162,25 +162,6 @@ object Actor { type Receive = PartialFunction[Any, Unit] - /** - * This decorator adds invocation logging to a Receive function. - */ - class LoggingReceive(source: AnyRef, r: Receive)(implicit system: ActorSystem) extends Receive { - def isDefinedAt(o: Any) = { - val handled = r.isDefinedAt(o) - system.eventStream.publish(Debug(LogSource.fromAnyRef(source), "received " + (if (handled) "handled" else "unhandled") + " message " + o)) - handled - } - def apply(o: Any): Unit = r(o) - } - - object LoggingReceive { - def apply(source: AnyRef, r: Receive)(implicit system: ActorSystem): Receive = r match { - case _: LoggingReceive ⇒ r - case _ ⇒ new LoggingReceive(source, r) - } - } - object emptyBehavior extends Receive { def isDefinedAt(x: Any) = false def apply(x: Any) = throw new UnsupportedOperationException("empty behavior apply()") @@ -237,22 +218,6 @@ trait Actor { */ implicit def defaultTimeout = system.settings.ActorTimeout - /** - * Wrap a Receive partial function in a logging enclosure, which sends a - * debug message to the EventHandler each time before a message is matched. - * This includes messages which are not handled. - * - *

-   * def receive = loggable {
-   *   case x => ...
-   * }
-   * 
- * - * This method does NOT modify the given Receive unless - * akka.actor.debug.receive is set within akka.conf. - */ - def loggable(self: AnyRef)(r: Receive): Receive = if (system.settings.AddLoggingReceive) LoggingReceive(self, r) else r //TODO FIXME Shouldn't this be in a Loggable-trait? - /** * The 'self' field holds the ActorRef for this actor. *

@@ -365,24 +330,24 @@ trait Actor { * Puts the behavior on top of the hotswap stack. * If "discardOld" is true, an unbecome will be issued prior to pushing the new behavior to the stack */ - def become(behavior: Receive, discardOld: Boolean = true) { context.become(behavior, discardOld) } + final def become(behavior: Receive, discardOld: Boolean = true) { context.become(behavior, discardOld) } /** * Reverts the Actor behavior to the previous one in the hotswap stack. */ - def unbecome() { context.unbecome() } + final def unbecome() { context.unbecome() } /** * Registers this actor as a Monitor for the provided ActorRef * @return the provided ActorRef */ - def watch(subject: ActorRef): ActorRef = self startsWatching subject + final def watch(subject: ActorRef): ActorRef = context startsWatching subject /** * Unregisters this actor as Monitor for the provided ActorRef * @return the provided ActorRef */ - def unwatch(subject: ActorRef): ActorRef = self stopsWatching subject + final def unwatch(subject: ActorRef): ActorRef = context stopsWatching subject // ========================================= // ==== INTERNAL IMPLEMENTATION DETAILS ==== @@ -397,6 +362,6 @@ trait Actor { } } - private val processingBehavior = receive //ProcessingBehavior is the original behavior + private[this] val processingBehavior = receive //ProcessingBehavior is the original behavior } diff --git a/akka-actor/src/main/scala/akka/actor/ActorCell.scala b/akka-actor/src/main/scala/akka/actor/ActorCell.scala index 8c17fa418e..766da4161b 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorCell.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorCell.scala @@ -47,6 +47,10 @@ trait ActorContext extends ActorRefFactory { def system: ActorSystem def parent: ActorRef + + def startsWatching(subject: ActorRef): ActorRef + + def stopsWatching(subject: ActorRef): ActorRef } private[akka] object ActorCell { @@ -148,13 +152,13 @@ private[akka] class ActorCell( // ➡➡➡ NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS ⬅⬅⬅ private[akka] def stop(): Unit = dispatcher.systemDispatch(this, Terminate()) - final def startsWatching(subject: ActorRef): ActorRef = { + override final def startsWatching(subject: ActorRef): ActorRef = { // ➡➡➡ NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS ⬅⬅⬅ dispatcher.systemDispatch(this, Link(subject)) subject } - final def stopsWatching(subject: ActorRef): ActorRef = { + override final def stopsWatching(subject: ActorRef): ActorRef = { // ➡➡➡ NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS ⬅⬅⬅ dispatcher.systemDispatch(this, Unlink(subject)) subject @@ -207,6 +211,7 @@ private[akka] class ActorCell( checkReceiveTimeout if (system.settings.DebugLifecycle) system.eventStream.publish(Debug(self.path.toString, "started (" + actor + ")")) } catch { + // FIXME catching all and continue isn't good for OOME, ticket #1418 case e ⇒ try { system.eventStream.publish(Error(e, self.path.toString, "error while creating actor")) @@ -239,6 +244,7 @@ private[akka] class ActorCell( props.faultHandler.handleSupervisorRestarted(cause, self, children) } catch { + // FIXME catching all and continue isn't good for OOME, ticket #1418 case e ⇒ try { system.eventStream.publish(Error(e, self.path.toString, "error while creating actor")) // prevent any further messages to be processed until the actor has been restarted @@ -300,7 +306,7 @@ private[akka] class ActorCell( } catch { case e ⇒ //Should we really catch everything here? system.eventStream.publish(Error(e, self.path.toString, "error while processing " + message)) - //TODO FIXME How should problems here be handled? + //TODO FIXME How should problems here be handled??? throw e } } @@ -311,7 +317,7 @@ private[akka] class ActorCell( currentMessage = messageHandle try { try { - cancelReceiveTimeout() // FIXME: leave this here? + cancelReceiveTimeout() // FIXME: leave this here??? messageHandle.message match { case msg: AutoReceivedMessage ⇒ autoReceiveMessage(messageHandle) case msg if stopping ⇒ // receiving Terminated in response to stopping children is too common to generate noise diff --git a/akka-actor/src/main/scala/akka/actor/ActorRef.scala b/akka-actor/src/main/scala/akka/actor/ActorRef.scala index bb2471eef7..b09cefdc9d 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorRef.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorRef.scala @@ -48,7 +48,7 @@ import akka.event.DeathWatch * @author Jonas Bonér */ abstract class ActorRef extends java.lang.Comparable[ActorRef] with Serializable { - scalaRef: ScalaActorRef ⇒ + scalaRef: ScalaActorRef with RefInternals ⇒ // Only mutable for RemoteServer in order to maintain identity across nodes /** @@ -100,16 +100,6 @@ abstract class ActorRef extends java.lang.Comparable[ActorRef] with Serializable */ def forward(message: Any)(implicit context: ActorContext) = tell(message, context.sender) - /** - * Suspends the actor. It will not process messages while suspended. - */ - def suspend(): Unit //TODO FIXME REMOVE THIS - - /** - * Resumes a suspended actor. - */ - def resume(): Unit //TODO FIXME REMOVE THIS - /** * Shuts down the actor its dispatcher and message queue. */ @@ -120,24 +110,6 @@ abstract class ActorRef extends java.lang.Comparable[ActorRef] with Serializable */ def isTerminated: Boolean - /** - * Registers this actor to be a death monitor of the provided ActorRef - * This means that this actor will get a Terminated()-message when the provided actor - * is permanently terminated. - * - * @return the same ActorRef that is provided to it, to allow for cleaner invocations - */ - def startsWatching(subject: ActorRef): ActorRef //TODO FIXME REMOVE THIS - - /** - * Deregisters this actor from being a death monitor of the provided ActorRef - * This means that this actor will not get a Terminated()-message when the provided actor - * is permanently terminated. - * - * @return the same ActorRef that is provided to it, to allow for cleaner invocations - */ - def stopsWatching(subject: ActorRef): ActorRef //TODO FIXME REMOVE THIS - // FIXME check if we should scramble the bits or whether they can stay the same override def hashCode: Int = path.hashCode @@ -162,7 +134,7 @@ class LocalActorRef private[akka] ( val systemService: Boolean = false, _receiveTimeout: Option[Long] = None, _hotswap: Stack[PartialFunction[Any, Unit]] = Props.noHotSwap) - extends ActorRef with ScalaActorRef { + extends ActorRef with ScalaActorRef with RefInternals { /* * actorCell.start() publishes actorCell & this to the dispatcher, which @@ -190,13 +162,13 @@ class LocalActorRef private[akka] ( * message sends done from the same thread after calling this method will not * be processed until resumed. */ - //FIXME TODO REMOVE THIS, NO REPLACEMENT + //FIXME TODO REMOVE THIS, NO REPLACEMENT, ticket #1415 def suspend(): Unit = actorCell.suspend() /** * Resumes a suspended actor. */ - //FIXME TODO REMOVE THIS, NO REPLACEMENT + //FIXME TODO REMOVE THIS, NO REPLACEMENT, ticket #1415 def resume(): Unit = actorCell.resume() /** @@ -204,29 +176,11 @@ class LocalActorRef private[akka] ( */ def stop(): Unit = actorCell.stop() - /** - * Registers this actor to be a death monitor of the provided ActorRef - * This means that this actor will get a Terminated()-message when the provided actor - * is permanently terminated. - * - * @return the same ActorRef that is provided to it, to allow for cleaner invocations - */ - def startsWatching(subject: ActorRef): ActorRef = actorCell.startsWatching(subject) - - /** - * Deregisters this actor from being a death monitor of the provided ActorRef - * This means that this actor will not get a Terminated()-message when the provided actor - * is permanently terminated. - * - * @return the same ActorRef that is provided to it, to allow for cleaner invocations - */ - def stopsWatching(subject: ActorRef): ActorRef = actorCell.stopsWatching(subject) - // ========= AKKA PROTECTED FUNCTIONS ========= protected[akka] def underlying: ActorCell = actorCell - // FIXME TODO: remove this method + // FIXME TODO: remove this method. It is used in testkit. // @deprecated("This method does a spin-lock to block for the actor, which might never be there, do not use this", "2.0") protected[akka] def underlyingActorInstance: Actor = { var instance = actorCell.actor @@ -285,7 +239,11 @@ trait ScalaActorRef { ref: ActorRef ⇒ * implicit timeout */ def ?(message: Any, timeout: Timeout)(implicit ignore: Int = 0): Future[Any] = ?(message)(timeout) +} +private[akka] trait RefInternals { + def resume(): Unit + def suspend(): Unit protected[akka] def restart(cause: Throwable): Unit } @@ -308,11 +266,10 @@ case class SerializedActorRef(path: String) { /** * Trait for ActorRef implementations where all methods contain default stubs. */ -trait MinimalActorRef extends ActorRef with ScalaActorRef { - - def startsWatching(actorRef: ActorRef): ActorRef = actorRef - def stopsWatching(actorRef: ActorRef): ActorRef = actorRef +trait MinimalActorRef extends ActorRef with ScalaActorRef with RefInternals { + //FIXME REMOVE THIS, ticket #1416 + //FIXME REMOVE THIS, ticket #1415 def suspend(): Unit = () def resume(): Unit = () diff --git a/akka-actor/src/main/scala/akka/actor/ActorRefProvider.scala b/akka-actor/src/main/scala/akka/actor/ActorRefProvider.scala index c7f4ff28ab..0de2de5b05 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorRefProvider.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorRefProvider.scala @@ -20,6 +20,7 @@ import org.jboss.netty.akka.util.internal.ConcurrentIdentityHashMap import akka.event._ import akka.event.Logging.Error._ import akka.event.Logging.Warning +import java.io.Closeable /** * Interface for all ActorRef providers to implement. @@ -41,10 +42,10 @@ trait ActorRefProvider { */ def deathWatch: DeathWatch - // FIXME: remove/replace? + // FIXME: remove/replace??? def nodename: String - // FIXME: remove/replace? + // FIXME: remove/replace??? def clustername: String /** @@ -76,8 +77,14 @@ trait ActorRefProvider { def actorOf(system: ActorSystemImpl, props: Props, supervisor: ActorRef, name: String, systemService: Boolean = false): ActorRef /** + * <<<<<<< HEAD * Create actor reference for a specified local or remote path. If no such * actor exists, it will be (equivalent to) a dead letter reference. + * ======= + * Create an Actor with the given full path below the given supervisor. + * + * FIXME: Remove! this is dangerous!? + * >>>>>>> master */ def actorFor(path: ActorPath): ActorRef @@ -400,7 +407,12 @@ class LocalDeathWatch extends DeathWatch with ActorClassification { } } -class DefaultScheduler(hashedWheelTimer: HashedWheelTimer, system: ActorSystem) extends Scheduler { +/** + * Scheduled tasks (Runnable and functions) are executed with the supplied dispatcher. + * Note that dispatcher is by-name parameter, because dispatcher might not be initialized + * when the scheduler is created. + */ +class DefaultScheduler(hashedWheelTimer: HashedWheelTimer, log: LoggingAdapter, dispatcher: ⇒ MessageDispatcher) extends Scheduler with Closeable { def schedule(receiver: ActorRef, message: Any, initialDelay: Duration, delay: Duration): Cancellable = new DefaultCancellable(hashedWheelTimer.newTimeout(createContinuousTask(receiver, message, delay), initialDelay)) @@ -420,8 +432,7 @@ class DefaultScheduler(hashedWheelTimer: HashedWheelTimer, system: ActorSystem) private def createSingleTask(runnable: Runnable): TimerTask = new TimerTask() { def run(timeout: org.jboss.netty.akka.util.Timeout) { - // FIXME: consider executing runnable inside main dispatcher to prevent blocking of scheduler - runnable.run() + dispatcher.dispatchTask(() ⇒ runnable.run()) } } @@ -435,7 +446,7 @@ class DefaultScheduler(hashedWheelTimer: HashedWheelTimer, system: ActorSystem) private def createSingleTask(f: () ⇒ Unit): TimerTask = new TimerTask { def run(timeout: org.jboss.netty.akka.util.Timeout) { - f() + dispatcher.dispatchTask(f) } } @@ -447,7 +458,7 @@ class DefaultScheduler(hashedWheelTimer: HashedWheelTimer, system: ActorSystem) receiver ! message timeout.getTimer.newTimeout(this, delay) } else { - system.eventStream.publish(Warning(this.getClass.getSimpleName, "Could not reschedule message to be sent because receiving actor has been terminated.")) + log.warning("Could not reschedule message to be sent because receiving actor has been terminated.") } } } @@ -456,13 +467,13 @@ class DefaultScheduler(hashedWheelTimer: HashedWheelTimer, system: ActorSystem) private def createContinuousTask(f: () ⇒ Unit, delay: Duration): TimerTask = { new TimerTask { def run(timeout: org.jboss.netty.akka.util.Timeout) { - f() + dispatcher.dispatchTask(f) timeout.getTimer.newTimeout(this, delay) } } } - private[akka] def stop() = hashedWheelTimer.stop() + def close() = hashedWheelTimer.stop() } class DefaultCancellable(val timeout: org.jboss.netty.akka.util.Timeout) extends Cancellable { diff --git a/akka-actor/src/main/scala/akka/actor/ActorSystem.scala b/akka-actor/src/main/scala/akka/actor/ActorSystem.scala index 75904086cf..6d9b0b7da7 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorSystem.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorSystem.scala @@ -13,15 +13,17 @@ import java.util.concurrent.TimeUnit.MILLISECONDS import java.util.concurrent.TimeUnit.NANOSECONDS import java.io.File import com.typesafe.config.Config -import com.typesafe.config.ConfigParseOptions -import com.typesafe.config.ConfigRoot import com.typesafe.config.ConfigFactory +import com.typesafe.config.ConfigParseOptions +import com.typesafe.config.ConfigResolveOptions +import com.typesafe.config.ConfigException import java.lang.reflect.InvocationTargetException import akka.util.{ Helpers, Duration, ReflectiveAccess } import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.{ CountDownLatch, Executors, ConcurrentHashMap } import scala.annotation.tailrec import org.jboss.netty.akka.util.internal.ConcurrentIdentityHashMap +import java.io.Closeable object ActorSystem { @@ -42,17 +44,32 @@ object ActorSystem { def create(name: String, config: Config): ActorSystem = apply(name, config) def apply(name: String, config: Config): ActorSystem = new ActorSystemImpl(name, config).start() + /** + * Uses the standard default Config from ConfigFactory.load(), since none is provided. + */ def create(name: String): ActorSystem = apply(name) - def apply(name: String): ActorSystem = apply(name, DefaultConfigurationLoader.defaultConfig) + /** + * Uses the standard default Config from ConfigFactory.load(), since none is provided. + */ + def apply(name: String): ActorSystem = apply(name, ConfigFactory.load()) def create(): ActorSystem = apply() def apply(): ActorSystem = apply("default") class Settings(cfg: Config, val name: String) { - private def referenceConfig: Config = - ConfigFactory.parseResource(classOf[ActorSystem], "/akka-actor-reference.conf", - ConfigParseOptions.defaults.setAllowMissing(false)) - val config: ConfigRoot = ConfigFactory.emptyRoot("akka-actor").withFallback(cfg).withFallback(referenceConfig).resolve() + + // Verify that the Config is sane and has our reference config. + val config: Config = + try { + cfg.checkValid(ConfigFactory.defaultReference, "akka") + cfg + } catch { + case e: ConfigException ⇒ + // try again with added defaultReference + val cfg2 = cfg.withFallback(ConfigFactory.defaultReference) + cfg2.checkValid(ConfigFactory.defaultReference, "akka") + cfg2 + } import scala.collection.JavaConverters._ import config._ @@ -95,15 +112,17 @@ object ActorSystem { throw new ConfigurationException("Akka JAR version [" + Version + "] does not match the provided config version [" + ConfigVersion + "]") - override def toString: String = { - config.toString - } + override def toString: String = config.root.render } - object DefaultConfigurationLoader { + // TODO move to migration kit + object OldConfigurationLoader { - val defaultConfig: Config = fromProperties orElse fromClasspath orElse fromHome getOrElse emptyConfig + val defaultConfig: Config = { + val cfg = fromProperties orElse fromClasspath orElse fromHome getOrElse emptyConfig + cfg.withFallback(ConfigFactory.defaultReference).resolve(ConfigResolveOptions.defaults) + } // file extensions (.conf, .json, .properties), are handled by parseFileAnySyntax val defaultLocation: String = (systemMode orElse envMode).map("akka." + _).getOrElse("akka") @@ -129,7 +148,7 @@ object ActorSystem { private def fromClasspath = try { Option(ConfigFactory.systemProperties.withFallback( - ConfigFactory.parseResourceAnySyntax(ActorSystem.getClass, "/" + defaultLocation, configParseOptions))) + ConfigFactory.parseResourcesAnySyntax(ActorSystem.getClass, "/" + defaultLocation, configParseOptions))) } catch { case _ ⇒ None } private def fromHome = try { @@ -208,8 +227,6 @@ abstract class ActorSystem extends ActorRefFactory { * effort basis and hence not strictly guaranteed. */ def deadLetters: ActorRef - // FIXME: do not publish this - def deadLetterMailbox: Mailbox /** * Light-weight scheduler for running asynchronous tasks after some deadline @@ -234,7 +251,7 @@ abstract class ActorSystem extends ActorRefFactory { * Register a block of code to run after all actors in this actor system have * been stopped. */ - def registerOnTermination(code: ⇒ Unit) + def registerOnTermination[T](code: ⇒ T) /** * Register a block of code to run after all actors in this actor system have @@ -272,7 +289,7 @@ abstract class ActorSystem extends ActorRefFactory { def hasExtension(ext: ExtensionId[_ <: Extension]): Boolean } -class ActorSystemImpl(val name: String, val applicationConfig: Config) extends ActorSystem { +class ActorSystemImpl(val name: String, applicationConfig: Config) extends ActorSystem { if (!name.matches("""^\w+$""")) throw new IllegalArgumentException("invalid ActorSystem name '" + name + "', must contain only word characters (i.e. [a-zA-Z_0-9])") @@ -314,7 +331,7 @@ class ActorSystemImpl(val name: String, val applicationConfig: Config) extends A eventStream.startStdoutLogger(settings) val log = new BusLogging(eventStream, "ActorSystem") // “this” used only for .getClass in tagging messages - val scheduler = new DefaultScheduler(new HashedWheelTimer(log, Executors.defaultThreadFactory, settings.SchedulerTickDuration, settings.SchedulerTicksPerWheel), this) + val scheduler = createScheduler() val deadLetters = new DeadLetterActorRef(eventStream) val deadLetterMailbox = new Mailbox(null) { @@ -361,9 +378,9 @@ class ActorSystemImpl(val name: String, val applicationConfig: Config) extends A } val dispatcherFactory = new Dispatchers(settings, DefaultDispatcherPrerequisites(eventStream, deadLetterMailbox, scheduler)) + // TODO why implicit val dispatcher? implicit val dispatcher = dispatcherFactory.defaultGlobalDispatcher - //FIXME Set this to a Failure when things bubble to the top def terminationFuture: Future[Unit] = provider.terminationFuture def guardian: ActorRef = provider.guardian def systemGuardian: ActorRef = provider.systemGuardian @@ -389,14 +406,42 @@ class ActorSystemImpl(val name: String, val applicationConfig: Config) extends A def start() = _start - def registerOnTermination(code: ⇒ Unit) { terminationFuture onComplete (_ ⇒ code) } + def registerOnTermination[T](code: ⇒ T) { terminationFuture onComplete (_ ⇒ code) } def registerOnTermination(code: Runnable) { terminationFuture onComplete (_ ⇒ code.run) } // TODO shutdown all that other stuff, whatever that may be def stop() { guardian.stop() - terminationFuture onComplete (_ ⇒ scheduler.stop()) - terminationFuture onComplete (_ ⇒ dispatcher.shutdown()) + try terminationFuture.await(10 seconds) catch { + case _: FutureTimeoutException ⇒ log.warning("Failed to stop [{}] within 10 seconds", name) + } + // Dispatchers shutdown themselves, but requires the scheduler + terminationFuture onComplete (_ ⇒ stopScheduler()) + } + + protected def createScheduler(): Scheduler = { + val threadFactory = new MonitorableThreadFactory("DefaultScheduler") + val hwt = new HashedWheelTimer(log, threadFactory, settings.SchedulerTickDuration, settings.SchedulerTicksPerWheel) + // note that dispatcher is by-name parameter in DefaultScheduler constructor, + // because dispatcher is not initialized when the scheduler is created + def safeDispatcher = { + if (dispatcher eq null) { + val exc = new IllegalStateException("Scheduler is using dispatcher before it has been initialized") + log.error(exc, exc.getMessage) + throw exc + } else { + dispatcher + } + } + new DefaultScheduler(hwt, log, safeDispatcher) + } + + protected def stopScheduler(): Unit = scheduler match { + case x: Closeable ⇒ + // Let dispatchers shutdown first. + // Dispatchers schedule shutdown and may also reschedule, therefore wait 4 times the shutdown delay. + x.scheduleOnce(() ⇒ { x.close(); dispatcher.shutdown() }, settings.DispatcherDefaultShutdown * 4) + case _ ⇒ } private val extensions = new ConcurrentIdentityHashMap[ExtensionId[_], AnyRef] diff --git a/akka-actor/src/main/scala/akka/actor/BootableActorLoaderService.scala b/akka-actor/src/main/scala/akka/actor/BootableActorLoaderService.scala index 6e0f99b50d..47c2cd86c7 100644 --- a/akka-actor/src/main/scala/akka/actor/BootableActorLoaderService.scala +++ b/akka-actor/src/main/scala/akka/actor/BootableActorLoaderService.scala @@ -57,9 +57,6 @@ trait BootableActorLoaderService extends Bootable { abstract override def onUnload() = { super.onUnload() - - // FIXME shutdown all actors - // system.registry.local.shutdownAll } } diff --git a/akka-actor/src/main/scala/akka/actor/Deployer.scala b/akka-actor/src/main/scala/akka/actor/Deployer.scala index 3bb7338d8d..7159c15ad6 100644 --- a/akka-actor/src/main/scala/akka/actor/Deployer.scala +++ b/akka-actor/src/main/scala/akka/actor/Deployer.scala @@ -18,7 +18,6 @@ import com.typesafe.config.Config trait ActorDeployer { private[akka] def init(deployments: Seq[Deploy]): Unit - private[akka] def shutdown(): Unit //TODO Why should we have "shutdown", should be crash only? private[akka] def deploy(deployment: Deploy): Unit private[akka] def lookupDeploymentFor(path: String): Option[Deploy] def lookupDeployment(path: String): Option[Deploy] = path match { @@ -49,8 +48,6 @@ class Deployer(val settings: ActorSystem.Settings, val eventStream: EventStream, private[akka] def init(deployments: Seq[Deploy]) = instance.init(deployments) - def shutdown(): Unit = instance.shutdown() //TODO FIXME Why should we have "shutdown", should be crash only? - def deploy(deployment: Deploy): Unit = instance.deploy(deployment) def isLocal(deployment: Deploy): Boolean = deployment match { @@ -88,7 +85,7 @@ class Deployer(val settings: ActorSystem.Settings, val eventStream: EventStream, } import scala.collection.JavaConverters._ - settings.config.getConfig("akka.actor.deployment").toObject.keySet.asScala + settings.config.getConfig("akka.actor.deployment").root.keySet.asScala .filterNot("default" ==) .map(path ⇒ pathSubstring(path)) .toSet.toList // toSet to force uniqueness diff --git a/akka-actor/src/main/scala/akka/actor/FaultHandling.scala b/akka-actor/src/main/scala/akka/actor/FaultHandling.scala index 4656f5a3e3..b079550998 100644 --- a/akka-actor/src/main/scala/akka/actor/FaultHandling.scala +++ b/akka-actor/src/main/scala/akka/actor/FaultHandling.scala @@ -122,12 +122,12 @@ abstract class FaultHandlingStrategy { def handleSupervisorFailing(supervisor: ActorRef, children: Iterable[ActorRef]): Unit = { if (children.nonEmpty) - children.foreach(_.suspend()) + children.foreach(_.asInstanceOf[RefInternals].suspend()) } def handleSupervisorRestarted(cause: Throwable, supervisor: ActorRef, children: Iterable[ActorRef]): Unit = { if (children.nonEmpty) - children.foreach(_.restart(cause)) + children.foreach(_.asInstanceOf[RefInternals].restart(cause)) } /** @@ -136,7 +136,7 @@ abstract class FaultHandlingStrategy { def handleFailure(child: ActorRef, cause: Throwable, stats: ChildRestartStats, children: Iterable[ChildRestartStats]): Boolean = { val action = if (decider.isDefinedAt(cause)) decider(cause) else Escalate action match { - case Resume ⇒ child.resume(); true + case Resume ⇒ child.asInstanceOf[RefInternals].resume(); true case Restart ⇒ processFailure(true, child, cause, stats, children); true case Stop ⇒ processFailure(false, child, cause, stats, children); true case Escalate ⇒ false @@ -194,7 +194,7 @@ case class AllForOneStrategy(decider: FaultHandlingStrategy.Decider, def processFailure(restart: Boolean, child: ActorRef, cause: Throwable, stats: ChildRestartStats, children: Iterable[ChildRestartStats]): Unit = { if (children.nonEmpty) { if (restart && children.forall(_.requestRestartPermission(retriesWindow))) - children.foreach(_.child.restart(cause)) + children.foreach(_.child.asInstanceOf[RefInternals].restart(cause)) else children.foreach(_.child.stop()) } @@ -247,7 +247,7 @@ case class OneForOneStrategy(decider: FaultHandlingStrategy.Decider, def processFailure(restart: Boolean, child: ActorRef, cause: Throwable, stats: ChildRestartStats, children: Iterable[ChildRestartStats]): Unit = { if (restart && stats.requestRestartPermission(retriesWindow)) - child.restart(cause) + child.asInstanceOf[RefInternals].restart(cause) else child.stop() //TODO optimization to drop child here already? } diff --git a/akka-actor/src/main/scala/akka/dispatch/AbstractDispatcher.scala b/akka-actor/src/main/scala/akka/dispatch/AbstractDispatcher.scala index 208eae51ca..99557e33c8 100644 --- a/akka-actor/src/main/scala/akka/dispatch/AbstractDispatcher.scala +++ b/akka-actor/src/main/scala/akka/dispatch/AbstractDispatcher.scala @@ -69,6 +69,7 @@ final case class TaskInvocation(eventStream: EventStream, function: () ⇒ Unit, try { function() } catch { + // FIXME catching all and continue isn't good for OOME, ticket #1418 case e ⇒ eventStream.publish(Error(e, "TaskInvocation", e.getMessage)) } finally { cleanup() @@ -135,7 +136,7 @@ abstract class MessageDispatcher(val prerequisites: DispatcherPrerequisites) ext shutdownScheduleUpdater.get(this) match { case UNSCHEDULED ⇒ if (shutdownScheduleUpdater.compareAndSet(this, UNSCHEDULED, SCHEDULED)) { - scheduler.scheduleOnce(shutdownAction, Duration(shutdownTimeout.toMillis, TimeUnit.MILLISECONDS)) + scheduler.scheduleOnce(shutdownAction, shutdownTimeout) () } else ifSensibleToDoSoThenScheduleShutdown() case SCHEDULED ⇒ @@ -210,7 +211,7 @@ abstract class MessageDispatcher(val prerequisites: DispatcherPrerequisites) ext } case RESCHEDULED ⇒ if (shutdownScheduleUpdater.compareAndSet(MessageDispatcher.this, RESCHEDULED, SCHEDULED)) - scheduler.scheduleOnce(this, Duration(shutdownTimeout.toMillis, TimeUnit.MILLISECONDS)) + scheduler.scheduleOnce(this, shutdownTimeout) else run() } } diff --git a/akka-actor/src/main/scala/akka/dispatch/Dispatcher.scala b/akka-actor/src/main/scala/akka/dispatch/Dispatcher.scala index ee28fd586e..1a40ee23cd 100644 --- a/akka-actor/src/main/scala/akka/dispatch/Dispatcher.scala +++ b/akka-actor/src/main/scala/akka/dispatch/Dispatcher.scala @@ -6,12 +6,9 @@ package akka.dispatch import akka.event.Logging.Warning import java.util.concurrent.atomic.AtomicReference -import java.util.concurrent.{ TimeUnit, ExecutorService, RejectedExecutionException, ConcurrentLinkedQueue } -import akka.actor.{ ActorCell, ActorKilledException } -import akka.actor.ActorSystem -import akka.event.EventStream -import akka.actor.Scheduler +import akka.actor.ActorCell import akka.util.Duration +import java.util.concurrent._ /** * Default settings are: @@ -156,4 +153,4 @@ abstract class PriorityGenerator extends java.util.Comparator[Envelope] { final def compare(thisMessage: Envelope, thatMessage: Envelope): Int = gen(thisMessage.message) - gen(thatMessage.message) -} +} \ No newline at end of file diff --git a/akka-actor/src/main/scala/akka/dispatch/Future.scala b/akka-actor/src/main/scala/akka/dispatch/Future.scala index e9a3035ea8..b9def99301 100644 --- a/akka-actor/src/main/scala/akka/dispatch/Future.scala +++ b/akka-actor/src/main/scala/akka/dispatch/Future.scala @@ -27,10 +27,7 @@ class FutureTimeoutException(message: String, cause: Throwable = null) extends A def this(message: String) = this(message, null) } -class FutureFactory(dispatcher: MessageDispatcher, timeout: Timeout) { - - // TODO: remove me ASAP !!! - implicit val _dispatcher = dispatcher +class FutureFactory()(implicit dispatcher: MessageDispatcher, timeout: Timeout) { /** * Java API, equivalent to Future.apply @@ -163,6 +160,7 @@ object Future { try { Right(body) } catch { + // FIXME catching all and continue isn't good for OOME, ticket #1418 case e ⇒ Left(e) } } @@ -411,7 +409,9 @@ object Future { try { next.apply() } catch { - case e ⇒ e.printStackTrace() //TODO FIXME strategy for handling exceptions in callbacks + case e ⇒ + // FIXME catching all and continue isn't good for OOME, ticket #1418 + dispatcher.prerequisites.eventStream.publish(Error(e, "Future.dispatchTask", "Failed to dispatch task, due to: " + e.getMessage)) } } } finally { _taskStack set None } @@ -984,7 +984,7 @@ class DefaultPromise[T](val timeout: Timeout)(implicit val dispatcher: MessageDi def run() { if (!isCompleted) { if (!isExpired) dispatcher.prerequisites.scheduler.scheduleOnce(this, Duration(timeLeftNoinline(), TimeUnit.NANOSECONDS)) - else promise complete (try { Right(fallback) } catch { case e ⇒ Left(e) }) + else promise complete (try { Right(fallback) } catch { case e ⇒ Left(e) }) // FIXME catching all and continue isn't good for OOME, ticket #1418 } } } @@ -994,6 +994,7 @@ class DefaultPromise[T](val timeout: Timeout)(implicit val dispatcher: MessageDi } else this private def notifyCompleted(func: Future[T] ⇒ Unit) { + // FIXME catching all and continue isn't good for OOME, ticket #1418 try { func(this) } catch { case e ⇒ dispatcher.prerequisites.eventStream.publish(Error(e, "Future", "Future onComplete-callback raised an exception")) } //TODO catch, everything? Really? } diff --git a/akka-actor/src/main/scala/akka/dispatch/Mailbox.scala b/akka-actor/src/main/scala/akka/dispatch/Mailbox.scala index ebc8f5df6b..06de342df3 100644 --- a/akka-actor/src/main/scala/akka/dispatch/Mailbox.scala +++ b/akka-actor/src/main/scala/akka/dispatch/Mailbox.scala @@ -28,9 +28,6 @@ object Mailbox { // secondary status: Scheduled bit may be added to Open/Suspended final val Scheduled = 4 - // mailbox debugging helper using println (see below) - // FIXME TODO take this out before release - final val debug = false } /** @@ -40,13 +37,13 @@ abstract class Mailbox(val actor: ActorCell) extends MessageQueue with SystemMes import Mailbox._ @volatile - protected var _status: Status = _ //0 by default + protected var _statusDoNotCallMeDirectly: Status = _ //0 by default @volatile - protected var _systemQueue: SystemMessage = _ //null by default + protected var _systemQueueDoNotCallMeDirectly: SystemMessage = _ //null by default @inline - final def status: Mailbox.Status = _status + final def status: Mailbox.Status = Unsafe.instance.getIntVolatile(this, AbstractMailbox.mailboxStatusOffset) @inline final def shouldProcessMessage: Boolean = (status & 3) == Open @@ -65,7 +62,8 @@ abstract class Mailbox(val actor: ActorCell) extends MessageQueue with SystemMes Unsafe.instance.compareAndSwapInt(this, AbstractMailbox.mailboxStatusOffset, oldStatus, newStatus) @inline - protected final def setStatus(newStatus: Status): Unit = _status = newStatus + protected final def setStatus(newStatus: Status): Unit = + Unsafe.instance.putIntVolatile(this, AbstractMailbox.mailboxStatusOffset, newStatus) /** * set new primary status Open. Caller does not need to worry about whether @@ -130,7 +128,8 @@ abstract class Mailbox(val actor: ActorCell) extends MessageQueue with SystemMes /* * AtomicReferenceFieldUpdater for system queue */ - protected final def systemQueueGet: SystemMessage = _systemQueue + protected final def systemQueueGet: SystemMessage = + Unsafe.instance.getObjectVolatile(this, AbstractMailbox.systemMessageOffset).asInstanceOf[SystemMessage] protected final def systemQueuePut(_old: SystemMessage, _new: SystemMessage): Boolean = Unsafe.instance.compareAndSwapObject(this, AbstractMailbox.systemMessageOffset, _old, _new) @@ -165,7 +164,6 @@ abstract class Mailbox(val actor: ActorCell) extends MessageQueue with SystemMes var processedMessages = 0 val deadlineNs = if (dispatcher.isThroughputDeadlineTimeDefined) System.nanoTime + dispatcher.throughputDeadlineTime.toNanos else 0 do { - if (debug) println(actor.self + " processing message " + nextMessage) actor invoke nextMessage processAllSystemMessages() //After we're done, process all system messages @@ -188,7 +186,6 @@ abstract class Mailbox(val actor: ActorCell) extends MessageQueue with SystemMes var nextMessage = systemDrain() try { while (nextMessage ne null) { - if (debug) println(actor.self + " processing system message " + nextMessage + " with children " + actor.childrenRefs) actor systemInvoke nextMessage nextMessage = nextMessage.next // don’t ever execute normal message when system message present! @@ -243,7 +240,6 @@ trait DefaultSystemMessageQueue { self: Mailbox ⇒ @tailrec final def systemEnqueue(receiver: ActorRef, message: SystemMessage): Unit = { assert(message.next eq null) - if (Mailbox.debug) println(actor.self + " having enqueued " + message) val head = systemQueueGet /* * this write is safely published by the compareAndSet contained within diff --git a/akka-actor/src/main/scala/akka/dispatch/ThreadPoolBuilder.scala b/akka-actor/src/main/scala/akka/dispatch/ThreadPoolBuilder.scala index f543e5c016..c45cc74593 100644 --- a/akka-actor/src/main/scala/akka/dispatch/ThreadPoolBuilder.scala +++ b/akka-actor/src/main/scala/akka/dispatch/ThreadPoolBuilder.scala @@ -11,6 +11,9 @@ import akka.event.Logging.{ Warning, Error } import akka.actor.ActorSystem import java.util.concurrent._ import akka.event.EventStream +import concurrent.forkjoin.ForkJoinPool._ +import concurrent.forkjoin.{ ForkJoinTask, ForkJoinWorkerThread, ForkJoinPool } +import concurrent.forkjoin.ForkJoinTask._ object ThreadPoolConfig { type Bounds = Int @@ -184,6 +187,52 @@ class MonitorableThread(runnable: Runnable, name: String) } } +case class ForkJoinPoolConfig(targetParallelism: Int = Runtime.getRuntime.availableProcessors()) extends ExecutorServiceFactoryProvider { + final def createExecutorServiceFactory(name: String): ExecutorServiceFactory = new ExecutorServiceFactory { + def createExecutorService: ExecutorService = { + new ForkJoinPool(targetParallelism) with ExecutorService { + setAsyncMode(true) + setMaintainsParallelism(true) + + override final def execute(r: Runnable) { + r match { + case fjmbox: FJMailbox ⇒ + //fjmbox.fjTask.reinitialize() + Thread.currentThread match { + case fjwt: ForkJoinWorkerThread if fjwt.getPool eq this ⇒ + fjmbox.fjTask.fork() //We should do fjwt.pushTask(fjmbox.fjTask) but it's package protected + case _ ⇒ super.execute[Unit](fjmbox.fjTask) + } + case _ ⇒ + super.execute(r) + } + } + + import java.util.{ Collection ⇒ JCollection } + + def invokeAny[T](callables: JCollection[_ <: Callable[T]]) = + throw new UnsupportedOperationException("invokeAny. NOT!") + + def invokeAny[T](callables: JCollection[_ <: Callable[T]], l: Long, timeUnit: TimeUnit) = + throw new UnsupportedOperationException("invokeAny. NOT!") + + def invokeAll[T](callables: JCollection[_ <: Callable[T]], l: Long, timeUnit: TimeUnit) = + throw new UnsupportedOperationException("invokeAny. NOT!") + } + } + } +} + +trait FJMailbox { self: Mailbox ⇒ + final val fjTask = new ForkJoinTask[Unit] with Runnable { + private[this] var result: Unit = () + final def getRawResult() = result + final def setRawResult(v: Unit) { result = v } + final def exec() = { self.run(); true } + final def run() { invoke() } + } +} + /** * As the name says */ diff --git a/akka-actor/src/main/scala/akka/event/Logging.scala b/akka-actor/src/main/scala/akka/event/Logging.scala index 374151376e..8e523d45a6 100644 --- a/akka-actor/src/main/scala/akka/event/Logging.scala +++ b/akka-actor/src/main/scala/akka/event/Logging.scala @@ -80,7 +80,7 @@ trait LoggingBus extends ActorEventBus { loggers = Seq(StandardOutLogger) _logLevel = level } - publish(Info(simpleName(this), "StandardOutLogger started")) + publish(Debug(simpleName(this), "StandardOutLogger started")) } private[akka] def startDefaultLoggers(system: ActorSystemImpl) { @@ -113,7 +113,7 @@ trait LoggingBus extends ActorEventBus { loggers = myloggers _logLevel = level } - publish(Info(simpleName(this), "Default Loggers started")) + publish(Debug(simpleName(this), "Default Loggers started")) if (!(defaultLoggers contains StandardOutLoggerName)) { unsubscribe(StandardOutLogger) } @@ -153,7 +153,7 @@ trait LoggingBus extends ActorEventBus { if (response != LoggerInitialized) throw new LoggerInitializationException("Logger " + name + " did not respond with LoggerInitialized, sent instead " + response) AllLogLevels filter (level >= _) foreach (l ⇒ subscribe(actor, classFor(l))) - publish(Info(simpleName(this), "logger " + name + " started")) + publish(Debug(simpleName(this), "logger " + name + " started")) actor } diff --git a/akka-actor/src/main/scala/akka/event/LoggingReceive.scala b/akka-actor/src/main/scala/akka/event/LoggingReceive.scala new file mode 100644 index 0000000000..250af89812 --- /dev/null +++ b/akka-actor/src/main/scala/akka/event/LoggingReceive.scala @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2009-2011 Typesafe Inc. + */ +package akka.event + +import akka.actor.Actor.Receive +import akka.actor.ActorSystem +import akka.event.Logging.Debug + +object LoggingReceive { + + /** + * Wrap a Receive partial function in a logging enclosure, which sends a + * debug message to the event bus each time before a message is matched. + * This includes messages which are not handled. + * + *


+   * def receive = LoggingReceive(this) {
+   *   case x => ...
+   * }
+   * 
+ * + * This method does NOT modify the given Receive unless + * akka.actor.debug.receive is set within akka.conf. + */ + def apply(source: AnyRef)(r: Receive)(implicit system: ActorSystem): Receive = r match { + case _: LoggingReceive ⇒ r + case _ if !system.settings.AddLoggingReceive ⇒ r + case _ ⇒ new LoggingReceive(source, r) + } +} + +/** + * This decorator adds invocation logging to a Receive function. + */ +class LoggingReceive(source: AnyRef, r: Receive)(implicit system: ActorSystem) extends Receive { + def isDefinedAt(o: Any) = { + val handled = r.isDefinedAt(o) + system.eventStream.publish(Debug(LogSource.fromAnyRef(source), "received " + (if (handled) "handled" else "unhandled") + " message " + o)) + handled + } + def apply(o: Any): Unit = r(o) +} \ No newline at end of file diff --git a/akka-actor/src/main/scala/akka/routing/ConnectionManager.scala b/akka-actor/src/main/scala/akka/routing/ConnectionManager.scala index b7655e376e..6e45a50cad 100644 --- a/akka-actor/src/main/scala/akka/routing/ConnectionManager.scala +++ b/akka-actor/src/main/scala/akka/routing/ConnectionManager.scala @@ -11,6 +11,7 @@ import scala.annotation.tailrec import java.util.concurrent.atomic.{ AtomicReference, AtomicInteger } import java.net.InetSocketAddress import akka.remote.RemoteAddress +import collection.JavaConverters /** * An Iterable that also contains a version. @@ -85,6 +86,10 @@ trait ConnectionManager { */ class LocalConnectionManager(initialConnections: Iterable[ActorRef]) extends ConnectionManager { + def this(iterable: java.lang.Iterable[ActorRef]) { + this(JavaConverters.iterableAsScalaIterableConverter(iterable).asScala) + } + case class State(version: Long, connections: Iterable[ActorRef]) extends VersionedIterable[ActorRef] { def iterable = connections } diff --git a/akka-actor/src/main/scala/akka/routing/Routing.scala b/akka-actor/src/main/scala/akka/routing/Routing.scala index 48814a4e43..0bcd4fe22a 100644 --- a/akka-actor/src/main/scala/akka/routing/Routing.scala +++ b/akka-actor/src/main/scala/akka/routing/Routing.scala @@ -9,11 +9,11 @@ import akka.actor._ import akka.config.ConfigurationException import akka.dispatch.{ Future, MessageDispatcher } import akka.util.{ ReflectiveAccess, Duration } -import java.net.InetSocketAddress import java.lang.reflect.InvocationTargetException import java.util.concurrent.atomic.{ AtomicReference, AtomicInteger } import scala.annotation.tailrec +import akka.japi.Creator sealed trait RouterType @@ -76,6 +76,12 @@ case class RoutedProps private[akka] ( connectionManager: ConnectionManager, timeout: Timeout = RoutedProps.defaultTimeout, localOnly: Boolean = RoutedProps.defaultLocalOnly) { + + // Java API + def this(creator: Creator[Router], connectionManager: ConnectionManager, timeout: Timeout, localOnly: Boolean) { + this(() ⇒ creator.create(), connectionManager, timeout, localOnly) + } + } object RoutedProps { @@ -245,7 +251,7 @@ trait BasicRouter extends Router { next match { case Some(connection) ⇒ try { - connection.?(message, timeout).asInstanceOf[Future[T]] //FIXME this does not preserve the original sender, shouldn't it? + connection.?(message, timeout).asInstanceOf[Future[T]] //FIXME this does not preserve the original sender, shouldn't it?? } catch { case e: Exception ⇒ connectionManager.remove(connection) diff --git a/akka-actor/src/main/scala/akka/serialization/Serialization.scala b/akka-actor/src/main/scala/akka/serialization/Serialization.scala index 7232375fa8..d1764e8390 100644 --- a/akka-actor/src/main/scala/akka/serialization/Serialization.scala +++ b/akka-actor/src/main/scala/akka/serialization/Serialization.scala @@ -7,8 +7,7 @@ package akka.serialization import akka.AkkaException import akka.util.ReflectiveAccess import scala.util.DynamicVariable -import com.typesafe.config.{ ConfigRoot, ConfigParseOptions, ConfigFactory, Config } -import com.typesafe.config.Config._ +import com.typesafe.config.Config import akka.config.ConfigurationException import akka.actor.{ Extension, ActorSystem, ActorSystemImpl } @@ -19,11 +18,7 @@ object Serialization { // TODO ensure that these are always set (i.e. withValue()) when doing deserialization val currentSystem = new DynamicVariable[ActorSystemImpl](null) - class Settings(cfg: Config) { - private def referenceConfig: Config = - ConfigFactory.parseResource(classOf[ActorSystem], "/akka-serialization-reference.conf", - ConfigParseOptions.defaults.setAllowMissing(false)) - val config: ConfigRoot = ConfigFactory.emptyRoot("akka-serialization").withFallback(cfg).withFallback(referenceConfig).resolve() + class Settings(val config: Config) { import scala.collection.JavaConverters._ import config._ @@ -37,7 +32,7 @@ object Serialization { hasPath(configPath) match { case false ⇒ Map() case true ⇒ - val serializationBindings: Map[String, Seq[String]] = getConfig(configPath).toObject.unwrapped.asScala.toMap.map { + val serializationBindings: Map[String, Seq[String]] = getConfig(configPath).root.unwrapped.asScala.toMap.map { case (k: String, v: java.util.Collection[_]) ⇒ (k -> v.asScala.toSeq.asInstanceOf[Seq[String]]) case invalid ⇒ throw new ConfigurationException("Invalid serialization-bindings [%s]".format(invalid)) } @@ -47,7 +42,7 @@ object Serialization { } private def toStringMap(mapConfig: Config): Map[String, String] = - mapConfig.toObject.unwrapped.asScala.toMap.map { case (k, v) ⇒ (k, v.toString) } + mapConfig.root.unwrapped.asScala.toMap.map { case (k, v) ⇒ (k, v.toString) } } } @@ -58,7 +53,7 @@ object Serialization { class Serialization(val system: ActorSystemImpl) extends Extension { import Serialization._ - val settings = new Settings(system.applicationConfig) + val settings = new Settings(system.settings.config) //TODO document me def serialize(o: AnyRef): Either[Exception, Array[Byte]] = diff --git a/akka-actor/src/main/scala/akka/util/BoundedBlockingQueue.scala b/akka-actor/src/main/scala/akka/util/BoundedBlockingQueue.scala index 5b17ca5c7d..3c0f386b84 100644 --- a/akka-actor/src/main/scala/akka/util/BoundedBlockingQueue.scala +++ b/akka-actor/src/main/scala/akka/util/BoundedBlockingQueue.scala @@ -108,6 +108,7 @@ class BoundedBlockingQueue[E <: AnyRef]( throw ie } false + // FIXME catching all and continue isn't good for OOME, ticket #1418 case e ⇒ notFull.signal() result = e @@ -234,7 +235,7 @@ class BoundedBlockingQueue[E <: AnyRef]( if (backing.removeAll(c)) { val sz = backing.size() if (sz < maxCapacity) notFull.signal() - if (sz > 0) notEmpty.signal() //FIXME needed? + if (sz > 0) notEmpty.signal() //FIXME needed?? true } else false } finally { @@ -247,7 +248,7 @@ class BoundedBlockingQueue[E <: AnyRef]( try { if (backing.retainAll(c)) { val sz = backing.size() - if (sz < maxCapacity) notFull.signal() //FIXME needed? + if (sz < maxCapacity) notFull.signal() //FIXME needed?? if (sz > 0) notEmpty.signal() true } else false diff --git a/akka-camel/src/main/scala/akka/camel/component/ActorComponent.scala b/akka-camel/src/main/scala/akka/camel/component/ActorComponent.scala index 795fbf5a54..c4ec7dcf31 100644 --- a/akka-camel/src/main/scala/akka/camel/component/ActorComponent.scala +++ b/akka-camel/src/main/scala/akka/camel/component/ActorComponent.scala @@ -293,9 +293,6 @@ private[akka] class AsyncCallbackAdapter(exchange: Exchange, callback: AsyncCall callback.done(false) } - def startsWatching(actorRef: ActorRef): ActorRef = unsupported - def stopsWatching(actorRef: ActorRef): ActorRef = unsupported - def ?(message: Any)(implicit timeout: Timeout): Future[Any] = new KeptPromise[Any](Left(new UnsupportedOperationException("Ask/? is not supported for %s".format(getClass.getName)))) def restart(reason: Throwable): Unit = unsupported diff --git a/akka-docs/general/code/ConfigDocSpec.scala b/akka-docs/general/code/ConfigDocSpec.scala index b7b106b94f..4b3de65e65 100644 --- a/akka-docs/general/code/ConfigDocSpec.scala +++ b/akka-docs/general/code/ConfigDocSpec.scala @@ -6,7 +6,6 @@ import org.scalatest.matchers.MustMatchers //#imports import akka.actor.ActorSystem import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigParseOptions //#imports @@ -21,7 +20,7 @@ class ConfigDocSpec extends WordSpec { nr-of-instances = 3 } } - """, ConfigParseOptions.defaults) + """) val system = ActorSystem("MySystem", ConfigFactory.systemProperties.withFallback(customConf)) //#custom-config diff --git a/akka-docs/general/configuration.rst b/akka-docs/general/configuration.rst index cffd15b4dd..58dfbd5edd 100644 --- a/akka-docs/general/configuration.rst +++ b/akka-docs/general/configuration.rst @@ -16,6 +16,8 @@ configuration files that you see below. You can specify your own configuration f property in the reference config. You only have to define the properties that differ from the default configuration. +FIXME: These default locations has changed + The location of the config file to use can be specified in various ways: * Define the ``-Dakka.config=...`` system property parameter with a file path to configuration file. @@ -44,47 +46,42 @@ Each Akka module has a reference configuration file with the default values. *akka-actor:* -.. literalinclude:: ../../akka-actor/src/main/resources/akka-actor-reference.conf +.. literalinclude:: ../../akka-actor/src/main/resources/reference.conf :language: none *akka-remote:* -.. literalinclude:: ../../akka-remote/src/main/resources/akka-remote-reference.conf +.. literalinclude:: ../../akka-remote/src/main/resources/reference.conf :language: none -*akka-serialization:* - -.. literalinclude:: ../../akka-actor/src/main/resources/akka-serialization-reference.conf - :language: none - *akka-testkit:* -.. literalinclude:: ../../akka-testkit/src/main/resources/akka-testkit-reference.conf +.. literalinclude:: ../../akka-testkit/src/main/resources/reference.conf :language: none *akka-beanstalk-mailbox:* -.. literalinclude:: ../../akka-durable-mailboxes/akka-beanstalk-mailbox/src/main/resources/akka-beanstalk-mailbox-reference.conf +.. literalinclude:: ../../akka-durable-mailboxes/akka-beanstalk-mailbox/src/main/resources/reference.conf :language: none *akka-file-mailbox:* -.. literalinclude:: ../../akka-durable-mailboxes/akka-file-mailbox/src/main/resources/akka-file-mailbox-reference.conf +.. literalinclude:: ../../akka-durable-mailboxes/akka-file-mailbox/src/main/resources/reference.conf :language: none *akka-mongo-mailbox:* -.. literalinclude:: ../../akka-durable-mailboxes/akka-mongo-mailbox/src/main/resources/akka-mongo-mailbox-reference.conf +.. literalinclude:: ../../akka-durable-mailboxes/akka-mongo-mailbox/src/main/resources/reference.conf :language: none *akka-redis-mailbox:* -.. literalinclude:: ../../akka-durable-mailboxes/akka-redis-mailbox/src/main/resources/akka-redis-mailbox-reference.conf +.. literalinclude:: ../../akka-durable-mailboxes/akka-redis-mailbox/src/main/resources/reference.conf :language: none *akka-zookeeper-mailbox:* -.. literalinclude:: ../../akka-durable-mailboxes/akka-zookeeper-mailbox/src/main/resources/akka-zookeeper-mailbox-reference.conf +.. literalinclude:: ../../akka-durable-mailboxes/akka-zookeeper-mailbox/src/main/resources/reference.conf :language: none A custom ``akka.conf`` might look like this:: @@ -121,7 +118,6 @@ A custom ``akka.conf`` might look like this:: } } -.. _-Dakka.mode: Config file format ------------------ @@ -129,9 +125,13 @@ Config file format The configuration file syntax is described in the `HOCON `_ specification. Note that it supports three formats; conf, json, and properties. +.. _-Dakka.mode: + Specifying files for different modes ------------------------------------ +FIXME: mode doesn't exist, or will it? + You can use different configuration files for different purposes by specifying a mode option, either as ``-Dakka.mode=...`` system property or as ``AKKA_MODE=...`` environment variable. For example using DEBUG log level when in development mode. Run with ``-Dakka.mode=dev`` and place the following ``akka.dev.conf`` in the root of @@ -152,6 +152,8 @@ The mode option is not used when specifying the configuration file with ``-Dakka Including files --------------- +FIXME: The include syntax has changed + Sometimes it can be useful to include another configuration file, for example if you have one ``akka.conf`` with all environment independent settings and then override some settings for specific modes. @@ -165,14 +167,14 @@ akka.dev.conf: loglevel = "DEBUG" } -.. _-Dakka.output.config.source: +.. _-Dakka.logConfigOnStart: -Showing Configuration Source ----------------------------- +Logging of Configuration +------------------------ -If the system property ``akka.output.config.source`` is set to anything but -null, then the source from which Akka reads its configuration is printed to the -console during application startup. +If the system or config property ``akka.logConfigOnStart`` is set to ``on``, then the +complete configuration at INFO level when the actor system is started. This is useful +when you are uncertain of what configuration is used. Summary of System Properties ---------------------------- @@ -180,4 +182,3 @@ Summary of System Properties * :ref:`akka.home <-Dakka.home>` (``AKKA_HOME``): where Akka searches for configuration * :ref:`akka.config <-Dakka.config>`: explicit configuration file location * :ref:`akka.mode <-Dakka.mode>` (``AKKA_MODE``): modify configuration file name for multiple profiles -* :ref:`akka.output.config.source <-Dakka.output.config.source>`: whether to print configuration source to console diff --git a/akka-docs/java/dispatchers.rst b/akka-docs/java/dispatchers.rst index d9f32f38c9..dc2684f9d8 100644 --- a/akka-docs/java/dispatchers.rst +++ b/akka-docs/java/dispatchers.rst @@ -155,6 +155,15 @@ Creating a Dispatcher with a priority mailbox using PriorityGenerator: public class Main { // A simple Actor that just prints the messages it processes public static class MyActor extends UntypedActor { + public MyActor() { + self.tell("lowpriority"); + getSelf().tell("lowpriority"); + getSelf().tell("highpriority"); + getSelf().tell("pigdog"); + getSelf().tell("pigdog2"); + getSelf().tell("pigdog3"); + getSelf().tell("highpriority"); + } public void onReceive(Object message) throws Exception { System.out.println(message); } @@ -170,19 +179,9 @@ Creating a Dispatcher with a priority mailbox using PriorityGenerator: } }; // We create an instance of the actor that will print out the messages it processes - ActorRef ref = Actors.actorOf(MyActor.class); - // We create a new Priority dispatcher and seed it with the priority generator - ref.setDispatcher(new Dispatcher("foo", 5, new UnboundedPriorityMailbox(gen))); + // We create a new Priority dispatcher and seed it with the priority generator + ActorRef ref = Actors.actorOf((new Props()).withCreator(MyActor.class).withDispatcher(new Dispatcher("foo", 5, new UnboundedPriorityMailbox(gen)))); - ref.getDispatcher().suspend(ref); // Suspending the actor so it doesn't start to treat the messages before we have enqueued all of them :-) - ref.tell("lowpriority"); - ref.tell("lowpriority"); - ref.tell("highpriority"); - ref.tell("pigdog"); - ref.tell("pigdog2"); - ref.tell("pigdog3"); - ref.tell("highpriority"); - ref.getDispatcher().resume(ref); // Resuming the actor so it will start treating its messages } } diff --git a/akka-docs/java/remote-actors.rst b/akka-docs/java/remote-actors.rst index 745679c537..2147e03db1 100644 --- a/akka-docs/java/remote-actors.rst +++ b/akka-docs/java/remote-actors.rst @@ -178,10 +178,7 @@ The messages that it prevents are all that extends 'LifeCycleMessage': * case object ReceiveTimeout It also prevents the client from invoking any life-cycle and side-effecting methods, such as: -* start * stop -* startsWatching -* stopsWatching * etc. Using secure cookie for remote client authentication diff --git a/akka-docs/scala/dispatchers.rst b/akka-docs/scala/dispatchers.rst index fa00c746f5..e16c336753 100644 --- a/akka-docs/scala/dispatchers.rst +++ b/akka-docs/scala/dispatchers.rst @@ -155,23 +155,18 @@ Creating a Dispatcher using PriorityGenerator: val a = Actor.actorOf( // We create a new Actor that just prints out what it processes Props(new Actor { + self ! 'lowpriority + self ! 'lowpriority + self ! 'highpriority + self ! 'pigdog + self ! 'pigdog2 + self ! 'pigdog3 + self ! 'highpriority def receive = { case x => println(x) } }).withDispatcher(new Dispatcher("foo", 5, UnboundedPriorityMailbox(gen)))) // We create a new Priority dispatcher and seed it with the priority generator - a.dispatcher.suspend(a) // Suspending the actor so it doesn't start to treat the messages before we have enqueued all of them :-) - - a ! 'lowpriority - a ! 'lowpriority - a ! 'highpriority - a ! 'pigdog - a ! 'pigdog2 - a ! 'pigdog3 - a ! 'highpriority - - a.dispatcher.resume(a) // Resuming the actor so it will start treating its messages - Prints: 'highpriority diff --git a/akka-docs/scala/remote-actors.rst b/akka-docs/scala/remote-actors.rst index ab2fe345c8..c408436b1e 100644 --- a/akka-docs/scala/remote-actors.rst +++ b/akka-docs/scala/remote-actors.rst @@ -180,10 +180,7 @@ The messages that it prevents are all that extends 'LifeCycleMessage': * class ReceiveTimeout..) It also prevents the client from invoking any life-cycle and side-effecting methods, such as: -* start * stop -* startsWatching -* stopsWatching * etc. Using secure cookie for remote client authentication diff --git a/akka-docs/scala/testing.rst b/akka-docs/scala/testing.rst index ca1469217c..7ff69fc9a9 100644 --- a/akka-docs/scala/testing.rst +++ b/akka-docs/scala/testing.rst @@ -717,11 +717,12 @@ options: ``akka.actor.debug.receive`` — which enables the :meth:`loggable` statement to be applied to an actor’s :meth:`receive` function:: - def receive = Actor.loggable(this) { // `Actor` unnecessary with import Actor._ + import akka.event.LoggingReceive + def receive = LoggingReceive(this) { case msg => ... } - The first argument to :meth:`loggable` defines the source to be used in the + The first argument to :meth:`LoggingReceive` defines the source to be used in the logging events, which should be the current actor. If the abovementioned setting is not given in ``akka.conf``, this method will diff --git a/akka-durable-mailboxes/akka-beanstalk-mailbox/src/main/resources/akka-beanstalk-mailbox-reference.conf b/akka-durable-mailboxes/akka-beanstalk-mailbox/src/main/resources/reference.conf similarity index 100% rename from akka-durable-mailboxes/akka-beanstalk-mailbox/src/main/resources/akka-beanstalk-mailbox-reference.conf rename to akka-durable-mailboxes/akka-beanstalk-mailbox/src/main/resources/reference.conf diff --git a/akka-durable-mailboxes/akka-beanstalk-mailbox/src/main/scala/akka/actor/mailbox/BeanstalkBasedMailboxExtension.scala b/akka-durable-mailboxes/akka-beanstalk-mailbox/src/main/scala/akka/actor/mailbox/BeanstalkBasedMailboxExtension.scala index 5f6fd40708..eaf8833c86 100644 --- a/akka-durable-mailboxes/akka-beanstalk-mailbox/src/main/scala/akka/actor/mailbox/BeanstalkBasedMailboxExtension.scala +++ b/akka-durable-mailboxes/akka-beanstalk-mailbox/src/main/scala/akka/actor/mailbox/BeanstalkBasedMailboxExtension.scala @@ -4,23 +4,16 @@ package akka.actor.mailbox import com.typesafe.config.Config -import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigParseOptions -import com.typesafe.config.ConfigRoot import akka.util.Duration import java.util.concurrent.TimeUnit.MILLISECONDS import akka.actor._ object BeanstalkBasedMailboxExtension extends ExtensionId[BeanstalkMailboxSettings] with ExtensionIdProvider { def lookup() = this - def createExtension(system: ActorSystemImpl) = new BeanstalkMailboxSettings(system.applicationConfig) + def createExtension(system: ActorSystemImpl) = new BeanstalkMailboxSettings(system.settings.config) } -class BeanstalkMailboxSettings(cfg: Config) extends Extension { - private def referenceConfig: Config = - ConfigFactory.parseResource(classOf[ActorSystem], "/akka-beanstalk-mailbox-reference.conf", - ConfigParseOptions.defaults.setAllowMissing(false)) - val config: ConfigRoot = ConfigFactory.emptyRoot("akka-beanstalk-mailbox").withFallback(cfg).withFallback(referenceConfig).resolve() +class BeanstalkMailboxSettings(val config: Config) extends Extension { import config._ diff --git a/akka-durable-mailboxes/akka-file-mailbox/src/main/resources/akka-file-mailbox-reference.conf b/akka-durable-mailboxes/akka-file-mailbox/src/main/resources/reference.conf similarity index 89% rename from akka-durable-mailboxes/akka-file-mailbox/src/main/resources/akka-file-mailbox-reference.conf rename to akka-durable-mailboxes/akka-file-mailbox/src/main/resources/reference.conf index 313b8d85e9..f81f8995f9 100644 --- a/akka-durable-mailboxes/akka-file-mailbox/src/main/resources/akka-file-mailbox-reference.conf +++ b/akka-durable-mailboxes/akka-file-mailbox/src/main/resources/reference.conf @@ -15,8 +15,8 @@ akka { max-items = 2147483647 max-item-size = 2147483647 bytes max-age = 0s - max-journal-size = 16 megabytes - max-memory-size = 128 megabytes + max-journal-size = 16 MiB + max-memory-size = 128 MiB max-journal-overflow = 10 max-journal-size-absolute = 9223372036854775807 bytes discard-old-when-full = on diff --git a/akka-durable-mailboxes/akka-file-mailbox/src/main/scala/akka/actor/mailbox/FileBasedMailboxExtension.scala b/akka-durable-mailboxes/akka-file-mailbox/src/main/scala/akka/actor/mailbox/FileBasedMailboxExtension.scala index 1bdf9ae958..fcdd3ac29f 100644 --- a/akka-durable-mailboxes/akka-file-mailbox/src/main/scala/akka/actor/mailbox/FileBasedMailboxExtension.scala +++ b/akka-durable-mailboxes/akka-file-mailbox/src/main/scala/akka/actor/mailbox/FileBasedMailboxExtension.scala @@ -4,36 +4,29 @@ package akka.actor.mailbox import com.typesafe.config.Config -import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigParseOptions -import com.typesafe.config.ConfigRoot import akka.util.Duration import java.util.concurrent.TimeUnit.MILLISECONDS import akka.actor._ object FileBasedMailboxExtension extends ExtensionId[FileBasedMailboxSettings] with ExtensionIdProvider { def lookup() = this - def createExtension(system: ActorSystemImpl) = new FileBasedMailboxSettings(system.applicationConfig) + def createExtension(system: ActorSystemImpl) = new FileBasedMailboxSettings(system.settings.config) } -class FileBasedMailboxSettings(cfg: Config) extends Extension { - private def referenceConfig: Config = - ConfigFactory.parseResource(classOf[ActorSystem], "/akka-file-mailbox-reference.conf", - ConfigParseOptions.defaults.setAllowMissing(false)) - val config: ConfigRoot = ConfigFactory.emptyRoot("akka-file-mailbox").withFallback(cfg).withFallback(referenceConfig).resolve() +class FileBasedMailboxSettings(val config: Config) extends Extension { import config._ val QueuePath = getString("akka.actor.mailbox.file-based.directory-path") val MaxItems = getInt("akka.actor.mailbox.file-based.max-items") - val MaxSize = getMemorySizeInBytes("akka.actor.mailbox.file-based.max-size") - val MaxItemSize = getMemorySizeInBytes("akka.actor.mailbox.file-based.max-item-size") + val MaxSize = getBytes("akka.actor.mailbox.file-based.max-size") + val MaxItemSize = getBytes("akka.actor.mailbox.file-based.max-item-size") val MaxAge = Duration(getMilliseconds("akka.actor.mailbox.file-based.max-age"), MILLISECONDS) - val MaxJournalSize = getMemorySizeInBytes("akka.actor.mailbox.file-based.max-journal-size") - val MaxMemorySize = getMemorySizeInBytes("akka.actor.mailbox.file-based.max-memory-size") + val MaxJournalSize = getBytes("akka.actor.mailbox.file-based.max-journal-size") + val MaxMemorySize = getBytes("akka.actor.mailbox.file-based.max-memory-size") val MaxJournalOverflow = getInt("akka.actor.mailbox.file-based.max-journal-overflow") - val MaxJournalSizeAbsolute = getMemorySizeInBytes("akka.actor.mailbox.file-based.max-journal-size-absolute") + val MaxJournalSizeAbsolute = getBytes("akka.actor.mailbox.file-based.max-journal-size-absolute") val DiscardOldWhenFull = getBoolean("akka.actor.mailbox.file-based.discard-old-when-full") val KeepJournal = getBoolean("akka.actor.mailbox.file-based.keep-journal") val SyncJournal = getBoolean("akka.actor.mailbox.file-based.sync-journal") diff --git a/akka-durable-mailboxes/akka-file-mailbox/src/main/scala/akka/actor/mailbox/FiledBasedMailbox.scala b/akka-durable-mailboxes/akka-file-mailbox/src/main/scala/akka/actor/mailbox/FiledBasedMailbox.scala index 8a81b2f8e4..8fa7f81e25 100644 --- a/akka-durable-mailboxes/akka-file-mailbox/src/main/scala/akka/actor/mailbox/FiledBasedMailbox.scala +++ b/akka-durable-mailboxes/akka-file-mailbox/src/main/scala/akka/actor/mailbox/FiledBasedMailbox.scala @@ -62,7 +62,8 @@ class FileBasedMailbox(val owner: ActorCell) extends DurableMailbox(owner) with queue.remove true } catch { - case e ⇒ false //review why catch Throwable? And swallow potential Errors? + // FIXME catching all and continue isn't good for OOME, ticket #1418 + case e ⇒ false } } diff --git a/akka-durable-mailboxes/akka-file-mailbox/src/main/scala/akka/actor/mailbox/filequeue/tools/QDumper.scala b/akka-durable-mailboxes/akka-file-mailbox/src/main/scala/akka/actor/mailbox/filequeue/tools/QDumper.scala index eb8a7f9e0b..54c5ba36b6 100644 --- a/akka-durable-mailboxes/akka-file-mailbox/src/main/scala/akka/actor/mailbox/filequeue/tools/QDumper.scala +++ b/akka-durable-mailboxes/akka-file-mailbox/src/main/scala/akka/actor/mailbox/filequeue/tools/QDumper.scala @@ -148,5 +148,7 @@ object QDumper { println("Queue: " + filename) new QueueDumper(filename, system.log)() } + + system.stop() } } diff --git a/akka-durable-mailboxes/akka-mailboxes-common/src/test/scala/akka/actor/mailbox/DurableMailboxSpec.scala b/akka-durable-mailboxes/akka-mailboxes-common/src/test/scala/akka/actor/mailbox/DurableMailboxSpec.scala index ab0f0206d3..9d118e3a96 100644 --- a/akka-durable-mailboxes/akka-mailboxes-common/src/test/scala/akka/actor/mailbox/DurableMailboxSpec.scala +++ b/akka-durable-mailboxes/akka-mailboxes-common/src/test/scala/akka/actor/mailbox/DurableMailboxSpec.scala @@ -44,7 +44,8 @@ abstract class DurableMailboxSpec(val backendName: String, val mailboxType: Dura sender ! PoisonPill } - "handle reply to ! for multiple messages" in { + // FIXME ignored due to zookeeper issue, ticket #1423 + "handle reply to ! for multiple messages" ignore { val latch = new CountDownLatch(5) val queueActor = createMailboxTestActor(backendName + " should handle reply to !") val sender = actorOf(Props(new Sender(latch))) diff --git a/akka-durable-mailboxes/akka-mongo-mailbox/src/main/resources/akka-mongo-mailbox-reference.conf b/akka-durable-mailboxes/akka-mongo-mailbox/src/main/resources/reference.conf similarity index 100% rename from akka-durable-mailboxes/akka-mongo-mailbox/src/main/resources/akka-mongo-mailbox-reference.conf rename to akka-durable-mailboxes/akka-mongo-mailbox/src/main/resources/reference.conf diff --git a/akka-durable-mailboxes/akka-mongo-mailbox/src/main/scala/akka/actor/mailbox/MongoBasedMailboxExtension.scala b/akka-durable-mailboxes/akka-mongo-mailbox/src/main/scala/akka/actor/mailbox/MongoBasedMailboxExtension.scala index 88eb95438c..3075edf9e9 100644 --- a/akka-durable-mailboxes/akka-mongo-mailbox/src/main/scala/akka/actor/mailbox/MongoBasedMailboxExtension.scala +++ b/akka-durable-mailboxes/akka-mongo-mailbox/src/main/scala/akka/actor/mailbox/MongoBasedMailboxExtension.scala @@ -4,23 +4,16 @@ package akka.actor.mailbox import com.typesafe.config.Config -import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigParseOptions -import com.typesafe.config.ConfigRoot import akka.util.Duration import java.util.concurrent.TimeUnit.MILLISECONDS import akka.actor._ object MongoBasedMailboxExtension extends ExtensionId[MongoBasedMailboxSettings] with ExtensionIdProvider { def lookup() = this - def createExtension(system: ActorSystemImpl) = new MongoBasedMailboxSettings(system.applicationConfig) + def createExtension(system: ActorSystemImpl) = new MongoBasedMailboxSettings(system.settings.config) } -class MongoBasedMailboxSettings(cfg: Config) extends Extension { - private def referenceConfig: Config = - ConfigFactory.parseResource(classOf[ActorSystem], "/akka-mongo-mailbox-reference.conf", - ConfigParseOptions.defaults.setAllowMissing(false)) - val config: ConfigRoot = ConfigFactory.emptyRoot("akka-mongo-mailbox").withFallback(cfg).withFallback(referenceConfig).resolve() +class MongoBasedMailboxSettings(val config: Config) extends Extension { import config._ diff --git a/akka-durable-mailboxes/akka-redis-mailbox/src/main/resources/akka-redis-mailbox-reference.conf b/akka-durable-mailboxes/akka-redis-mailbox/src/main/resources/reference.conf similarity index 100% rename from akka-durable-mailboxes/akka-redis-mailbox/src/main/resources/akka-redis-mailbox-reference.conf rename to akka-durable-mailboxes/akka-redis-mailbox/src/main/resources/reference.conf diff --git a/akka-durable-mailboxes/akka-redis-mailbox/src/main/scala/akka/actor/mailbox/RedisBasedMailboxExtension.scala b/akka-durable-mailboxes/akka-redis-mailbox/src/main/scala/akka/actor/mailbox/RedisBasedMailboxExtension.scala index beccf4051f..ef25eead1d 100644 --- a/akka-durable-mailboxes/akka-redis-mailbox/src/main/scala/akka/actor/mailbox/RedisBasedMailboxExtension.scala +++ b/akka-durable-mailboxes/akka-redis-mailbox/src/main/scala/akka/actor/mailbox/RedisBasedMailboxExtension.scala @@ -4,21 +4,14 @@ package akka.actor.mailbox import com.typesafe.config.Config -import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigParseOptions -import com.typesafe.config.ConfigRoot import akka.actor._ object RedisBasedMailboxExtension extends ExtensionId[RedisBasedMailboxSettings] with ExtensionIdProvider { def lookup() = this - def createExtension(system: ActorSystemImpl) = new RedisBasedMailboxSettings(system.applicationConfig) + def createExtension(system: ActorSystemImpl) = new RedisBasedMailboxSettings(system.settings.config) } -class RedisBasedMailboxSettings(cfg: Config) extends Extension { - private def referenceConfig: Config = - ConfigFactory.parseResource(classOf[ActorSystem], "/akka-redis-mailbox-reference.conf", - ConfigParseOptions.defaults.setAllowMissing(false)) - val config: ConfigRoot = ConfigFactory.emptyRoot("akka-redis-mailbox").withFallback(cfg).withFallback(referenceConfig).resolve() +class RedisBasedMailboxSettings(val config: Config) extends Extension { import config._ diff --git a/akka-durable-mailboxes/akka-zookeeper-mailbox/src/main/java/akka/cluster/zookeeper/ZooKeeperQueue.java b/akka-durable-mailboxes/akka-zookeeper-mailbox/src/main/java/akka/cluster/zookeeper/ZooKeeperQueue.java index af76896f4b..4e06b64e6b 100644 --- a/akka-durable-mailboxes/akka-zookeeper-mailbox/src/main/java/akka/cluster/zookeeper/ZooKeeperQueue.java +++ b/akka-durable-mailboxes/akka-zookeeper-mailbox/src/main/java/akka/cluster/zookeeper/ZooKeeperQueue.java @@ -66,7 +66,7 @@ public class ZooKeeperQueue { return element.getData(); } else { throw new UnsupportedOperationException("Non-blocking ZooKeeperQueue is not yet supported"); - /* FIXME DOES NOT WORK + /* TODO DOES NOT WORK try { String headName = getSmallestElement(_zkClient.getChildren(_elementsPath)); String headPath = getElementPath(headName); diff --git a/akka-durable-mailboxes/akka-zookeeper-mailbox/src/main/resources/akka-zookeeper-mailbox-reference.conf b/akka-durable-mailboxes/akka-zookeeper-mailbox/src/main/resources/reference.conf similarity index 100% rename from akka-durable-mailboxes/akka-zookeeper-mailbox/src/main/resources/akka-zookeeper-mailbox-reference.conf rename to akka-durable-mailboxes/akka-zookeeper-mailbox/src/main/resources/reference.conf diff --git a/akka-durable-mailboxes/akka-zookeeper-mailbox/src/main/scala/akka/actor/mailbox/ZooKeeperBasedMailbox.scala b/akka-durable-mailboxes/akka-zookeeper-mailbox/src/main/scala/akka/actor/mailbox/ZooKeeperBasedMailbox.scala index 8350f743d5..c5efa62358 100644 --- a/akka-durable-mailboxes/akka-zookeeper-mailbox/src/main/scala/akka/actor/mailbox/ZooKeeperBasedMailbox.scala +++ b/akka-durable-mailboxes/akka-zookeeper-mailbox/src/main/scala/akka/actor/mailbox/ZooKeeperBasedMailbox.scala @@ -59,7 +59,7 @@ class ZooKeeperBasedMailbox(val owner: ActorCell) extends DurableMailbox(owner) queue.clear true } catch { - case e ⇒ false + case e: Exception ⇒ false } override def cleanUp() { diff --git a/akka-durable-mailboxes/akka-zookeeper-mailbox/src/main/scala/akka/actor/mailbox/ZooKeeperBasedMailboxExtension.scala b/akka-durable-mailboxes/akka-zookeeper-mailbox/src/main/scala/akka/actor/mailbox/ZooKeeperBasedMailboxExtension.scala index e2b0ad45f7..2d2e7c1be1 100644 --- a/akka-durable-mailboxes/akka-zookeeper-mailbox/src/main/scala/akka/actor/mailbox/ZooKeeperBasedMailboxExtension.scala +++ b/akka-durable-mailboxes/akka-zookeeper-mailbox/src/main/scala/akka/actor/mailbox/ZooKeeperBasedMailboxExtension.scala @@ -4,22 +4,15 @@ package akka.actor.mailbox import com.typesafe.config.Config -import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigParseOptions -import com.typesafe.config.ConfigRoot import akka.util.Duration import java.util.concurrent.TimeUnit.MILLISECONDS import akka.actor._ object ZooKeeperBasedMailboxExtension extends ExtensionId[ZooKeeperBasedMailboxSettings] with ExtensionIdProvider { def lookup() = this - def createExtension(system: ActorSystemImpl) = new ZooKeeperBasedMailboxSettings(system.applicationConfig) + def createExtension(system: ActorSystemImpl) = new ZooKeeperBasedMailboxSettings(system.settings.config) } -class ZooKeeperBasedMailboxSettings(cfg: Config) extends Extension { - private def referenceConfig: Config = - ConfigFactory.parseResource(classOf[ActorSystem], "/akka-zookeeper-mailbox-reference.conf", - ConfigParseOptions.defaults.setAllowMissing(false)) - val config: ConfigRoot = ConfigFactory.emptyRoot("akka-zookeeper-mailbox").withFallback(cfg).withFallback(referenceConfig).resolve() +class ZooKeeperBasedMailboxSettings(val config: Config) extends Extension { import config._ diff --git a/akka-durable-mailboxes/akka-zookeeper-mailbox/src/test/scala/akka/actor/mailbox/ZooKeeperBasedMailboxSpec.scala b/akka-durable-mailboxes/akka-zookeeper-mailbox/src/test/scala/akka/actor/mailbox/ZooKeeperBasedMailboxSpec.scala index da363beaaf..3cdc734830 100644 --- a/akka-durable-mailboxes/akka-zookeeper-mailbox/src/test/scala/akka/actor/mailbox/ZooKeeperBasedMailboxSpec.scala +++ b/akka-durable-mailboxes/akka-zookeeper-mailbox/src/test/scala/akka/actor/mailbox/ZooKeeperBasedMailboxSpec.scala @@ -19,7 +19,7 @@ class ZooKeeperBasedMailboxSpec extends DurableMailboxSpec("ZooKeeper", ZooKeepe } override def atTermination() { - zkServer.shutdown + zkServer.shutdown() super.atTermination() } } diff --git a/akka-remote/src/main/resources/akka-remote-reference.conf b/akka-remote/src/main/resources/reference.conf similarity index 86% rename from akka-remote/src/main/resources/akka-remote-reference.conf rename to akka-remote/src/main/resources/reference.conf index 39fcda8cd7..a80c4fa6a7 100644 --- a/akka-remote/src/main/resources/akka-remote-reference.conf +++ b/akka-remote/src/main/resources/reference.conf @@ -8,8 +8,7 @@ akka { remote { - # FIXME rename to transport - layer = "akka.cluster.netty.NettyRemoteSupport" + transport = "akka.cluster.netty.NettyRemoteSupport" use-compression = off @@ -27,11 +26,20 @@ akka { # generates fewer mistakes but needs more time to detect actual crashes max-sample-size = 1000 } + + gossip { + initialDelay = 5s + frequency = 1s + } + + compute-grid-dispatcher { # The dispatcher used for remote system messages + name = ComputeGridDispatcher # defaults to same settings as default-dispatcher + } server { hostname = "" # The hostname or ip to bind the remoting to, InetAddress.getLocalHost.getHostAddress is used if empty port = 2552 # The default remote server port clients should connect to. Default is 2552 (AKKA) - message-frame-size = 1048576 # Increase this if you want to be able to send messages with large payloads + message-frame-size = 1 MiB # Increase this if you want to be able to send messages with large payloads connection-timeout = 120s # Timeout duration require-cookie = off # Should the remote server require that it peers share the same secure-cookie (defined in the 'remote' section)? untrusted-mode = off # Enable untrusted mode for full security of server managed actors, allows untrusted clients to connect. @@ -46,7 +54,7 @@ akka { } reconnect-delay = 5s read-timeout = 3600s - message-frame-size = 1048576 + message-frame-size = 1 MiB reconnection-time-window = 600s # Maximum time window that a client should try to reconnect for } } diff --git a/akka-remote/src/main/scala/akka/remote/Gossiper.scala b/akka-remote/src/main/scala/akka/remote/Gossiper.scala index 4854489839..e03e5e2685 100644 --- a/akka-remote/src/main/scala/akka/remote/Gossiper.scala +++ b/akka-remote/src/main/scala/akka/remote/Gossiper.scala @@ -15,6 +15,7 @@ import akka.config.ConfigurationException import akka.serialization.SerializationExtension import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.TimeUnit.SECONDS import java.security.SecureRandom import System.{ currentTimeMillis ⇒ newTimestamp } @@ -122,19 +123,15 @@ class Gossiper(remote: Remote) { private val nodeFingerprint = address.## private val random = SecureRandom.getInstance("SHA1PRNG") - private val initalDelayForGossip = 5 seconds // FIXME make configurable - private val gossipFrequency = 1 seconds // FIXME make configurable - private val timeUnit = { - assert(gossipFrequency.unit == initalDelayForGossip.unit) - initalDelayForGossip.unit - } + private val initalDelayForGossip = remoteExtension.InitalDelayForGossip + private val gossipFrequency = remoteExtension.GossipFrequency private val state = new AtomicReference[State](State(currentGossip = newGossip())) { // start periodic gossip and cluster scrutinization - default is run them every second with 1/2 second in between - system.scheduler schedule (() ⇒ initateGossip(), Duration(initalDelayForGossip.toSeconds, timeUnit), Duration(gossipFrequency.toSeconds, timeUnit)) - system.scheduler schedule (() ⇒ scrutinize(), Duration(initalDelayForGossip.toSeconds, timeUnit), Duration(gossipFrequency.toSeconds, timeUnit)) + system.scheduler schedule (() ⇒ initateGossip(), Duration(initalDelayForGossip.toSeconds, SECONDS), Duration(gossipFrequency.toSeconds, SECONDS)) + system.scheduler schedule (() ⇒ scrutinize(), Duration(initalDelayForGossip.toSeconds, SECONDS), Duration(gossipFrequency.toSeconds, SECONDS)) } /** diff --git a/akka-remote/src/main/scala/akka/remote/NetworkEventStream.scala b/akka-remote/src/main/scala/akka/remote/NetworkEventStream.scala index c6e77c3416..2cad35c948 100644 --- a/akka-remote/src/main/scala/akka/remote/NetworkEventStream.scala +++ b/akka-remote/src/main/scala/akka/remote/NetworkEventStream.scala @@ -45,7 +45,7 @@ object NetworkEventStream { case event: RemoteClientLifeCycleEvent ⇒ listeners(event.remoteAddress) foreach (_ notify event) - case event: RemoteServerLifeCycleEvent ⇒ // FIXME handle RemoteServerLifeCycleEvent + case event: RemoteServerLifeCycleEvent ⇒ // FIXME handle RemoteServerLifeCycleEvent, ticket #1408 and #1190 case Register(listener, connectionAddress) ⇒ listeners(connectionAddress) += listener @@ -62,7 +62,7 @@ class NetworkEventStream(system: ActorSystemImpl) { import NetworkEventStream._ - // FIXME: check that this supervision is correct + // FIXME: check that this supervision is correct, ticket #1408 private[akka] val sender = system.systemActorOf(Props[Channel].copy(dispatcher = system.dispatcherFactory.newPinnedDispatcher("NetworkEventStream")), "network-event-sender") diff --git a/akka-remote/src/main/scala/akka/remote/Remote.scala b/akka-remote/src/main/scala/akka/remote/Remote.scala index ee8a04efed..71ca70ec99 100644 --- a/akka-remote/src/main/scala/akka/remote/Remote.scala +++ b/akka-remote/src/main/scala/akka/remote/Remote.scala @@ -47,10 +47,9 @@ class Remote(val system: ActorSystemImpl, val nodename: String) { val remoteDaemonServiceName = "akka-system-remote-daemon".intern - // FIXME configure computeGridDispatcher to what? - val computeGridDispatcher = dispatcherFactory.newDispatcher("akka:compute-grid").build + val computeGridDispatcher = dispatcherFactory.fromConfig("akka.remote.compute-grid-dispatcher") - // FIXME it is probably better to create another supervisor for handling the children created by handle_* + // FIXME it is probably better to create another supervisor for handling the children created by handle_*, ticket #1408 private[remote] lazy val remoteDaemonSupervisor = system.actorOf(Props( OneForOneStrategy(List(classOf[Exception]), None, None)), "akka-system-remote-supervisor") // is infinite restart what we want? @@ -71,19 +70,17 @@ class Remote(val system: ActorSystemImpl, val nodename: String) { lazy val eventStream = new NetworkEventStream(system) - @volatile - private var _server: RemoteSupport = _ - def server = _server - - def start(): Unit = { + lazy val server: RemoteSupport = { val remote = new akka.remote.netty.NettyRemoteSupport(system, this) - remote.start() //TODO FIXME Any application loader here? + remote.start() //TODO Any application loader here? system.eventStream.subscribe(eventStream.sender, classOf[RemoteLifeCycleEvent]) system.eventStream.subscribe(remoteClientLifeCycleHandler, classOf[RemoteLifeCycleEvent]) - _server = remote + remote + } + def start() { val daemonPath = remoteDaemon.path //Force init of daemon log.info("Starting remote server on [{}] and starting remoteDaemon with path [{}]", remoteAddress, daemonPath) } @@ -160,9 +157,9 @@ class RemoteSystemDaemon(remote: Remote) extends Actor { sender ! Success(remoteAddress) } catch { - case error: Throwable ⇒ //FIXME doesn't seem sensible - sender ! Failure(error) - throw error + case exc: Exception ⇒ + sender ! Failure(exc) + throw exc } } @@ -195,7 +192,7 @@ class RemoteSystemDaemon(remote: Remote) extends Actor { def tempName = "$_" + Helpers.base64(tempNumber.getAndIncrement()) def tempPath = remoteDaemon.path / tempName - // FIXME: handle real remote supervision + // FIXME: handle real remote supervision, ticket #1408 def handle_fun0_unit(message: RemoteSystemDaemonMessageProtocol) { new LocalActorRef(systemImpl, Props( @@ -204,7 +201,7 @@ class RemoteSystemDaemon(remote: Remote) extends Actor { }).copy(dispatcher = computeGridDispatcher), remoteDaemon, tempPath, systemService = true) ! payloadFor(message, classOf[Function0[Unit]]) } - // FIXME: handle real remote supervision + // FIXME: handle real remote supervision, ticket #1408 def handle_fun0_any(message: RemoteSystemDaemonMessageProtocol) { new LocalActorRef(systemImpl, Props( @@ -213,7 +210,7 @@ class RemoteSystemDaemon(remote: Remote) extends Actor { }).copy(dispatcher = computeGridDispatcher), remoteDaemon, tempPath, systemService = true) forward payloadFor(message, classOf[Function0[Any]]) } - // FIXME: handle real remote supervision + // FIXME: handle real remote supervision, ticket #1408 def handle_fun1_arg_unit(message: RemoteSystemDaemonMessageProtocol) { new LocalActorRef(systemImpl, Props( @@ -222,7 +219,7 @@ class RemoteSystemDaemon(remote: Remote) extends Actor { }).copy(dispatcher = computeGridDispatcher), remoteDaemon, tempPath, systemService = true) ! payloadFor(message, classOf[Tuple2[Function1[Any, Unit], Any]]) } - // FIXME: handle real remote supervision + // FIXME: handle real remote supervision, ticket #1408 def handle_fun1_arg_any(message: RemoteSystemDaemonMessageProtocol) { new LocalActorRef(systemImpl, Props( diff --git a/akka-remote/src/main/scala/akka/remote/RemoteActorRefProvider.scala b/akka-remote/src/main/scala/akka/remote/RemoteActorRefProvider.scala index 41f25c70bd..cf55c8edb1 100644 --- a/akka-remote/src/main/scala/akka/remote/RemoteActorRefProvider.scala +++ b/akka-remote/src/main/scala/akka/remote/RemoteActorRefProvider.scala @@ -83,7 +83,7 @@ class RemoteActorRefProvider( if (systemService) local.actorOf(system, props, supervisor, name, systemService) else { val path = supervisor.path / name - val newFuture = Promise[ActorRef](5000)(dispatcher) // FIXME is this proper timeout? + val newFuture = Promise[ActorRef](system.settings.ActorTimeout)(dispatcher) actors.putIfAbsent(path.toString, newFuture) match { // we won the race -- create the actor and resolve the future case null ⇒ @@ -97,7 +97,7 @@ class RemoteActorRefProvider( if (isReplicaNode) { // we are on one of the replica node for this remote actor - local.actorOf(system, props, supervisor, name, true) //FIXME systemService = true here to bypass Deploy, should be fixed when create-or-get is replaced by get-or-create + local.actorOf(system, props, supervisor, name, true) //FIXME systemService = true here to bypass Deploy, should be fixed when create-or-get is replaced by get-or-create (is this fixed now?) } else { implicit val dispatcher = if (props.dispatcher == Props.defaultDispatcher) system.dispatcher else props.dispatcher @@ -174,7 +174,7 @@ class RemoteActorRefProvider( /** * Copied from LocalActorRefProvider... */ - // FIXME: implement supervision + // FIXME: implement supervision, ticket #1408 def actorOf(system: ActorSystem, props: RoutedProps, supervisor: ActorRef, name: String): ActorRef = { if (props.connectionManager.isEmpty) throw new ConfigurationException("RoutedProps used for creating actor [" + name + "] has zero connections configured; can't create a router") new RoutedActorRef(system, props, supervisor, name) @@ -246,7 +246,7 @@ class RemoteActorRefProvider( } } - private[akka] def createDeathWatch(): DeathWatch = local.createDeathWatch() //FIXME Implement Remote DeathWatch + private[akka] def createDeathWatch(): DeathWatch = local.createDeathWatch() //FIXME Implement Remote DeathWatch, ticket ##1190 private[akka] def ask(message: Any, recipient: ActorRef, within: Timeout): Future[Any] = local.ask(message, recipient, within) @@ -265,7 +265,7 @@ private[akka] case class RemoteActorRef private[akka] ( remoteAddress: RemoteAddress, path: ActorPath, loader: Option[ClassLoader]) - extends ActorRef with ScalaActorRef { + extends ActorRef with ScalaActorRef with RefInternals { @volatile private var running: Boolean = true @@ -276,7 +276,7 @@ private[akka] case class RemoteActorRef private[akka] ( def isTerminated: Boolean = !running - protected[akka] def sendSystemMessage(message: SystemMessage): Unit = unsupported + protected[akka] def sendSystemMessage(message: SystemMessage): Unit = throw new UnsupportedOperationException("Not supported for RemoteActorRef") override def !(message: Any)(implicit sender: ActorRef = null): Unit = remote.send(message, Option(sender), remoteAddress, this, loader) @@ -286,7 +286,7 @@ private[akka] case class RemoteActorRef private[akka] ( def resume(): Unit = () - def stop() { //FIXME send the cause as well! + def stop() { synchronized { if (running) { running = false @@ -298,11 +298,5 @@ private[akka] case class RemoteActorRef private[akka] ( @throws(classOf[java.io.ObjectStreamException]) private def writeReplace(): AnyRef = SerializedActorRef(path.toString) - def startsWatching(actorRef: ActorRef): ActorRef = unsupported //FIXME Implement - - def stopsWatching(actorRef: ActorRef): ActorRef = unsupported //FIXME Implement - protected[akka] def restart(cause: Throwable): Unit = () - - private def unsupported = throw new UnsupportedOperationException("Not supported for RemoteActorRef") } diff --git a/akka-remote/src/main/scala/akka/remote/RemoteExtension.scala b/akka-remote/src/main/scala/akka/remote/RemoteExtension.scala index 9c44f18462..33ba83dd73 100644 --- a/akka-remote/src/main/scala/akka/remote/RemoteExtension.scala +++ b/akka-remote/src/main/scala/akka/remote/RemoteExtension.scala @@ -4,44 +4,37 @@ package akka.remote import com.typesafe.config.Config -import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigParseOptions -import com.typesafe.config.ConfigRoot import akka.util.Duration import java.util.concurrent.TimeUnit.MILLISECONDS import java.net.InetAddress import akka.config.ConfigurationException import com.eaio.uuid.UUID import akka.actor._ - import scala.collection.JavaConverters._ object RemoteExtension extends ExtensionId[RemoteExtensionSettings] with ExtensionIdProvider { def lookup() = this - def createExtension(system: ActorSystemImpl) = new RemoteExtensionSettings(system.applicationConfig, system.name) + def createExtension(system: ActorSystemImpl) = new RemoteExtensionSettings(system.settings.config, system.name) } -class RemoteExtensionSettings(cfg: Config, val systemName: String) extends Extension { - private def referenceConfig: Config = - ConfigFactory.parseResource(classOf[ActorSystem], "/akka-remote-reference.conf", - ConfigParseOptions.defaults.setAllowMissing(false)) - val config: ConfigRoot = ConfigFactory.emptyRoot("akka-remote").withFallback(cfg).withFallback(referenceConfig).resolve() +class RemoteExtensionSettings(val config: Config, val systemName: String) extends Extension { import config._ - val RemoteTransport = getString("akka.remote.layer") + val RemoteTransport = getString("akka.remote.transport") val FailureDetectorThreshold = getInt("akka.remote.failure-detector.threshold") val FailureDetectorMaxSampleSize = getInt("akka.remote.failure-detector.max-sample-size") val ShouldCompressData = config.getBoolean("akka.remote.use-compression") val RemoteSystemDaemonAckTimeout = Duration(config.getMilliseconds("akka.remote.remote-daemon-ack-timeout"), MILLISECONDS) + val InitalDelayForGossip = Duration(config.getMilliseconds("akka.remote.gossip.initialDelay"), MILLISECONDS) + val GossipFrequency = Duration(config.getMilliseconds("akka.remote.gossip.frequency"), MILLISECONDS) // TODO cluster config will go into akka-cluster-reference.conf when we enable that module val ClusterName = getString("akka.cluster.name") val SeedNodes = Set.empty[RemoteAddress] ++ getStringList("akka.cluster.seed-nodes").asScala.toSeq.map(RemoteAddress(_, systemName)) - // FIXME remove nodename from config - should only be passed as command line arg or read from properties file etc. val NodeName: String = config.getString("akka.cluster.nodename") match { - case "" ⇒ new UUID().toString + case "" ⇒ throw new ConfigurationException("akka.cluster.nodename configuration property must be defined") case value ⇒ value } @@ -57,12 +50,12 @@ class RemoteExtensionSettings(cfg: Config, val systemName: String) extends Exten val ReconnectionTimeWindow = Duration(config.getMilliseconds("akka.remote.client.reconnection-time-window"), MILLISECONDS) val ReadTimeout = Duration(config.getMilliseconds("akka.remote.client.read-timeout"), MILLISECONDS) val ReconnectDelay = Duration(config.getMilliseconds("akka.remote.client.reconnect-delay"), MILLISECONDS) - val MessageFrameSize = config.getInt("akka.remote.client.message-frame-size") + val MessageFrameSize = config.getBytes("akka.remote.client.message-frame-size").toInt } class RemoteServerSettings { import scala.collection.JavaConverters._ - val MessageFrameSize = config.getInt("akka.remote.server.message-frame-size") + val MessageFrameSize = config.getBytes("akka.remote.server.message-frame-size").toInt val SecureCookie: Option[String] = config.getString("akka.remote.secure-cookie") match { case "" ⇒ None case cookie ⇒ Some(cookie) diff --git a/akka-remote/src/main/scala/akka/remote/netty/NettyRemoteSupport.scala b/akka-remote/src/main/scala/akka/remote/netty/NettyRemoteSupport.scala index 04abcdc038..f5802fe8ee 100644 --- a/akka-remote/src/main/scala/akka/remote/netty/NettyRemoteSupport.scala +++ b/akka-remote/src/main/scala/akka/remote/netty/NettyRemoteSupport.scala @@ -59,35 +59,34 @@ abstract class RemoteClient private[akka] ( /** * Converts the message to the wireprotocol and sends the message across the wire */ - def send(message: Any, senderOption: Option[ActorRef], recipient: ActorRef): Unit = + def send(message: Any, senderOption: Option[ActorRef], recipient: ActorRef): Unit = if (isRunning) { send(remoteSupport.createRemoteMessageProtocolBuilder(Left(recipient), Right(message), senderOption).build) + } else { + val exception = new RemoteClientException("RemoteModule client is not running, make sure you have invoked 'RemoteClient.connect()' before using it.", remoteSupport, remoteAddress) + remoteSupport.notifyListeners(RemoteClientError(exception, remoteSupport, remoteAddress)) + throw exception + } /** * Sends the message across the wire */ - def send(request: RemoteMessageProtocol) { - if (isRunning) { //TODO FIXME RACY - log.debug("Sending message: " + new RemoteMessage(request, remoteSupport)) + def send(request: RemoteMessageProtocol): Unit = { + log.debug("Sending message: {}", new RemoteMessage(request, remoteSupport)) - try { - val payload = remoteSupport.createMessageSendEnvelope(request) - currentChannel.write(payload).addListener( - new ChannelFutureListener { - def operationComplete(future: ChannelFuture) { - if (future.isCancelled) { - //Not interesting at the moment - } else if (!future.isSuccess) { - remoteSupport.notifyListeners(RemoteClientWriteFailed(payload, future.getCause, remoteSupport, remoteAddress)) - } + try { + val payload = remoteSupport.createMessageSendEnvelope(request) + currentChannel.write(payload).addListener( + new ChannelFutureListener { + def operationComplete(future: ChannelFuture) { + if (future.isCancelled) { + //Not interesting at the moment + } else if (!future.isSuccess) { + remoteSupport.notifyListeners(RemoteClientWriteFailed(payload, future.getCause, remoteSupport, remoteAddress)) } - }) - } catch { - case e: Exception ⇒ remoteSupport.notifyListeners(RemoteClientError(e, remoteSupport, remoteAddress)) - } - } else { - val exception = new RemoteClientException("RemoteModule client is not running, make sure you have invoked 'RemoteClient.connect()' before using it.", remoteSupport, remoteAddress) - remoteSupport.notifyListeners(RemoteClientError(exception, remoteSupport, remoteAddress)) - throw exception + } + }) + } catch { + case e: Exception ⇒ remoteSupport.notifyListeners(RemoteClientError(e, remoteSupport, remoteAddress)) } } @@ -125,15 +124,14 @@ class ActiveRemoteClient private[akka] ( import remoteSupport.clientSettings._ - //FIXME rewrite to a wrapper object (minimize volatile access and maximize encapsulation) + //TODO rewrite to a wrapper object (minimize volatile access and maximize encapsulation) @volatile private var bootstrap: ClientBootstrap = _ @volatile private[remote] var connection: ChannelFuture = _ @volatile private[remote] var openChannels: DefaultChannelGroup = _ - @volatile - private var timer: HashedWheelTimer = _ + @volatile private var reconnectionTimeWindowStart = 0L @@ -161,7 +159,7 @@ class ActiveRemoteClient private[akka] ( def closeChannel(connection: ChannelFuture) = { val channel = connection.getChannel openChannels.remove(channel) - channel.close + channel.close() } def attemptReconnect(): Boolean = { @@ -180,10 +178,9 @@ class ActiveRemoteClient private[akka] ( runSwitch switchOn { openChannels = new DefaultDisposableChannelGroup(classOf[RemoteClient].getName) - timer = new HashedWheelTimer bootstrap = new ClientBootstrap(new NioClientSocketChannelFactory(Executors.newCachedThreadPool, Executors.newCachedThreadPool)) - bootstrap.setPipelineFactory(new ActiveRemoteClientPipelineFactory(name, bootstrap, remoteAddress, timer, this)) + bootstrap.setPipelineFactory(new ActiveRemoteClientPipelineFactory(name, bootstrap, remoteAddress, this)) bootstrap.setOption("tcpNoDelay", true) bootstrap.setOption("keepAlive", true) @@ -219,8 +216,6 @@ class ActiveRemoteClient private[akka] ( log.debug("Shutting down remote client [{}]", name) notifyListeners(RemoteClientShutdown(remoteSupport, remoteAddress)) - timer.stop() - timer = null openChannels.close.awaitUninterruptibly openChannels = null bootstrap.releaseExternalResources() @@ -253,18 +248,17 @@ class ActiveRemoteClientPipelineFactory( name: String, bootstrap: ClientBootstrap, remoteAddress: RemoteAddress, - timer: HashedWheelTimer, client: ActiveRemoteClient) extends ChannelPipelineFactory { import client.remoteSupport.clientSettings._ def getPipeline: ChannelPipeline = { - val timeout = new ReadTimeoutHandler(timer, ReadTimeout.length, ReadTimeout.unit) + val timeout = new ReadTimeoutHandler(client.remoteSupport.timer, ReadTimeout.length, ReadTimeout.unit) val lenDec = new LengthFieldBasedFrameDecoder(MessageFrameSize, 0, 4, 0, 4) val lenPrep = new LengthFieldPrepender(4) val protobufDec = new ProtobufDecoder(AkkaRemoteProtocol.getDefaultInstance) val protobufEnc = new ProtobufEncoder - val remoteClient = new ActiveRemoteClientHandler(name, bootstrap, remoteAddress, timer, client) + val remoteClient = new ActiveRemoteClientHandler(name, bootstrap, remoteAddress, client.remoteSupport.timer, client) new StaticChannelPipeline(timeout, lenDec, protobufDec, lenPrep, protobufEnc, remoteClient) } @@ -345,7 +339,7 @@ class ActiveRemoteClientHandler( client.remoteSupport.shutdownClientConnection(remoteAddress) // spawn in another thread } case e: Exception ⇒ - event.getChannel.close //FIXME Is this the correct behavior? + event.getChannel.close() //FIXME Is this the correct behavior??? } } else client.notifyListeners(RemoteClientError(new Exception("Unknown cause"), client.remoteSupport, client.remoteAddress)) @@ -361,6 +355,10 @@ class NettyRemoteSupport(_system: ActorSystem, val remote: Remote) extends Remot val serverSettings = RemoteExtension(system).serverSettings val clientSettings = RemoteExtension(system).clientSettings + val timer: HashedWheelTimer = new HashedWheelTimer + + _system.registerOnTermination(timer.stop()) //Shut this guy down at the end + private val remoteClients = new HashMap[RemoteAddress, RemoteClient] private val clientsLock = new ReentrantReadWriteLock @@ -519,6 +517,10 @@ class NettyRemoteServer(val remoteSupport: NettyRemoteSupport, val loader: Optio try { val shutdownSignal = { val b = RemoteControlProtocol.newBuilder.setCommandType(CommandType.SHUTDOWN) + b.setOrigin(RemoteProtocol.AddressProtocol.newBuilder + .setHostname(address.host) + .setPort(address.port) + .build) if (SecureCookie.nonEmpty) b.setCookie(SecureCookie.get) b.build @@ -649,7 +651,7 @@ class RemoteServerHandler( val inbound = RemoteAddress("BORKED", origin.getHostname, origin.getPort) val client = new PassiveRemoteClient(event.getChannel, remoteSupport, inbound) remoteSupport.bindClient(inbound, client) - case CommandType.SHUTDOWN ⇒ //TODO FIXME Dispose passive connection here + case CommandType.SHUTDOWN ⇒ //FIXME Dispose passive connection here, ticket #1410 case _ ⇒ //Unknown command } case _ ⇒ //ignore @@ -660,7 +662,7 @@ class RemoteServerHandler( override def exceptionCaught(ctx: ChannelHandlerContext, event: ExceptionEvent) = { remoteSupport.notifyListeners(RemoteServerError(event.getCause, remoteSupport)) - event.getChannel.close + event.getChannel.close() } private def getClientAddress(c: Channel): Option[RemoteAddress] = @@ -680,7 +682,7 @@ class DefaultDisposableChannelGroup(name: String) extends DefaultChannelGroup(na if (open.get) { super.add(channel) } else { - channel.close + channel.close() false } } finally { @@ -691,7 +693,7 @@ class DefaultDisposableChannelGroup(name: String) extends DefaultChannelGroup(na override def close(): ChannelGroupFuture = { guard.writeLock().lock() try { - if (open.getAndSet(false)) super.close else throw new IllegalStateException("ChannelGroup already closed, cannot add new channel") + if (open.getAndSet(false)) super.close() else throw new IllegalStateException("ChannelGroup already closed, cannot add new channel") } finally { guard.writeLock().unlock() } diff --git a/akka-remote/src/multi-jvm/scala/akka/remote/AkkaRemoteSpec.scala b/akka-remote/src/multi-jvm/scala/akka/remote/AkkaRemoteSpec.scala index f41ea0e855..01cc597d49 100644 --- a/akka-remote/src/multi-jvm/scala/akka/remote/AkkaRemoteSpec.scala +++ b/akka-remote/src/multi-jvm/scala/akka/remote/AkkaRemoteSpec.scala @@ -6,8 +6,27 @@ package akka.remote import akka.testkit._ import akka.actor.ActorSystemImpl +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import com.typesafe.config.ConfigParseOptions +import com.typesafe.config.ConfigResolveOptions +import java.io.File -abstract class AkkaRemoteSpec extends AkkaSpec with MultiJvmSync { +object AkkaRemoteSpec { + private def configParseOptions = ConfigParseOptions.defaults.setAllowMissing(false) + + val testConf: Config = { + System.getProperty("akka.config") match { + case null ⇒ AkkaSpec.testConf + case location ⇒ + ConfigFactory.systemProperties + .withFallback(ConfigFactory.parseFileAnySyntax(new File(location), configParseOptions)) + .withFallback(ConfigFactory.defaultReference).resolve(ConfigResolveOptions.defaults) + } + } +} + +abstract class AkkaRemoteSpec extends AkkaSpec(AkkaRemoteSpec.testConf) with MultiJvmSync { /** * Helper function for accessing the underlying remoting. diff --git a/akka-remote/src/test/scala/akka/remote/NetworkFailureSpec.scala b/akka-remote/src/test/scala/akka/remote/NetworkFailureSpec.scala index 7f7072b427..56a27079ea 100644 --- a/akka-remote/src/test/scala/akka/remote/NetworkFailureSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/NetworkFailureSpec.scala @@ -20,7 +20,7 @@ trait NetworkFailureSpec { self: AkkaSpec ⇒ val BytesPerSecond = "60KByte/s" val DelayMillis = "350ms" - val PortRang = "1024-65535" + val PortRange = "1024-65535" def replyWithTcpResetFor(duration: Duration, dead: AtomicBoolean) = { Future { @@ -82,12 +82,12 @@ trait NetworkFailureSpec { self: AkkaSpec ⇒ def enableNetworkDrop() = { restoreIP() - assert(new ProcessBuilder("ipfw", "add", "1", "deny", "tcp", "from", "any", "to", "any", PortRang).start.waitFor == 0) + assert(new ProcessBuilder("ipfw", "add", "1", "deny", "tcp", "from", "any", "to", "any", PortRange).start.waitFor == 0) } def enableTcpReset() = { restoreIP() - assert(new ProcessBuilder("ipfw", "add", "1", "reset", "tcp", "from", "any", "to", "any", PortRang).start.waitFor == 0) + assert(new ProcessBuilder("ipfw", "add", "1", "reset", "tcp", "from", "any", "to", "any", PortRange).start.waitFor == 0) } def restoreIP() = { diff --git a/akka-remote/src/test/scala/akka/remote/RemoteConfigSpec.scala b/akka-remote/src/test/scala/akka/remote/RemoteConfigSpec.scala index f72904fc3f..d11d07ddd3 100644 --- a/akka-remote/src/test/scala/akka/remote/RemoteConfigSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/RemoteConfigSpec.scala @@ -3,23 +3,23 @@ package akka.remote import akka.testkit.AkkaSpec @org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) -class RemoteConfigSpec extends AkkaSpec { +class RemoteConfigSpec extends AkkaSpec("akka.cluster.nodename = node1") { - "ClusterSpec: A Deployer" must { - "be able to parse 'akka.actor.cluster._' config elements" in { + "RemoteExtension" must { + "be able to parse remote and cluster config elements" in { val config = RemoteExtension(system).config import config._ //akka.remote - getString("akka.remote.layer") must equal("akka.cluster.netty.NettyRemoteSupport") + getString("akka.remote.transport") must equal("akka.cluster.netty.NettyRemoteSupport") getString("akka.remote.secure-cookie") must equal("") getBoolean("akka.remote.use-passive-connections") must equal(true) // getMilliseconds("akka.remote.remote-daemon-ack-timeout") must equal(30 * 1000) //akka.remote.server getInt("akka.remote.server.port") must equal(2552) - getInt("akka.remote.server.message-frame-size") must equal(1048576) + getBytes("akka.remote.server.message-frame-size") must equal(1048576L) getMilliseconds("akka.remote.server.connection-timeout") must equal(120 * 1000) getBoolean("akka.remote.server.require-cookie") must equal(false) getBoolean("akka.remote.server.untrusted-mode") must equal(false) @@ -35,7 +35,7 @@ class RemoteConfigSpec extends AkkaSpec { // TODO cluster config will go into akka-cluster-reference.conf when we enable that module //akka.cluster getString("akka.cluster.name") must equal("default-cluster") - getString("akka.cluster.nodename") must equal("") + getString("akka.cluster.nodename") must equal("node1") getStringList("akka.cluster.seed-nodes") must equal(new java.util.ArrayList[String]) // getMilliseconds("akka.cluster.max-time-to-wait-until-connected") must equal(30 * 1000) diff --git a/akka-samples/akka-sample-fsm/README b/akka-samples/akka-sample-fsm/README new file mode 100644 index 0000000000..1391071f0b --- /dev/null +++ b/akka-samples/akka-sample-fsm/README @@ -0,0 +1,28 @@ +FSM +=== + +Requirements +------------ + +To build and run FSM you need [Simple Build Tool][sbt] (sbt). + +Running +------- + +First time, 'sbt update' to get dependencies, then to run Ants use 'sbt run'. +Here is an example. First type 'sbt' to start SBT interactively, the run 'update' and 'run': +> cd $AKKA_HOME + +> % sbt + +> > project akka-sample-fsm + +> > run + +> > Choose 1 or 2 depending on what sample you wish to run + +Notice +------ + +[akka]: http://akka.io +[sbt]: http://code.google.com/p/simple-build-tool/ diff --git a/akka-samples/akka-sample-fsm/src/main/scala/Buncher.scala b/akka-samples/akka-sample-fsm/src/main/scala/Buncher.scala index 0dcf33e401..d039609a98 100644 --- a/akka-samples/akka-sample-fsm/src/main/scala/Buncher.scala +++ b/akka-samples/akka-sample-fsm/src/main/scala/Buncher.scala @@ -1,3 +1,6 @@ +/** + * Copyright (C) 2009-2010 Typesafe Inc. . + */ package sample.fsm.buncher import akka.actor.ActorRefFactory @@ -6,15 +9,15 @@ import akka.util.Duration import akka.actor.{ FSM, Actor, ActorRef } /* - * generic typed object buncher. - * - * To instantiate it, use the factory method like so: - * Buncher(100, 500)(x : List[AnyRef] => x foreach println) - * which will yield a fully functional ActorRef. - * The type of messages allowed is strongly typed to match the - * supplied processing method; other messages are discarded (and - * possibly logged). - */ +* generic typed object buncher. +* +* To instantiate it, use the factory method like so: +* Buncher(100, 500)(x : List[AnyRef] => x foreach println) +* which will yield a fully functional ActorRef. +* The type of messages allowed is strongly typed to match the +* supplied processing method; other messages are discarded (and +* possibly logged). +*/ object GenericBuncher { trait State case object Idle extends State diff --git a/akka-samples/akka-sample-fsm/src/main/scala/DiningHakkersOnBecome.scala b/akka-samples/akka-sample-fsm/src/main/scala/DiningHakkersOnBecome.scala index 346324e3b2..a13e034cae 100644 --- a/akka-samples/akka-sample-fsm/src/main/scala/DiningHakkersOnBecome.scala +++ b/akka-samples/akka-sample-fsm/src/main/scala/DiningHakkersOnBecome.scala @@ -1,3 +1,6 @@ +/** + * Copyright (C) 2009-2010 Typesafe Inc. . + */ package sample.fsm.dining.become //Akka adaptation of @@ -7,8 +10,8 @@ import akka.actor.{ ActorRef, Actor, ActorSystem } import akka.util.duration._ /* - * First we define our messages, they basically speak for themselves - */ +* First we define our messages, they basically speak for themselves +*/ sealed trait DiningHakkerMessage case class Busy(chopstick: ActorRef) extends DiningHakkerMessage case class Put(hakker: ActorRef) extends DiningHakkerMessage @@ -18,9 +21,9 @@ object Eat extends DiningHakkerMessage object Think extends DiningHakkerMessage /* - * A Chopstick is an actor, it can be taken, and put back - */ -class Chopstick(name: String) extends Actor { +* A Chopstick is an actor, it can be taken, and put back +*/ +class Chopstick extends Actor { //When a Chopstick is taken by a hakker //It will refuse to be taken by other hakkers @@ -44,8 +47,8 @@ class Chopstick(name: String) extends Actor { } /* - * A hakker is an awesome dude or dudett who either thinks about hacking or has to eat ;-) - */ +* A hakker is an awesome dude or dudett who either thinks about hacking or has to eat ;-) +*/ class Hakker(name: String, left: ActorRef, right: ActorRef) extends Actor { //When a hakker is thinking it can become hungry @@ -75,7 +78,7 @@ class Hakker(name: String, left: ActorRef, right: ActorRef) extends Actor { //back to think about how he should obtain his chopsticks :-) def waiting_for(chopstickToWaitFor: ActorRef, otherChopstick: ActorRef): Receive = { case Taken(`chopstickToWaitFor`) ⇒ - println("%s has picked up %s and %s, and starts to eat", name, left.path.name, right.path.name) + println("%s has picked up %s and %s and starts to eat".format(name, left.path.name, right.path.name)) become(eating) system.scheduler.scheduleOnce(self, Think, 5 seconds) @@ -105,27 +108,33 @@ class Hakker(name: String, left: ActorRef, right: ActorRef) extends Actor { become(thinking) left ! Put(self) right ! Put(self) - println("%s puts down his chopsticks and starts to think", name) + println("%s puts down his chopsticks and starts to think".format(name)) system.scheduler.scheduleOnce(self, Eat, 5 seconds) } //All hakkers start in a non-eating state def receive = { case Think ⇒ - println("%s starts to think", name) + println("%s starts to think".format(name)) become(thinking) system.scheduler.scheduleOnce(self, Eat, 5 seconds) } } /* - * Alright, here's our test-harness - */ +* Alright, here's our test-harness +*/ object DiningHakkers { val system = ActorSystem() + + def main(args: Array[String]): Unit = { + run + } + def run { //Create 5 chopsticks - val chopsticks = for (i ← 1 to 5) yield system.actorOf(new Chopstick("Chopstick " + i)) + val chopsticks = for (i ← 1 to 5) yield system.actorOf[Chopstick]("Chopstick " + i) + //Create 5 awesome hakkers and assign them their left and right chopstick val hakkers = for { (name, i) ← List("Ghosh", "Bonér", "Klang", "Krasser", "Manie").zipWithIndex diff --git a/akka-samples/akka-sample-fsm/src/main/scala/DiningHakkersOnFsm.scala b/akka-samples/akka-sample-fsm/src/main/scala/DiningHakkersOnFsm.scala index a76ecbc619..4ae21e48cb 100644 --- a/akka-samples/akka-sample-fsm/src/main/scala/DiningHakkersOnFsm.scala +++ b/akka-samples/akka-sample-fsm/src/main/scala/DiningHakkersOnFsm.scala @@ -1,3 +1,6 @@ +/** + * Copyright (C) 2009-2010 Typesafe Inc. . + */ package sample.fsm.dining.fsm import akka.actor.{ ActorRef, Actor, FSM, ActorSystem } @@ -6,8 +9,8 @@ import akka.util.Duration import akka.util.duration._ /* - * Some messages for the chopstick - */ +* Some messages for the chopstick +*/ sealed trait ChopstickMessage object Take extends ChopstickMessage object Put extends ChopstickMessage @@ -27,9 +30,9 @@ case object Taken extends ChopstickState case class TakenBy(hakker: ActorRef) /* - * A chopstick is an actor, it can be taken, and put back - */ -class Chopstick(name: String) extends Actor with FSM[ChopstickState, TakenBy] { +* A chopstick is an actor, it can be taken, and put back +*/ +class Chopstick extends Actor with FSM[ChopstickState, TakenBy] { // A chopstick begins its existence as available and taken by no one startWith(Available, TakenBy(system.deadLetters)) @@ -77,8 +80,8 @@ case object Eating extends FSMHakkerState case class TakenChopsticks(left: Option[ActorRef], right: Option[ActorRef]) /* - * A fsm hakker is an awesome dude or dudette who either thinks about hacking or has to eat ;-) - */ +* A fsm hakker is an awesome dude or dudette who either thinks about hacking or has to eat ;-) +*/ class FSMHakker(name: String, left: ActorRef, right: ActorRef) extends Actor with FSM[FSMHakkerState, TakenChopsticks] { //All hakkers start waiting @@ -86,7 +89,7 @@ class FSMHakker(name: String, left: ActorRef, right: ActorRef) extends Actor wit when(Waiting) { case Event(Think, _) ⇒ - println("%s starts to think", name) + println("%s starts to think".format(name)) startThinking(5 seconds) } @@ -125,7 +128,7 @@ class FSMHakker(name: String, left: ActorRef, right: ActorRef) extends Actor wit } private def startEating(left: ActorRef, right: ActorRef): State = { - println("%s has picked up %s and %s, and starts to eat", name, left.path.name, right.path.name) + println("%s has picked up %s and %s and starts to eat".format(name, left.path.name, right.path.name)) goto(Eating) using TakenChopsticks(Some(left), Some(right)) forMax (5 seconds) } @@ -144,7 +147,7 @@ class FSMHakker(name: String, left: ActorRef, right: ActorRef) extends Actor wit // then he puts down his chopsticks and starts to think when(Eating) { case Event(StateTimeout, _) ⇒ - println("%s puts down his chopsticks and starts to think", name) + println("%s puts down his chopsticks and starts to think".format(name)) left ! Put right ! Put startThinking(5 seconds) @@ -159,15 +162,19 @@ class FSMHakker(name: String, left: ActorRef, right: ActorRef) extends Actor wit } /* - * Alright, here's our test-harness - */ +* Alright, here's our test-harness +*/ object DiningHakkersOnFsm { val system = ActorSystem() + def main(args: Array[String]): Unit = { + run + } + def run = { // Create 5 chopsticks - val chopsticks = for (i ← 1 to 5) yield system.actorOf(new Chopstick("Chopstick " + i)) + val chopsticks = for (i ← 1 to 5) yield system.actorOf[Chopstick]("Chopstick " + i) // Create 5 awesome fsm hakkers and assign them their left and right chopstick val hakkers = for { (name, i) ← List("Ghosh", "Bonér", "Klang", "Krasser", "Manie").zipWithIndex diff --git a/akka-samples/akka-sample-hello/README b/akka-samples/akka-sample-hello/README new file mode 100644 index 0000000000..81a6db8d3e --- /dev/null +++ b/akka-samples/akka-sample-hello/README @@ -0,0 +1,26 @@ +HELLO +===== + +Requirements +------------ + +To build and run FSM you need [Simple Build Tool][sbt] (sbt). + +Running +------- + +First time, 'sbt update' to get dependencies, then to run Ants use 'sbt run'. +Here is an example. First type 'sbt' to start SBT interactively, the run 'update' and 'run': +> cd $AKKA_HOME + +> % sbt + +> > project akka-sample-hello + +> > run + +Notice +------ + +[akka]: http://akka.io +[sbt]: http://code.google.com/p/simple-build-tool/ diff --git a/akka-samples/akka-sample-hello/src/main/scala/sample/hello/Main.scala b/akka-samples/akka-sample-hello/src/main/scala/sample/hello/Main.scala new file mode 100644 index 0000000000..5df2661800 --- /dev/null +++ b/akka-samples/akka-sample-hello/src/main/scala/sample/hello/Main.scala @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2009-2011 Typesafe Inc. + */ +package sample.hello + +import akka.actor.{ ActorSystem, Actor } + +case object Start + +object Main { + def main(args: Array[String]): Unit = { + val system = ActorSystem() + system.actorOf[HelloActor] ! Start + } +} + +class HelloActor extends Actor { + val worldActor = system.actorOf[WorldActor] + def receive = { + case Start ⇒ worldActor ! "Hello" + case s: String ⇒ + println("Received message: %s".format(s)) + system.stop() + } +} + +class WorldActor extends Actor { + def receive = { + case s: String ⇒ sender ! s.toUpperCase + " world!" + } +} + diff --git a/akka-stm/src/main/resources/akka-stm-reference.conf b/akka-stm/src/main/resources/reference.conf similarity index 100% rename from akka-stm/src/main/resources/akka-stm-reference.conf rename to akka-stm/src/main/resources/reference.conf diff --git a/akka-stm/src/test/java/akka/stm/example/RetryExample.java b/akka-stm/src/test/java/akka/stm/example/RetryExample.java index ad86126deb..f15850d232 100644 --- a/akka-stm/src/test/java/akka/stm/example/RetryExample.java +++ b/akka-stm/src/test/java/akka/stm/example/RetryExample.java @@ -1,5 +1,8 @@ package akka.stm.example; +import org.junit.AfterClass; +import org.junit.BeforeClass; + import akka.actor.ActorSystem; import akka.stm.*; import akka.actor.*; @@ -7,11 +10,8 @@ import akka.testkit.AkkaSpec; public class RetryExample { public static void main(String[] args) { - System.out.println(); - System.out.println("Retry example"); - System.out.println(); - ActorSystem application = ActorSystem.create("RetryExample", AkkaSpec.testConf()); + ActorSystem application = ActorSystem.create("RetryExample", AkkaSpec.testConf()); final Ref account1 = new Ref(100.0); final Ref account2 = new Ref(100.0); @@ -47,5 +47,7 @@ public class RetryExample { // Account 2: 600.0 transferer.stop(); + + application.stop(); } } diff --git a/akka-stm/src/test/java/akka/transactor/example/UntypedCoordinatedExample.java b/akka-stm/src/test/java/akka/transactor/example/UntypedCoordinatedExample.java index 344c98dfee..9baf0f1485 100644 --- a/akka-stm/src/test/java/akka/transactor/example/UntypedCoordinatedExample.java +++ b/akka-stm/src/test/java/akka/transactor/example/UntypedCoordinatedExample.java @@ -9,11 +9,8 @@ import akka.transactor.Coordinated; public class UntypedCoordinatedExample { public static void main(String[] args) throws InterruptedException { - System.out.println(); - System.out.println("Untyped transactor example"); - System.out.println(); - ActorSystem application = ActorSystem.create("UntypedCoordinatedExample", AkkaSpec.testConf()); + ActorSystem application = ActorSystem.create("UntypedCoordinatedExample", AkkaSpec.testConf()); ActorRef counter1 = application.actorOf(new Props().withCreator(UntypedCoordinatedCounter.class)); ActorRef counter2 = application.actorOf(new Props().withCreator(UntypedCoordinatedCounter.class)); @@ -45,5 +42,7 @@ public class UntypedCoordinatedExample { counter1.stop(); counter2.stop(); + + application.stop(); } } diff --git a/akka-stm/src/test/java/akka/transactor/example/UntypedTransactorExample.java b/akka-stm/src/test/java/akka/transactor/example/UntypedTransactorExample.java index 882d5b7b1f..55e28f872f 100644 --- a/akka-stm/src/test/java/akka/transactor/example/UntypedTransactorExample.java +++ b/akka-stm/src/test/java/akka/transactor/example/UntypedTransactorExample.java @@ -8,11 +8,8 @@ import akka.testkit.AkkaSpec; public class UntypedTransactorExample { public static void main(String[] args) throws InterruptedException { - System.out.println(); - System.out.println("Untyped transactor example"); - System.out.println(); - ActorSystem application = ActorSystem.create("UntypedTransactorExample", AkkaSpec.testConf()); + ActorSystem application = ActorSystem.create("UntypedTransactorExample", AkkaSpec.testConf()); ActorRef counter1 = application.actorOf(new Props().withCreator(UntypedCounter.class)); ActorRef counter2 = application.actorOf(new Props().withCreator(UntypedCounter.class)); @@ -44,5 +41,7 @@ public class UntypedTransactorExample { counter1.stop(); counter2.stop(); + + application.stop(); } } diff --git a/akka-stm/src/test/java/akka/transactor/test/UntypedCoordinatedIncrementTest.java b/akka-stm/src/test/java/akka/transactor/test/UntypedCoordinatedIncrementTest.java index 0d44d16496..a90e0a1952 100644 --- a/akka-stm/src/test/java/akka/transactor/test/UntypedCoordinatedIncrementTest.java +++ b/akka-stm/src/test/java/akka/transactor/test/UntypedCoordinatedIncrementTest.java @@ -3,6 +3,8 @@ package akka.transactor.test; import static org.junit.Assert.*; import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.Before; @@ -31,7 +33,20 @@ import scala.collection.JavaConverters; import scala.collection.Seq; public class UntypedCoordinatedIncrementTest { - ActorSystem application = ActorSystem.create("UntypedCoordinatedIncrementTest", AkkaSpec.testConf()); + ActorSystem application = ActorSystem.create("UntypedCoordinatedIncrementTest", AkkaSpec.testConf()); + + private static ActorSystem system; + + @BeforeClass + public static void beforeAll() { + system = ActorSystem.create("UntypedTransactorTest", AkkaSpec.testConf()); + } + + @AfterClass + public static void afterAll() { + system.stop(); + system = null; + } List counters; ActorRef failer; diff --git a/akka-stm/src/test/java/akka/transactor/test/UntypedTransactorTest.java b/akka-stm/src/test/java/akka/transactor/test/UntypedTransactorTest.java index 8d2a3e4db8..528a2a14f8 100644 --- a/akka-stm/src/test/java/akka/transactor/test/UntypedTransactorTest.java +++ b/akka-stm/src/test/java/akka/transactor/test/UntypedTransactorTest.java @@ -1,6 +1,9 @@ package akka.transactor.test; import static org.junit.Assert.*; + +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.Before; @@ -28,7 +31,19 @@ import scala.collection.Seq; import akka.testkit.AkkaSpec; public class UntypedTransactorTest { - ActorSystem application = ActorSystem.create("UntypedTransactorTest", AkkaSpec.testConf()); + + private static ActorSystem system; + + @BeforeClass + public static void beforeAll() { + system = ActorSystem.create("UntypedTransactorTest", AkkaSpec.testConf()); + } + + @AfterClass + public static void afterAll() { + system.stop(); + system = null; + } List counters; ActorRef failer; @@ -42,14 +57,14 @@ public class UntypedTransactorTest { counters = new ArrayList(); for (int i = 1; i <= numCounters; i++) { final String name = "counter" + i; - ActorRef counter = application.actorOf(new Props().withCreator(new UntypedActorFactory() { + ActorRef counter = system.actorOf(new Props().withCreator(new UntypedActorFactory() { public UntypedActor create() { return new UntypedCounter(name); } })); counters.add(counter); } - failer = application.actorOf(new Props().withCreator(UntypedFailer.class)); + failer = system.actorOf(new Props().withCreator(UntypedFailer.class)); } @Test @@ -80,7 +95,7 @@ public class UntypedTransactorTest { EventFilter expectedFailureFilter = (EventFilter) new ErrorFilter(ExpectedFailureException.class); EventFilter coordinatedFilter = (EventFilter) new ErrorFilter(CoordinatedTransactionException.class); Seq ignoreExceptions = seq(expectedFailureFilter, coordinatedFilter); - application.eventStream().publish(new TestEvent.Mute(ignoreExceptions)); + system.eventStream().publish(new TestEvent.Mute(ignoreExceptions)); CountDownLatch incrementLatch = new CountDownLatch(numCounters); List actors = new ArrayList(counters); actors.add(failer); diff --git a/akka-stm/src/test/scala/akka/stm/test/ConfigSpec.scala b/akka-stm/src/test/scala/akka/stm/test/ConfigSpec.scala index 19a4450cf3..3c455bf702 100644 --- a/akka-stm/src/test/scala/akka/stm/test/ConfigSpec.scala +++ b/akka-stm/src/test/scala/akka/stm/test/ConfigSpec.scala @@ -10,11 +10,10 @@ import org.scalatest.junit.JUnitRunner import org.scalatest.matchers.MustMatchers import akka.actor.ActorSystem import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigParseOptions import akka.testkit.AkkaSpec @RunWith(classOf[JUnitRunner]) -class ConfigSpec extends AkkaSpec(ConfigFactory.parseResource(classOf[ConfigSpec], "/akka-stm-reference.conf", ConfigParseOptions.defaults)) { +class ConfigSpec extends AkkaSpec(ConfigFactory.defaultReference) { "The default configuration file (i.e. akka-stm-reference.conf)" should { "contain all configuration properties for akka-stm that are used in code with their correct defaults" in { diff --git a/akka-testkit/src/main/resources/akka-testkit-reference.conf b/akka-testkit/src/main/resources/reference.conf similarity index 100% rename from akka-testkit/src/main/resources/akka-testkit-reference.conf rename to akka-testkit/src/main/resources/reference.conf diff --git a/akka-testkit/src/main/scala/akka/testkit/CallingThreadDispatcher.scala b/akka-testkit/src/main/scala/akka/testkit/CallingThreadDispatcher.scala index 382c25523d..2a3933c93b 100644 --- a/akka-testkit/src/main/scala/akka/testkit/CallingThreadDispatcher.scala +++ b/akka-testkit/src/main/scala/akka/testkit/CallingThreadDispatcher.scala @@ -15,7 +15,12 @@ import akka.dispatch._ import akka.actor.Scheduler import akka.event.EventStream import akka.util.Duration +import akka.util.duration._ import java.util.concurrent.TimeUnit +import akka.actor.ExtensionId +import akka.actor.ExtensionIdProvider +import akka.actor.ActorSystemImpl +import akka.actor.Extension /* * Locking rules: @@ -34,7 +39,12 @@ import java.util.concurrent.TimeUnit * within one of its methods taking a closure argument. */ -private[testkit] object CallingThreadDispatcher { +private[testkit] object CallingThreadDispatcherQueues extends ExtensionId[CallingThreadDispatcherQueues] with ExtensionIdProvider { + override def lookup = CallingThreadDispatcherQueues + override def createExtension(system: ActorSystemImpl): CallingThreadDispatcherQueues = new CallingThreadDispatcherQueues +} + +private[testkit] class CallingThreadDispatcherQueues extends Extension { // PRIVATE DATA @@ -127,7 +137,7 @@ class CallingThreadDispatcher( protected[akka] override def throughputDeadlineTime = Duration.Zero protected[akka] override def registerForExecution(mbox: Mailbox, hasMessageHint: Boolean, hasSystemMessageHint: Boolean): Boolean = false - protected[akka] override def shutdownTimeout = Duration(100L, TimeUnit.MILLISECONDS) + protected[akka] override def shutdownTimeout = 1 second override def suspend(actor: ActorCell) { getMailbox(actor) foreach (_.suspendSwitch.switchOn) @@ -139,7 +149,7 @@ class CallingThreadDispatcher( val queue = mbox.queue val wasActive = queue.isActive val switched = mbox.suspendSwitch.switchOff { - gatherFromAllOtherQueues(mbox, queue) + CallingThreadDispatcherQueues(actor.system).gatherFromAllOtherQueues(mbox, queue) } if (switched && !wasActive) { runQueue(mbox, queue) @@ -214,7 +224,6 @@ class CallingThreadDispatcher( } if (handle ne null) { try { - if (Mailbox.debug) println(mbox.actor.self + " processing message " + handle) mbox.actor.invoke(handle) true } catch { @@ -268,7 +277,7 @@ class CallingThreadMailbox(_receiver: ActorCell) extends Mailbox(_receiver) with private val q = new ThreadLocal[NestingQueue]() { override def initialValue = { val queue = new NestingQueue - CallingThreadDispatcher.registerQueue(CallingThreadMailbox.this, queue) + CallingThreadDispatcherQueues(actor.system).registerQueue(CallingThreadMailbox.this, queue) queue } } diff --git a/akka-testkit/src/main/scala/akka/testkit/TestActorRef.scala b/akka-testkit/src/main/scala/akka/testkit/TestActorRef.scala index ed4472e6f6..d974236824 100644 --- a/akka-testkit/src/main/scala/akka/testkit/TestActorRef.scala +++ b/akka-testkit/src/main/scala/akka/testkit/TestActorRef.scala @@ -42,6 +42,24 @@ class TestActorRef[T <: Actor]( */ def underlyingActor: T = underlyingActorInstance.asInstanceOf[T] + /** + * Registers this actor to be a death monitor of the provided ActorRef + * This means that this actor will get a Terminated()-message when the provided actor + * is permanently terminated. + * + * @return the same ActorRef that is provided to it, to allow for cleaner invocations + */ + def startsWatching(subject: ActorRef): ActorRef = underlying.startsWatching(subject) + + /** + * Deregisters this actor from being a death monitor of the provided ActorRef + * This means that this actor will not get a Terminated()-message when the provided actor + * is permanently terminated. + * + * @return the same ActorRef that is provided to it, to allow for cleaner invocations + */ + def stopsWatching(subject: ActorRef): ActorRef = underlying.stopsWatching(subject) + override def toString = "TestActor[" + path + "]" override def equals(other: Any) = other match { diff --git a/akka-testkit/src/main/scala/akka/testkit/TestKit.scala b/akka-testkit/src/main/scala/akka/testkit/TestKit.scala index dba8437ef6..c0476a74cc 100644 --- a/akka-testkit/src/main/scala/akka/testkit/TestKit.scala +++ b/akka-testkit/src/main/scala/akka/testkit/TestKit.scala @@ -92,7 +92,7 @@ class TestKit(_system: ActorSystem) { * ActorRef of the test actor. Access is provided to enable e.g. * registration as message target. */ - val testActor: ActorRef = { + lazy val testActor: ActorRef = { val impl = system.asInstanceOf[ActorSystemImpl] impl.systemActorOf(Props(new TestActor(queue)) .copy(dispatcher = new CallingThreadDispatcher(system.dispatcherFactory.prerequisites)), diff --git a/akka-testkit/src/main/scala/akka/testkit/TestKitExtension.scala b/akka-testkit/src/main/scala/akka/testkit/TestKitExtension.scala index 5af1bde50a..103241636a 100644 --- a/akka-testkit/src/main/scala/akka/testkit/TestKitExtension.scala +++ b/akka-testkit/src/main/scala/akka/testkit/TestKitExtension.scala @@ -4,22 +4,15 @@ package akka.testkit import com.typesafe.config.Config -import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigParseOptions -import com.typesafe.config.ConfigRoot import akka.util.Duration import java.util.concurrent.TimeUnit.MILLISECONDS import akka.actor.{ ExtensionId, ActorSystem, Extension, ActorSystemImpl } object TestKitExtension extends ExtensionId[TestKitSettings] { - def createExtension(system: ActorSystemImpl): TestKitSettings = new TestKitSettings(system.applicationConfig) + def createExtension(system: ActorSystemImpl): TestKitSettings = new TestKitSettings(system.settings.config) } -class TestKitSettings(cfg: Config) extends Extension { - private def referenceConfig: Config = - ConfigFactory.parseResource(classOf[ActorSystem], "/akka-testkit-reference.conf", - ConfigParseOptions.defaults.setAllowMissing(false)) - val config: ConfigRoot = ConfigFactory.emptyRoot("akka-testkit").withFallback(cfg).withFallback(referenceConfig).resolve() +class TestKitSettings(val config: Config) extends Extension { import config._ diff --git a/akka-testkit/src/test/scala/akka/testkit/AkkaSpec.scala b/akka-testkit/src/test/scala/akka/testkit/AkkaSpec.scala index fb7216d69d..8e94bf10b2 100644 --- a/akka-testkit/src/test/scala/akka/testkit/AkkaSpec.scala +++ b/akka-testkit/src/test/scala/akka/testkit/AkkaSpec.scala @@ -13,26 +13,25 @@ import akka.util.duration._ import akka.dispatch.FutureTimeoutException import com.typesafe.config.Config import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigParseOptions object TimingTest extends Tag("timing") object AkkaSpec { - val testConf = - ActorSystem.DefaultConfigurationLoader.defaultConfig.withFallback( - ConfigFactory.parseString(""" + val testConf = { + val cfg = ConfigFactory.parseString(""" akka { event-handlers = ["akka.testkit.TestEventListener"] loglevel = "WARNING" stdout-loglevel = "WARNING" actor { default-dispatcher { - core-pool-size = 4 - max-pool-size = 32 + core-pool-size-factor = 2 } } } - """, ConfigParseOptions.defaults)) + """) + ConfigFactory.load(cfg) + } def mapToConfig(map: Map[String, Any]): Config = { import scala.collection.JavaConverters._ @@ -51,7 +50,7 @@ abstract class AkkaSpec(_system: ActorSystem) def this(config: Config) = this(ActorSystem(AkkaSpec.getCallerName, config.withFallback(AkkaSpec.testConf))) - def this(s: String) = this(ConfigFactory.parseString(s, ConfigParseOptions.defaults)) + def this(s: String) = this(ConfigFactory.parseString(s)) def this(configMap: Map[String, _]) = this(AkkaSpec.mapToConfig(configMap)) @@ -66,7 +65,7 @@ abstract class AkkaSpec(_system: ActorSystem) final override def afterAll { system.stop() try system.asInstanceOf[ActorSystemImpl].terminationFuture.await(5 seconds) catch { - case _: FutureTimeoutException ⇒ system.log.warning("failed to stop within 5 seconds") + case _: FutureTimeoutException ⇒ system.log.warning("Failed to stop [{}] within 5 seconds", system.name) } atTermination() } @@ -92,12 +91,11 @@ abstract class AkkaSpec(_system: ActorSystem) class AkkaSpecSpec extends WordSpec with MustMatchers { "An AkkaSpec" must { "terminate all actors" in { - import ActorSystem.DefaultConfigurationLoader.defaultConfig import scala.collection.JavaConverters._ val conf = Map( "akka.actor.debug.lifecycle" -> true, "akka.actor.debug.event-stream" -> true, "akka.loglevel" -> "DEBUG", "akka.stdout-loglevel" -> "DEBUG") - val system = ActorSystem("test", ConfigFactory.parseMap(conf.asJava).withFallback(defaultConfig)) + val system = ActorSystem("test", ConfigFactory.parseMap(conf.asJava).withFallback(AkkaSpec.testConf)) val spec = new AkkaSpec(system) { val ref = Seq(testActor, system.actorOf(Props.empty, "name")) } diff --git a/akka-testkit/src/test/scala/akka/testkit/TestActorRefSpec.scala b/akka-testkit/src/test/scala/akka/testkit/TestActorRefSpec.scala index a6248ff63c..12096a61b1 100644 --- a/akka-testkit/src/test/scala/akka/testkit/TestActorRefSpec.scala +++ b/akka-testkit/src/test/scala/akka/testkit/TestActorRefSpec.scala @@ -155,7 +155,10 @@ class TestActorRefSpec extends AkkaSpec with BeforeAndAfterEach { "stop when sent a poison pill" in { EventFilter[ActorKilledException]() intercept { val a = TestActorRef(Props[WorkerActor]) - testActor startsWatching a + val forwarder = actorOf(Props(new Actor { + watch(a) + def receive = { case x ⇒ testActor forward x } + })) a.!(PoisonPill)(testActor) expectMsgPF(5 seconds) { case Terminated(`a`) ⇒ true diff --git a/akka-tutorials/akka-tutorial-first/pom.xml b/akka-tutorials/akka-tutorial-first/pom.xml index 8e25d972f3..1cec835a9c 100644 --- a/akka-tutorials/akka-tutorial-first/pom.xml +++ b/akka-tutorials/akka-tutorial-first/pom.xml @@ -13,7 +13,7 @@ - se.scalablesolutions.akka + com.typesafe.akka akka-actor 2.0-SNAPSHOT diff --git a/akka-tutorials/akka-tutorial-first/project/TutorialBuild.scala b/akka-tutorials/akka-tutorial-first/project/TutorialBuild.scala new file mode 100644 index 0000000000..5e5ef32493 --- /dev/null +++ b/akka-tutorials/akka-tutorial-first/project/TutorialBuild.scala @@ -0,0 +1,22 @@ +import sbt._ +import Keys._ + +object TutorialBuild extends Build { + lazy val buildSettings = Seq( + organization := "com.typesafe.akka", + version := "2.0-SNAPSHOT", + scalaVersion := "2.9.1" + ) + + lazy val akka = Project( + id = "akka-tutorial-first", + base = file("."), + settings = Defaults.defaultSettings ++ Seq( + libraryDependencies ++= Seq( + "com.typesafe.akka" % "akka-actor" % "2.0-SNAPSHOT", + "junit" % "junit" % "4.5" % "test", + "org.scalatest" % "scalatest_2.9.0" % "1.6.1" % "test", + "com.typesafe.akka" % "akka-testkit" % "2.0-SNAPSHOT" % "test") + ) + ) +} \ No newline at end of file diff --git a/akka-tutorials/akka-tutorial-first/project/build.properties b/akka-tutorials/akka-tutorial-first/project/build.properties index 4981c1c2c3..c6158f7be4 100644 --- a/akka-tutorials/akka-tutorial-first/project/build.properties +++ b/akka-tutorials/akka-tutorial-first/project/build.properties @@ -1,5 +1 @@ -project.organization=se.scalablesolutions.akka -project.name=akka-tutorial-first -project.version=2.0-SNAPSHOT -build.scala.versions=2.9.0 -sbt.version=0.7.7 +sbt.version=0.11.0 \ No newline at end of file diff --git a/akka-tutorials/akka-tutorial-first/project/build/Project.scala b/akka-tutorials/akka-tutorial-first/project/build/Project.scala deleted file mode 100644 index 975f2ce970..0000000000 --- a/akka-tutorials/akka-tutorial-first/project/build/Project.scala +++ /dev/null @@ -1,3 +0,0 @@ -import sbt._ - -class TutorialOneProject(info: ProjectInfo) extends DefaultProject(info) with AkkaProject diff --git a/akka-tutorials/akka-tutorial-first/project/plugins/Plugins.scala b/akka-tutorials/akka-tutorial-first/project/plugins/Plugins.scala deleted file mode 100644 index fb121fcd3e..0000000000 --- a/akka-tutorials/akka-tutorial-first/project/plugins/Plugins.scala +++ /dev/null @@ -1,6 +0,0 @@ -import sbt._ - -class Plugins(info: ProjectInfo) extends PluginDefinition(info) { - val akkaRepo = "Akka Repo" at "http://akka.io/repository" - val akkaPlugin = "se.scalablesolutions.akka" % "akka-sbt-plugin" % "2.0-SNAPSHOT" -} diff --git a/akka-tutorials/akka-tutorial-first/src/main/java/akka/tutorial/first/java/Pi.java b/akka-tutorials/akka-tutorial-first/src/main/java/akka/tutorial/first/java/Pi.java index ca8fe597f7..5a80699ade 100644 --- a/akka-tutorials/akka-tutorial-first/src/main/java/akka/tutorial/first/java/Pi.java +++ b/akka-tutorials/akka-tutorial-first/src/main/java/akka/tutorial/first/java/Pi.java @@ -1,182 +1,182 @@ -// * -// * Copyright (C) 2009-2011 Typesafe Inc. +/** + * Copyright (C) 2009-2011 Typesafe Inc. + */ +package akka.tutorial.first.java; -// package akka.tutorial.first.java; +import akka.actor.ActorRef; +import akka.actor.ActorSystem; +import akka.actor.UntypedActor; +import akka.actor.UntypedActorFactory; +import akka.japi.Creator; +import akka.routing.*; -// import static akka.actor.Actors.poisonPill; -// import static java.util.Arrays.asList; +import java.util.LinkedList; +import java.util.concurrent.CountDownLatch; -// import akka.actor.ActorRef; -// import akka.actor.Actors; -// import akka.actor.ActorSystem; -// import akka.actor.UntypedActor; -// import akka.actor.UntypedActorFactory; -// import akka.routing.RoutedProps; -// import akka.routing.RouterType; -// import akka.routing.LocalConnectionManager; -// import akka.routing.Routing; -// import akka.routing.Routing.Broadcast; -// import scala.collection.JavaConversions; +public class Pi { -// import java.util.LinkedList; -// import java.util.concurrent.CountDownLatch; + public static void main(String[] args) throws Exception { + Pi pi = new Pi(); + pi.calculate(4, 10000, 10000); + } -// public class Pi { + // ==================== + // ===== Messages ===== + // ==================== + static class Calculate { + } -// private static final ActorSystem system = new ActorSystem(); + static class Work { + private final int start; + private final int nrOfElements; -// public static void main(String[] args) throws Exception { -// Pi pi = new Pi(); -// pi.calculate(4, 10000, 10000); -// } + public Work(int start, int nrOfElements) { + this.start = start; + this.nrOfElements = nrOfElements; + } -// // ==================== -// // ===== Messages ===== -// // ==================== -// static class Calculate {} + public int getStart() { + return start; + } -// static class Work { -// private final int start; -// private final int nrOfElements; + public int getNrOfElements() { + return nrOfElements; + } + } -// public Work(int start, int nrOfElements) { -// this.start = start; -// this.nrOfElements = nrOfElements; -// } + static class Result { + private final double value; -// public int getStart() { return start; } -// public int getNrOfElements() { return nrOfElements; } -// } + public Result(double value) { + this.value = value; + } -// static class Result { -// private final double value; + public double getValue() { + return value; + } + } -// public Result(double value) { -// this.value = value; -// } + // ================== + // ===== Worker ===== + // ================== + public static class Worker extends UntypedActor { -// public double getValue() { return value; } -// } + // define the work + private double calculatePiFor(int start, int nrOfElements) { + double acc = 0.0; + for (int i = start * nrOfElements; i <= ((start + 1) * nrOfElements - 1); i++) { + acc += 4.0 * (1 - (i % 2) * 2) / (2 * i + 1); + } + return acc; + } -// // ================== -// // ===== Worker ===== -// // ================== -// static class Worker extends UntypedActor { + // message handler + public void onReceive(Object message) { + if (message instanceof Work) { + Work work = (Work) message; -// // define the work -// private double calculatePiFor(int start, int nrOfElements) { -// double acc = 0.0; -// for (int i = start * nrOfElements; i <= ((start + 1) * nrOfElements - 1); i++) { -// acc += 4.0 * (1 - (i % 2) * 2) / (2 * i + 1); -// } -// return acc; -// } + // perform the work + double result = calculatePiFor(work.getStart(), work.getNrOfElements()); -// // message handler -// public void onReceive(Object message) { -// if (message instanceof Work) { -// Work work = (Work) message; + // reply with the result + getSender().tell(new Result(result)); -// // perform the work -// double result = calculatePiFor(work.getStart(), work.getNrOfElements()); + } else throw new IllegalArgumentException("Unknown message [" + message + "]"); + } + } -// // reply with the result -// getSender().tell(new Result(result)); + // ================== + // ===== Master ===== + // ================== + public static class Master extends UntypedActor { + private final int nrOfMessages; + private final int nrOfElements; + private final CountDownLatch latch; -// } else throw new IllegalArgumentException("Unknown message [" + message + "]"); -// } -// } + private double pi; + private int nrOfResults; + private long start; -// // ================== -// // ===== Master ===== -// // ================== -// static class Master extends UntypedActor { -// private final int nrOfMessages; -// private final int nrOfElements; -// private final CountDownLatch latch; + private ActorRef router; -// private double pi; -// private int nrOfResults; -// private long start; + public Master(final int nrOfWorkers, int nrOfMessages, int nrOfElements, CountDownLatch latch) { + this.nrOfMessages = nrOfMessages; + this.nrOfElements = nrOfElements; + this.latch = latch; + Creator routerCreator = new Creator() { + public Router create() { + return new RoundRobinRouter(dispatcher(), new akka.actor.Timeout(-1)); + } + }; + LinkedList actors = new LinkedList() { + { + for (int i = 0; i < nrOfWorkers; i++) add(context().actorOf(Worker.class)); + } + }; + RoutedProps props = new RoutedProps(routerCreator, new LocalConnectionManager(actors), new akka.actor.Timeout(-1), true); + router = new RoutedActorRef(system(), props, getSelf(), "pi"); + } -// private ActorRef router; + // message handler + public void onReceive(Object message) { -// public Master(int nrOfWorkers, int nrOfMessages, int nrOfElements, CountDownLatch latch) { -// this.nrOfMessages = nrOfMessages; -// this.nrOfElements = nrOfElements; -// this.latch = latch; + if (message instanceof Calculate) { + // schedule work + for (int start = 0; start < nrOfMessages; start++) { + router.tell(new Work(start, nrOfElements), getSelf()); + } -// LinkedList workers = new LinkedList(); -// for (int i = 0; i < nrOfWorkers; i++) { -// ActorRef worker = system.actorOf(Worker.class); -// workers.add(worker); -// } + } else if (message instanceof Result) { -// router = system.actorOf(new RoutedProps().withRoundRobinRouter().withLocalConnections(workers), "pi"); -// } + // handle result from the worker + Result result = (Result) message; + pi += result.getValue(); + nrOfResults += 1; + if (nrOfResults == nrOfMessages) getSelf().stop(); -// // message handler -// public void onReceive(Object message) { + } else throw new IllegalArgumentException("Unknown message [" + message + "]"); + } -// if (message instanceof Calculate) { -// // schedule work -// for (int start = 0; start < nrOfMessages; start++) { -// router.tell(new Work(start, nrOfElements), getSelf()); -// } + @Override + public void preStart() { + start = System.currentTimeMillis(); + } -// // send a PoisonPill to all workers telling them to shut down themselves -// router.tell(new Broadcast(poisonPill())); + @Override + public void postStop() { + // tell the world that the calculation is complete + System.out.println(String.format( + "\n\tPi estimate: \t\t%s\n\tCalculation time: \t%s millis", + pi, (System.currentTimeMillis() - start))); + latch.countDown(); + } + } -// // send a PoisonPill to the router, telling him to shut himself down -// router.tell(poisonPill()); + // ================== + // ===== Run it ===== + // ================== + public void calculate(final int nrOfWorkers, final int nrOfElements, final int nrOfMessages) + throws Exception { + final ActorSystem system = ActorSystem.create(); -// } else if (message instanceof Result) { + // this latch is only plumbing to know when the calculation is completed + final CountDownLatch latch = new CountDownLatch(1); -// // handle result from the worker -// Result result = (Result) message; -// pi += result.getValue(); -// nrOfResults += 1; -// if (nrOfResults == nrOfMessages) getSelf().stop(); + // create the master + ActorRef master = system.actorOf(new UntypedActorFactory() { + public UntypedActor create() { + return new Master(nrOfWorkers, nrOfMessages, nrOfElements, latch); + } + }); -// } else throw new IllegalArgumentException("Unknown message [" + message + "]"); -// } + // start the calculation + master.tell(new Calculate()); -// @Override -// public void preStart() { -// start = System.currentTimeMillis(); -// } + // wait for master to shut down + latch.await(); -// @Override -// public void postStop() { -// // tell the world that the calculation is complete -// System.out.println(String.format( -// "\n\tPi estimate: \t\t%s\n\tCalculation time: \t%s millis", -// pi, (System.currentTimeMillis() - start))); -// latch.countDown(); -// } -// } - -// // ================== -// // ===== Run it ===== -// // ================== -// public void calculate(final int nrOfWorkers, final int nrOfElements, final int nrOfMessages) -// throws Exception { - -// // this latch is only plumbing to know when the calculation is completed -// final CountDownLatch latch = new CountDownLatch(1); - -// // create the master -// ActorRef master = system.actorOf(new UntypedActorFactory() { -// public UntypedActor create() { -// return new Master(nrOfWorkers, nrOfMessages, nrOfElements, latch); -// } -// }); - -// // start the calculation -// master.tell(new Calculate()); - -// // wait for master to shut down -// latch.await(); -// } -// } + // Shut down the system + system.stop(); + } +} diff --git a/akka-tutorials/akka-tutorial-first/src/main/scala/Pi.scala b/akka-tutorials/akka-tutorial-first/src/main/scala/Pi.scala index 836f766e12..3283a591f4 100644 --- a/akka-tutorials/akka-tutorial-first/src/main/scala/Pi.scala +++ b/akka-tutorials/akka-tutorial-first/src/main/scala/Pi.scala @@ -1,113 +1,109 @@ -// /** -// * Copyright (C) 2009-2011 Typesafe Inc. -// */ +/** + * Copyright (C) 2009-2011 Typesafe Inc. + */ +package akka.tutorial.first.scala -// package akka.tutorial.first.scala +import java.util.concurrent.CountDownLatch +import akka.routing.{ RoutedActorRef, LocalConnectionManager, RoundRobinRouter, RoutedProps } +import akka.actor.{ ActorSystemImpl, Actor, ActorSystem } -// import akka.actor.{ Actor, PoisonPill, ActorSystem } -// import Actor._ -// import java.util.concurrent.CountDownLatch -// import akka.routing.Routing.Broadcast -// import akka.routing.{ RoutedProps, Routing } +object Pi extends App { -// object Pi extends App { + // Initiate the calculation + calculate(nrOfWorkers = 4, nrOfElements = 10000, nrOfMessages = 10000) -// val system = ActorSystem() + // ==================== + // ===== Messages ===== + // ==================== + sealed trait PiMessage -// calculate(nrOfWorkers = 4, nrOfElements = 10000, nrOfMessages = 10000) + case object Calculate extends PiMessage -// // ==================== -// // ===== Messages ===== -// // ==================== -// sealed trait PiMessage + case class Work(start: Int, nrOfElements: Int) extends PiMessage -// case object Calculate extends PiMessage + case class Result(value: Double) extends PiMessage -// case class Work(start: Int, nrOfElements: Int) extends PiMessage + // ================== + // ===== Worker ===== + // ================== + class Worker extends Actor { -// case class Result(value: Double) extends PiMessage + // define the work + def calculatePiFor(start: Int, nrOfElements: Int): Double = { + var acc = 0.0 + for (i ← start until (start + nrOfElements)) + acc += 4.0 * (1 - (i % 2) * 2) / (2 * i + 1) + acc + } -// // ================== -// // ===== Worker ===== -// // ================== -// class Worker extends Actor { + def receive = { + case Work(start, nrOfElements) ⇒ sender ! Result(calculatePiFor(start, nrOfElements)) // perform the work + } + } -// // define the work -// def calculatePiFor(start: Int, nrOfElements: Int): Double = { -// var acc = 0.0 -// for (i ← start until (start + nrOfElements)) -// acc += 4.0 * (1 - (i % 2) * 2) / (2 * i + 1) -// acc -// } + // ================== + // ===== Master ===== + // ================== + class Master(nrOfWorkers: Int, nrOfMessages: Int, nrOfElements: Int, latch: CountDownLatch) + extends Actor { -// def receive = { -// case Work(start, nrOfElements) ⇒ sender ! Result(calculatePiFor(start, nrOfElements)) // perform the work -// } -// } + var pi: Double = _ + var nrOfResults: Int = _ + var start: Long = _ -// // ================== -// // ===== Master ===== -// // ================== -// class Master(nrOfWorkers: Int, nrOfMessages: Int, nrOfElements: Int, latch: CountDownLatch) -// extends Actor { + // create the workers + val workers = Vector.fill(nrOfWorkers)(system.actorOf[Worker]) -// var pi: Double = _ -// var nrOfResults: Int = _ -// var start: Long = _ + // wrap them with a load-balancing router + val props = RoutedProps(routerFactory = () ⇒ new RoundRobinRouter, connectionManager = new LocalConnectionManager(workers)) + val router = new RoutedActorRef(system, props, self, "pi") -// // create the workers -// val workers = Vector.fill(nrOfWorkers)(system.actorOf[Worker]) + // message handler + def receive = { + case Calculate ⇒ + // schedule work + for (i ← 0 until nrOfMessages) router ! Work(i * nrOfElements, nrOfElements) + case Result(value) ⇒ + // handle result from the worker + pi += value + nrOfResults += 1 -// // wrap them with a load-balancing router -// val router = system.actorOf(RoutedProps().withRoundRobinRouter.withLocalConnections(workers), "pi") + // Stop this actor and all its supervised children + if (nrOfResults == nrOfMessages) self.stop() + } -// // message handler -// def receive = { -// case Calculate ⇒ -// // schedule work -// for (i ← 0 until nrOfMessages) router ! Work(i * nrOfElements, nrOfElements) + override def preStart() { + start = System.currentTimeMillis + } -// // send a PoisonPill to all workers telling them to shut down themselves -// router ! Broadcast(PoisonPill) + override def postStop() { + // tell the world that the calculation is complete + println( + "\n\tPi estimate: \t\t%s\n\tCalculation time: \t%s millis" + .format(pi, (System.currentTimeMillis - start))) + latch.countDown() + } + } -// // send a PoisonPill to the router, telling him to shut himself down -// router ! PoisonPill + // ================== + // ===== Run it ===== + // ================== + def calculate(nrOfWorkers: Int, nrOfElements: Int, nrOfMessages: Int) { + val system = ActorSystem() -// case Result(value) ⇒ -// // handle result from the worker -// pi += value -// nrOfResults += 1 -// if (nrOfResults == nrOfMessages) self.stop() -// } + // this latch is only plumbing to know when the calculation is completed + val latch = new CountDownLatch(1) -// override def preStart() { -// start = System.currentTimeMillis -// } + // create the master + val master = system.actorOf(new Master(nrOfWorkers, nrOfMessages, nrOfElements, latch)) -// override def postStop() { -// // tell the world that the calculation is complete -// println( -// "\n\tPi estimate: \t\t%s\n\tCalculation time: \t%s millis" -// .format(pi, (System.currentTimeMillis - start))) -// latch.countDown() -// } -// } + // start the calculation + master ! Calculate -// // ================== -// // ===== Run it ===== -// // ================== -// def calculate(nrOfWorkers: Int, nrOfElements: Int, nrOfMessages: Int) { + // wait for master to shut down + latch.await() -// // this latch is only plumbing to know when the calculation is completed -// val latch = new CountDownLatch(1) - -// // create the master -// val master = system.actorOf(new Master(nrOfWorkers, nrOfMessages, nrOfElements, latch)) - -// // start the calculation -// master ! Calculate - -// // wait for master to shut down -// latch.await() -// } -// } + // Shut down the system + system.stop() + } +} diff --git a/akka-tutorials/akka-tutorial-first/src/test/scala/WorkerSpec.scala b/akka-tutorials/akka-tutorial-first/src/test/scala/WorkerSpec.scala new file mode 100644 index 0000000000..a752b3c783 --- /dev/null +++ b/akka-tutorials/akka-tutorial-first/src/test/scala/WorkerSpec.scala @@ -0,0 +1,31 @@ +/** + * Copyright (C) 2009-2011 Typesafe Inc. + */ +package akka.tutorial.first.scala + +import org.junit.runner.RunWith +import org.scalatest.matchers.MustMatchers +import org.scalatest.BeforeAndAfterAll +import org.scalatest.WordSpec +import akka.testkit.TestActorRef +import akka.tutorial.first.scala.Pi.Worker +import akka.actor.ActorSystem + +@org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) +class WorkerSpec extends WordSpec with MustMatchers with BeforeAndAfterAll { + + implicit val system = ActorSystem() + + override def afterAll { + system.stop() + } + + "Worker" must { + "calculate pi correctly" in { + val testActor = TestActorRef[Worker] + val actor = testActor.underlyingActor + actor.calculatePiFor(0, 0) must equal(0.0) + actor.calculatePiFor(1, 1) must be(-1.3333333333333333 plusOrMinus 0.0000000001) + } + } +} diff --git a/project/AkkaBuild.scala b/project/AkkaBuild.scala index 3454442322..cc698631e4 100644 --- a/project/AkkaBuild.scala +++ b/project/AkkaBuild.scala @@ -215,7 +215,7 @@ object AkkaBuild extends Build { id = "akka-samples", base = file("akka-samples"), settings = parentSettings, - aggregate = Seq(fsmSample) + aggregate = Seq(fsmSample, helloSample) ) lazy val fsmSample = Project( @@ -224,27 +224,36 @@ object AkkaBuild extends Build { dependencies = Seq(actor), settings = defaultSettings ) - + + lazy val helloSample = Project( + id = "akka-sample-hello", + base = file("akka-samples/akka-sample-hello"), + dependencies = Seq(actor), + settings = defaultSettings + ) + lazy val tutorials = Project( id = "akka-tutorials", base = file("akka-tutorials"), settings = parentSettings, - aggregate = Seq(firstTutorial, secondTutorial) + aggregate = Seq(firstTutorial) ) lazy val firstTutorial = Project( id = "akka-tutorial-first", base = file("akka-tutorials/akka-tutorial-first"), - dependencies = Seq(actor), - settings = defaultSettings + dependencies = Seq(actor, testkit), + settings = defaultSettings ++ Seq( + libraryDependencies ++= Dependencies.tutorials + ) ) - lazy val secondTutorial = Project( - id = "akka-tutorial-second", - base = file("akka-tutorials/akka-tutorial-second"), - dependencies = Seq(actor), - settings = defaultSettings - ) + // lazy val secondTutorial = Project( + // id = "akka-tutorial-second", + // base = file("akka-tutorials/akka-tutorial-second"), + // dependencies = Seq(actor), + // settings = defaultSettings + // ) lazy val docs = Project( id = "akka-docs", @@ -406,6 +415,8 @@ object Dependencies { // val sampleCamel = Seq(camelCore, camelSpring, commonsCodec, Runtime.camelJms, Runtime.activemq, Runtime.springJms, // Test.junit, Test.scalatest, Test.logback) + val tutorials = Seq(Test.scalatest, Test.junit) + val docs = Seq(Test.scalatest, Test.junit) }