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 aa460e8d20..d0ef5104cd 100644 --- a/akka-actor-tests/src/test/java/akka/actor/JavaAPI.java +++ b/akka-actor-tests/src/test/java/akka/actor/JavaAPI.java @@ -3,29 +3,30 @@ package akka.actor; import akka.actor.ActorSystem; import akka.japi.Creator; import org.junit.Test; -import akka.actor.Actors; -import akka.remote.RemoteSupport; import static org.junit.Assert.*; public class JavaAPI { private ActorSystem system = ActorSystem.create(); - @Test void mustBeAbleToCreateActorRefFromClass() { - ActorRef ref = system.actorOf(JavaAPITestActor.class); - assertNotNull(ref); + @Test + void mustBeAbleToCreateActorRefFromClass() { + ActorRef ref = system.actorOf(JavaAPITestActor.class); + assertNotNull(ref); } - @Test void mustBeAbleToCreateActorRefFromFactory() { - ActorRef ref = system.actorOf(new Props().withCreator(new Creator() { - public Actor create() { - return new JavaAPITestActor(); - } - })); - assertNotNull(ref); + @Test + void mustBeAbleToCreateActorRefFromFactory() { + ActorRef ref = system.actorOf(new Props().withCreator(new Creator() { + public Actor create() { + return new JavaAPITestActor(); + } + })); + assertNotNull(ref); } - @Test void mustAcceptSingleArgTell() { + @Test + 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/dispatch/JavaFutureTests.java b/akka-actor-tests/src/test/java/akka/dispatch/JavaFutureTests.java index 27367ff162..12dbe736d6 100644 --- a/akka-actor-tests/src/test/java/akka/dispatch/JavaFutureTests.java +++ b/akka-actor-tests/src/test/java/akka/dispatch/JavaFutureTests.java @@ -14,248 +14,267 @@ import akka.japi.Function; import akka.japi.Function2; import akka.japi.Procedure; import akka.japi.Option; -import scala.Some; -import scala.Right; 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 final ActorSystem system = ActorSystem.create(); + private final Timeout t = system.settings().ActorTimeout(); + private final FutureFactory ff = new FutureFactory(system.dispatcher(), t); - @Test public void mustBeAbleToMapAFuture() { - Future f1 = ff.future(new Callable() { - public String call() { - return "Hello"; - } - }); + @Test + public void mustBeAbleToMapAFuture() { + Future f1 = ff.future(new Callable() { + public String call() { + return "Hello"; + } + }); - Future f2 = f1.map(new Function() { - public String apply(String s) { - return s + " World"; - } - }, t); + Future f2 = f1.map(new Function() { + public String apply(String s) { + return s + " World"; + } + }, t); - assertEquals("Hello World", f2.get()); - } + assertEquals("Hello World", f2.get()); + } - @Test public void mustBeAbleToExecuteAnOnResultCallback() throws Throwable { - final CountDownLatch latch = new CountDownLatch(1); - Promise cf = new akka.dispatch.DefaultPromise(1000, TimeUnit.MILLISECONDS, system.dispatcherFactory().defaultGlobalDispatcher()); - Future f = cf; - f.onResult(new Procedure() { - public void apply(String result) { - if(result.equals("foo")) - latch.countDown(); - } - }); + @Test + public void mustBeAbleToExecuteAnOnResultCallback() throws Throwable { + final CountDownLatch latch = new CountDownLatch(1); + Promise cf = new akka.dispatch.DefaultPromise(1000, TimeUnit.MILLISECONDS, system + .dispatcherFactory().defaultGlobalDispatcher()); + Future f = cf; + f.onResult(new Procedure() { + public void apply(String result) { + if (result.equals("foo")) + latch.countDown(); + } + }); - cf.completeWithResult("foo"); - assertTrue(latch.await(5000, TimeUnit.MILLISECONDS)); - assertEquals(f.get(), "foo"); - } + cf.completeWithResult("foo"); + assertTrue(latch.await(5000, TimeUnit.MILLISECONDS)); + assertEquals(f.get(), "foo"); + } - @Test public void mustBeAbleToExecuteAnOnExceptionCallback() throws Throwable { - final CountDownLatch latch = new CountDownLatch(1); - Promise cf = new akka.dispatch.DefaultPromise(1000, TimeUnit.MILLISECONDS, system.dispatcherFactory().defaultGlobalDispatcher()); - Future f = cf; - f.onException(new Procedure() { - public void apply(Throwable t) { - if(t instanceof NullPointerException) - latch.countDown(); - } - }); + @Test + public void mustBeAbleToExecuteAnOnExceptionCallback() throws Throwable { + final CountDownLatch latch = new CountDownLatch(1); + Promise cf = new akka.dispatch.DefaultPromise(1000, TimeUnit.MILLISECONDS, system + .dispatcherFactory().defaultGlobalDispatcher()); + Future f = cf; + f.onException(new Procedure() { + public void apply(Throwable t) { + if (t instanceof NullPointerException) + latch.countDown(); + } + }); - Throwable exception = new NullPointerException(); - cf.completeWithException(exception); - assertTrue(latch.await(5000, TimeUnit.MILLISECONDS)); - assertEquals(f.exception().get(), exception); - } + Throwable exception = new NullPointerException(); + cf.completeWithException(exception); + assertTrue(latch.await(5000, TimeUnit.MILLISECONDS)); + assertEquals(f.exception().get(), exception); + } - @Test public void mustBeAbleToExecuteAnOnTimeoutCallback() throws Throwable { - final CountDownLatch latch = new CountDownLatch(1); - Promise cf = new akka.dispatch.DefaultPromise(1000, TimeUnit.MILLISECONDS, system.dispatcherFactory().defaultGlobalDispatcher()); - Future f = cf; - f.onTimeout(new Procedure>() { - public void apply(Future future) { - latch.countDown(); - } - }); + @Test + public void mustBeAbleToExecuteAnOnTimeoutCallback() throws Throwable { + final CountDownLatch latch = new CountDownLatch(1); + Promise cf = new akka.dispatch.DefaultPromise(1000, TimeUnit.MILLISECONDS, system + .dispatcherFactory().defaultGlobalDispatcher()); + Future f = cf; + f.onTimeout(new Procedure>() { + public void apply(Future future) { + latch.countDown(); + } + }); - assertTrue(latch.await(5000, TimeUnit.MILLISECONDS)); - assertTrue(f.value().isEmpty()); - } + assertTrue(latch.await(5000, TimeUnit.MILLISECONDS)); + assertTrue(f.value().isEmpty()); + } - @Test public void mustBeAbleToExecuteAnOnCompleteCallback() throws Throwable { - final CountDownLatch latch = new CountDownLatch(1); - Promise cf = new akka.dispatch.DefaultPromise(1000, TimeUnit.MILLISECONDS, system.dispatcherFactory().defaultGlobalDispatcher()); - Future f = cf; - f.onComplete(new Procedure>() { - public void apply(akka.dispatch.Future future) { - latch.countDown(); - } - }); + @Test + public void mustBeAbleToExecuteAnOnCompleteCallback() throws Throwable { + final CountDownLatch latch = new CountDownLatch(1); + Promise cf = new akka.dispatch.DefaultPromise(1000, TimeUnit.MILLISECONDS, system + .dispatcherFactory().defaultGlobalDispatcher()); + Future f = cf; + f.onComplete(new Procedure>() { + public void apply(akka.dispatch.Future future) { + latch.countDown(); + } + }); - cf.completeWithResult("foo"); - assertTrue(latch.await(5000, TimeUnit.MILLISECONDS)); - assertEquals(f.get(), "foo"); - } + cf.completeWithResult("foo"); + assertTrue(latch.await(5000, TimeUnit.MILLISECONDS)); + assertEquals(f.get(), "foo"); + } - @Test public void mustBeAbleToForeachAFuture() throws Throwable { - final CountDownLatch latch = new CountDownLatch(1); - Promise cf = new akka.dispatch.DefaultPromise(1000, TimeUnit.MILLISECONDS, system.dispatcherFactory().defaultGlobalDispatcher()); - Future f = cf; - f.foreach(new Procedure() { - public void apply(String future) { - latch.countDown(); - } - }); + @Test + public void mustBeAbleToForeachAFuture() throws Throwable { + final CountDownLatch latch = new CountDownLatch(1); + Promise cf = new akka.dispatch.DefaultPromise(1000, TimeUnit.MILLISECONDS, system + .dispatcherFactory().defaultGlobalDispatcher()); + Future f = cf; + f.foreach(new Procedure() { + public void apply(String future) { + latch.countDown(); + } + }); - cf.completeWithResult("foo"); - assertTrue(latch.await(5000, TimeUnit.MILLISECONDS)); - assertEquals(f.get(), "foo"); - } + cf.completeWithResult("foo"); + assertTrue(latch.await(5000, TimeUnit.MILLISECONDS)); + assertEquals(f.get(), "foo"); + } - @Test public void mustBeAbleToFlatMapAFuture() throws Throwable { - final CountDownLatch latch = new CountDownLatch(1); - Promise cf = new akka.dispatch.DefaultPromise(1000, TimeUnit.MILLISECONDS, system.dispatcherFactory().defaultGlobalDispatcher()); - cf.completeWithResult("1000"); - Future f = cf; - Future r = f.flatMap(new Function>() { - public Future apply(String r) { - latch.countDown(); - Promise cf = new akka.dispatch.DefaultPromise(1000, TimeUnit.MILLISECONDS, system.dispatcherFactory().defaultGlobalDispatcher()); - cf.completeWithResult(Integer.parseInt(r)); - return cf; - } - }, t); + @Test + public void mustBeAbleToFlatMapAFuture() throws Throwable { + final CountDownLatch latch = new CountDownLatch(1); + Promise cf = new akka.dispatch.DefaultPromise(1000, TimeUnit.MILLISECONDS, system + .dispatcherFactory().defaultGlobalDispatcher()); + cf.completeWithResult("1000"); + Future f = cf; + Future r = f.flatMap(new Function>() { + public Future apply(String r) { + latch.countDown(); + Promise cf = new akka.dispatch.DefaultPromise(1000, TimeUnit.MILLISECONDS, system + .dispatcherFactory().defaultGlobalDispatcher()); + cf.completeWithResult(Integer.parseInt(r)); + return cf; + } + }, t); - assertEquals(f.get(), "1000"); - assertEquals(r.get().intValue(), 1000); - assertTrue(latch.await(5000, TimeUnit.MILLISECONDS)); - } + assertEquals(f.get(), "1000"); + assertEquals(r.get().intValue(), 1000); + assertTrue(latch.await(5000, TimeUnit.MILLISECONDS)); + } - @Test public void mustBeAbleToFilterAFuture() throws Throwable { - final CountDownLatch latch = new CountDownLatch(1); - Promise cf = new akka.dispatch.DefaultPromise(1000, TimeUnit.MILLISECONDS, system.dispatcherFactory().defaultGlobalDispatcher()); - Future f = cf; - Future r = f.filter(new Function() { - public Boolean apply(String r) { - latch.countDown(); - return r.equals("foo"); - } - }, t); + @Test + public void mustBeAbleToFilterAFuture() throws Throwable { + final CountDownLatch latch = new CountDownLatch(1); + Promise cf = new akka.dispatch.DefaultPromise(1000, TimeUnit.MILLISECONDS, system + .dispatcherFactory().defaultGlobalDispatcher()); + Future f = cf; + Future r = f.filter(new Function() { + public Boolean apply(String r) { + latch.countDown(); + return r.equals("foo"); + } + }, t); - cf.completeWithResult("foo"); - assertTrue(latch.await(5000, TimeUnit.MILLISECONDS)); - assertEquals(f.get(), "foo"); - assertEquals(r.get(), "foo"); - } + cf.completeWithResult("foo"); + assertTrue(latch.await(5000, TimeUnit.MILLISECONDS)); + assertEquals(f.get(), "foo"); + assertEquals(r.get(), "foo"); + } - // TODO: Improve this test, perhaps with an Actor - @Test public void mustSequenceAFutureList() { - LinkedList> listFutures = new LinkedList>(); - LinkedList listExpected = new LinkedList(); + // TODO: Improve this test, perhaps with an Actor + @Test + public void mustSequenceAFutureList() { + LinkedList> listFutures = new LinkedList>(); + LinkedList listExpected = new LinkedList(); - for (int i = 0; i < 10; i++) { - listExpected.add("test"); - listFutures.add(ff.future(new Callable() { - public String call() { - return "test"; - } - })); + for (int i = 0; i < 10; i++) { + listExpected.add("test"); + listFutures.add(ff.future(new Callable() { + public String call() { + return "test"; } - - Future> futureList = ff.sequence(listFutures, t); - - assertEquals(futureList.get(), listExpected); + })); } - // TODO: Improve this test, perhaps with an Actor - @Test public void foldForJavaApiMustWork() { - LinkedList> listFutures = new LinkedList>(); - StringBuilder expected = new StringBuilder(); + Future> futureList = ff.sequence(listFutures, t); - for (int i = 0; i < 10; i++) { - expected.append("test"); - listFutures.add(ff.future(new Callable() { - public String call() { - return "test"; - } - })); + assertEquals(futureList.get(), listExpected); + } + + // TODO: Improve this test, perhaps with an Actor + @Test + public void foldForJavaApiMustWork() { + LinkedList> listFutures = new LinkedList>(); + StringBuilder expected = new StringBuilder(); + + for (int i = 0; i < 10; i++) { + expected.append("test"); + listFutures.add(ff.future(new Callable() { + public String call() { + return "test"; } + })); + } - Future result = ff.fold("", 15000,listFutures, new Function2() { - public String apply(String r, String t) { - return r + t; + Future result = ff.fold("", 15000, listFutures, new Function2() { + public String apply(String r, String t) { + return r + t; + } + }); + + assertEquals(result.get(), expected.toString()); + } + + @Test + public void reduceForJavaApiMustWork() { + LinkedList> listFutures = new LinkedList>(); + StringBuilder expected = new StringBuilder(); + + for (int i = 0; i < 10; i++) { + expected.append("test"); + listFutures.add(ff.future(new Callable() { + public String call() { + return "test"; + } + })); + } + + Future result = ff.reduce(listFutures, 15000, new Function2() { + public String apply(String r, String t) { + return r + t; + } + }); + + assertEquals(result.get(), expected.toString()); + } + + @Test + public void traverseForJavaApiMustWork() { + LinkedList listStrings = new LinkedList(); + LinkedList expectedStrings = new LinkedList(); + + for (int i = 0; i < 10; i++) { + expectedStrings.add("TEST"); + listStrings.add("test"); + } + + Future> result = ff.traverse(listStrings, t, new Function>() { + public Future apply(final String r) { + return ff.future(new Callable() { + public String call() { + return r.toUpperCase(); } }); + } + }); - assertEquals(result.get(), expected.toString()); - } + assertEquals(result.get(), expectedStrings); + } - @Test public void reduceForJavaApiMustWork() { - LinkedList> listFutures = new LinkedList>(); - StringBuilder expected = new StringBuilder(); - - for (int i = 0; i < 10; i++) { - expected.append("test"); - listFutures.add(ff.future(new Callable() { - public String call() { - return "test"; - } - })); + @Test + public void findForJavaApiMustWork() { + LinkedList> listFutures = new LinkedList>(); + for (int i = 0; i < 10; i++) { + final Integer fi = i; + listFutures.add(ff.future(new Callable() { + public Integer call() { + return fi; } - - Future result = ff.reduce(listFutures, 15000, new Function2() { - public String apply(String r, String t) { - return r + t; - } - }); - - assertEquals(result.get(), expected.toString()); + })); } + final Integer expect = 5; + Future> f = ff.find(listFutures, new Function() { + public Boolean apply(Integer i) { + return i == 5; + } + }, t); - @Test public void traverseForJavaApiMustWork() { - LinkedList listStrings = new LinkedList(); - LinkedList expectedStrings = new LinkedList(); - - for (int i = 0; i < 10; i++) { - expectedStrings.add("TEST"); - listStrings.add("test"); - } - - Future> result = ff.traverse(listStrings, t, new Function>() { - public Future apply(final String r) { - return ff.future(new Callable() { - public String call() { - return r.toUpperCase(); - } - }); - } - }); - - assertEquals(result.get(), expectedStrings); - } - - @Test public void findForJavaApiMustWork() { - LinkedList> listFutures = new LinkedList>(); - for (int i = 0; i < 10; i++) { - final Integer fi = i; - listFutures.add(ff.future(new Callable() { - public Integer call() { - return fi; - } - })); - } - final Integer expect = 5; - Future> f = ff.find(listFutures, new Function() { - public Boolean apply(Integer i) { - return i == 5; - } - }, t); - - final Integer got = f.get().get(); - assertEquals(expect, got); - } + final Integer got = f.get().get(); + assertEquals(expect, got); + } } diff --git a/akka-actor-tests/src/test/java/akka/japi/JavaAPITestBase.java b/akka-actor-tests/src/test/java/akka/japi/JavaAPITestBase.java index e3a160f776..c0361530da 100644 --- a/akka-actor-tests/src/test/java/akka/japi/JavaAPITestBase.java +++ b/akka-actor-tests/src/test/java/akka/japi/JavaAPITestBase.java @@ -6,37 +6,44 @@ import static org.junit.Assert.*; public class JavaAPITestBase { - @Test public void shouldCreateSomeString() { - Option o = Option.some("abc"); - assertFalse(o.isEmpty()); - assertTrue(o.isDefined()); - assertEquals("abc", o.get()); - } + @Test + public void shouldCreateSomeString() { + Option o = Option.some("abc"); + assertFalse(o.isEmpty()); + assertTrue(o.isDefined()); + assertEquals("abc", o.get()); + } - @Test public void shouldCreateNone() { - Option o1 = Option.none(); - assertTrue(o1.isEmpty()); - assertFalse(o1.isDefined()); + @Test + public void shouldCreateNone() { + Option o1 = Option.none(); + assertTrue(o1.isEmpty()); + assertFalse(o1.isDefined()); - Option o2 = Option.none(); - assertTrue(o2.isEmpty()); - assertFalse(o2.isDefined()); - } + Option o2 = Option.none(); + assertTrue(o2.isEmpty()); + assertFalse(o2.isDefined()); + } - @Test public void shouldEnterForLoop() { - for(String s : Option.some("abc")) { - return; - } - fail("for-loop not entered"); + @Test + public void shouldEnterForLoop() { + for (@SuppressWarnings("unused") + String s : Option.some("abc")) { + return; } + fail("for-loop not entered"); + } - @Test public void shouldNotEnterForLoop() { - for(Object o : Option.none()) { - fail("for-loop entered"); - } + @Test + public void shouldNotEnterForLoop() { + for (@SuppressWarnings("unused") + Object o : Option.none()) { + fail("for-loop entered"); } + } - @Test public void shouldBeSingleton() { - assertSame(Option.none(), Option.none()); - } + @Test + public void shouldBeSingleton() { + assertSame(Option.none(), Option.none()); + } } diff --git a/akka-actor-tests/src/test/scala/akka/actor/ClusterSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/ClusterSpec.scala index a96fc68435..4d5cae60b4 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/ClusterSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/ClusterSpec.scala @@ -13,42 +13,42 @@ class ClusterSpec extends AkkaSpec { import config._ //akka.cluster - getString("akka.cluster.name") must equal(Some("test-cluster")) - getString("akka.cluster.zookeeper-server-addresses") must equal(Some("localhost:2181")) - getInt("akka.remote.server.port") must equal(Some(2552)) - getInt("akka.cluster.max-time-to-wait-until-connected") must equal(Some(30)) - getInt("akka.cluster.session-timeout") must equal(Some(60)) - getInt("akka.cluster.connection-timeout") must equal(Some(60)) - getInt("akka.remote.remote-daemon-ack-timeout") must equal(Some(30)) - getBool("akka.cluster.include-ref-node-in-replica-set") must equal(Some(true)) - getString("akka.remote.layer") must equal(Some("akka.cluster.netty.NettyRemoteSupport")) - getString("akka.remote.secure-cookie") must equal(Some("")) - getBool("akka.remote.use-passive-connections") must equal(Some(true)) - getString("akka.cluster.log-directory") must equal(Some("_akka_cluster")) + getString("akka.cluster.name") must equal("test-cluster") + getString("akka.cluster.zookeeper-server-addresses") must equal("localhost:2181") + getInt("akka.remote.server.port") must equal(2552) + getInt("akka.cluster.max-time-to-wait-until-connected") must equal(30) + getInt("akka.cluster.session-timeout") must equal(60) + getInt("akka.cluster.connection-timeout") must equal(60) + getInt("akka.remote.remote-daemon-ack-timeout") must equal(30) + getBoolean("akka.cluster.include-ref-node-in-replica-set") must equal(true) + getString("akka.remote.layer") must equal("akka.cluster.netty.NettyRemoteSupport") + getString("akka.remote.secure-cookie") must equal("") + getBoolean("akka.remote.use-passive-connections") must equal(true) + getString("akka.cluster.log-directory") must equal("_akka_cluster") //akka.cluster.replication - getString("akka.cluster.replication.digest-type") must equal(Some("MAC")) - getString("akka.cluster.replication.password") must equal(Some("secret")) - getInt("akka.cluster.replication.ensemble-size") must equal(Some(3)) - getInt("akka.cluster.replication.quorum-size") must equal(Some(2)) - getInt("akka.cluster.replication.snapshot-frequency") must equal(Some(1000)) - getInt("akka.cluster.replication.timeout") must equal(Some(30)) + getString("akka.cluster.replication.digest-type") must equal("MAC") + getString("akka.cluster.replication.password") must equal("secret") + getInt("akka.cluster.replication.ensemble-size") must equal(3) + getInt("akka.cluster.replication.quorum-size") must equal(2) + getInt("akka.cluster.replication.snapshot-frequency") must equal(1000) + getInt("akka.cluster.replication.timeout") must equal(30) //akka.remote.server - getInt("akka.remote.server.port") must equal(Some(2552)) - getInt("akka.remote.server.message-frame-size") must equal(Some(1048576)) - getInt("akka.remote.server.connection-timeout") must equal(Some(120)) - getBool("akka.remote.server.require-cookie") must equal(Some(false)) - getBool("akka.remote.server.untrusted-mode") must equal(Some(false)) - getInt("akka.remote.server.backlog") must equal(Some(4096)) + getInt("akka.remote.server.port") must equal(2552) + getInt("akka.remote.server.message-frame-size") must equal(1048576) + getInt("akka.remote.server.connection-timeout") must equal(120) + getBoolean("akka.remote.server.require-cookie") must equal(false) + getBoolean("akka.remote.server.untrusted-mode") must equal(false) + getInt("akka.remote.server.backlog") must equal(4096) //akka.remote.client - getBool("akka.remote.client.buffering.retry-message-send-on-failure") must equal(Some(false)) - getInt("akka.remote.client.buffering.capacity") must equal(Some(-1)) - getInt("akka.remote.client.reconnect-delay") must equal(Some(5)) - getInt("akka.remote.client.read-timeout") must equal(Some(3600)) - getInt("akka.remote.client.reap-futures-delay") must equal(Some(5)) - getInt("akka.remote.client.reconnection-time-window") must equal(Some(600)) + getBoolean("akka.remote.client.buffering.retry-message-send-on-failure") must equal(false) + getInt("akka.remote.client.buffering.capacity") must equal(-1) + getInt("akka.remote.client.reconnect-delay") must equal(5) + getInt("akka.remote.client.read-timeout") must equal(3600) + getInt("akka.remote.client.reap-futures-delay") must equal(5) + getInt("akka.remote.client.reconnection-time-window") must equal(600) } } } 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 bbcc84eb46..b7878662f2 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/DeployerSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/DeployerSpec.scala @@ -8,12 +8,31 @@ import akka.testkit.AkkaSpec import akka.util.duration._ import DeploymentConfig._ import akka.remote.RemoteAddress +import java.io.StringReader +import com.typesafe.config.ConfigFactory +import com.typesafe.config.ConfigParseOptions + +object DeployerSpec { + val deployerConf = ConfigFactory.parseReader(new StringReader(""" + akka.actor.deployment { + /app/service-ping { + nr-of-instances = 3 + remote { + nodes = ["wallace:2552", "gromit:2552"] + } + } + } + """), ConfigParseOptions.defaults) + +} @org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) -class DeployerSpec extends AkkaSpec { +class DeployerSpec extends AkkaSpec(DeployerSpec.deployerConf) { "A Deployer" must { + "be able to parse 'akka.actor.deployment._' config elements" in { + val p = system.settings.config.getString("akka.actor.provider") val deployment = system.asInstanceOf[ActorSystemImpl].provider.deployer.lookupInConfig("/app/service-ping") deployment must be('defined) 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 2409d80734..d9e888731c 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/FSMActorSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/FSMActorSpec.scala @@ -5,15 +5,13 @@ package akka.actor import org.scalatest.{ BeforeAndAfterAll, BeforeAndAfterEach } - import akka.testkit._ import TestEvent.Mute import FSM._ import akka.util.Duration import akka.util.duration._ import akka.event._ -import akka.actor.ActorSystem.defaultConfig -import akka.config.Configuration +import com.typesafe.config.ConfigFactory object FSMActorSpec { @@ -103,7 +101,7 @@ object FSMActorSpec { } @org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) -class FSMActorSpec extends AkkaSpec(Configuration("akka.actor.debug.fsm" -> true)) with ImplicitSender { +class FSMActorSpec extends AkkaSpec(Map("akka.actor.debug.fsm" -> true)) with ImplicitSender { import FSMActorSpec._ "An FSM Actor" must { @@ -193,9 +191,10 @@ class FSMActorSpec extends AkkaSpec(Configuration("akka.actor.debug.fsm" -> true } "log events and transitions if asked to do so" in { - new TestKit(ActorSystem("fsm event", ActorSystem.defaultConfig ++ - Configuration("akka.loglevel" -> "DEBUG", - "akka.actor.debug.fsm" -> true))) { + import scala.collection.JavaConverters._ + val config = ConfigFactory.parseMap(Map("akka.loglevel" -> "DEBUG", + "akka.actor.debug.fsm" -> true).asJava) + new TestKit(ActorSystem("fsm event", config)) { EventFilter.debug() intercept { val fsm = TestActorRef(new Actor with LoggingFSM[Int, Null] { startWith(1, null) diff --git a/akka-actor-tests/src/test/scala/akka/actor/LoggingReceiveSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/LoggingReceiveSpec.scala index 858c0dcdea..fc9e5812e7 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/LoggingReceiveSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/LoggingReceiveSpec.scala @@ -7,10 +7,12 @@ import org.scalatest.{ BeforeAndAfterAll, BeforeAndAfterEach } import akka.util.duration._ import akka.testkit._ import org.scalatest.WordSpec -import akka.actor.ActorSystem.defaultConfig -import akka.config.Configuration 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 object LoggingReceiveSpec { class TestLogActor extends Actor { @@ -22,11 +24,10 @@ object LoggingReceiveSpec { class LoggingReceiveSpec extends WordSpec with BeforeAndAfterEach with BeforeAndAfterAll { import LoggingReceiveSpec._ - - val config = defaultConfig ++ Configuration("akka.event-handler-level" -> "DEBUG") - val appLogging = ActorSystem("logging", config ++ Configuration("akka.actor.debug.receive" -> true)) - val appAuto = ActorSystem("autoreceive", config ++ Configuration("akka.actor.debug.autoreceive" -> true)) - val appLifecycle = ActorSystem("lifecycle", config ++ Configuration("akka.actor.debug.lifecycle" -> true)) + val config = ConfigFactory.parseMap(Map("akka.logLevel" -> "DEBUG").asJava) + val appLogging = ActorSystem("logging", ConfigFactory.parseMap(Map("akka.actor.debug.receive" -> true).asJava).withFallback(config)) + val appAuto = ActorSystem("autoreceive", ConfigFactory.parseMap(Map("akka.actor.debug.autoreceive" -> true).asJava).withFallback(config)) + val appLifecycle = ActorSystem("lifecycle", ConfigFactory.parseMap(Map("akka.actor.debug.lifecycle" -> true).asJava).withFallback(config)) val filter = TestEvent.Mute(EventFilter.custom { case _: Logging.Debug ⇒ true @@ -70,7 +71,8 @@ class LoggingReceiveSpec extends WordSpec with BeforeAndAfterEach with BeforeAnd system.eventStream.subscribe(testActor, classOf[Logging.Error]) val actor = TestActorRef(new Actor { def receive = loggable(this) { - case _ ⇒ sender ! "x" + case x ⇒ + sender ! "x" } }) actor ! "buh" 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 d73560c996..852ce08ee4 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 @@ -7,7 +7,10 @@ import java.util.concurrent.{ CountDownLatch, TimeUnit } import scala.reflect.{ Manifest } import akka.dispatch._ import akka.testkit.AkkaSpec -import akka.config.Configuration +import scala.collection.JavaConverters._ +import java.io.StringReader +import com.typesafe.config.ConfigFactory +import com.typesafe.config.ConfigParseOptions @org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) class DispatchersSpec extends AkkaSpec { @@ -31,19 +34,46 @@ class DispatchersSpec extends AkkaSpec { def validTypes = typesAndValidators.keys.toList + val defaultDispatcherConfig = settings.config.getConfig("akka.actor.default-dispatcher") + + val dispatcherConf = ConfigFactory.parseReader( + new StringReader(""" + myapp { + mydispatcher { + throughput = 17 + } + } + """), ConfigParseOptions.defaults) + lazy val allDispatchers: Map[String, Option[MessageDispatcher]] = { - validTypes.map(t ⇒ (t, from(Configuration.fromMap(Map(tipe -> t))))).toMap + validTypes.map(t ⇒ (t, from(ConfigFactory.parseMap(Map(tipe -> t).asJava).withFallback(defaultDispatcherConfig)))).toMap } "Dispatchers" must { - "yield None if type is missing" in { - assert(from(Configuration.fromMap(Map())) === None) + "use default dispatcher if type is missing" in { + val dispatcher = from(ConfigFactory.parseMap(Map().asJava).withFallback(defaultDispatcherConfig)) + dispatcher.map(_.name) must be(Some("DefaultDispatcher")) + } + + "use defined properties" in { + val dispatcher = from(ConfigFactory.parseMap(Map("throughput" -> 17).asJava).withFallback(defaultDispatcherConfig)) + dispatcher.map(_.throughput) must be(Some(17)) + } + + "use defined properties when fromConfig" in { + val dispatcher = fromConfig("myapp.mydispatcher", cfg = dispatcherConf) + dispatcher.throughput must be(17) + } + + "use specific name when fromConfig" in { + val dispatcher = fromConfig("myapp.mydispatcher", cfg = dispatcherConf) + dispatcher.name must be("mydispatcher") } "throw IllegalArgumentException if type does not exist" in { intercept[IllegalArgumentException] { - from(Configuration.fromMap(Map(tipe -> "typedoesntexist"))) + from(ConfigFactory.parseMap(Map(tipe -> "typedoesntexist").asJava).withFallback(defaultDispatcherConfig)) } } @@ -54,9 +84,6 @@ class DispatchersSpec extends AkkaSpec { assert(typesAndValidators.forall(tuple ⇒ tuple._2(allDispatchers(tuple._1).get))) } - "default to default while loading the default" in { - assert(from(Configuration.fromMap(Map())).getOrElse(defaultGlobalDispatcher) == defaultGlobalDispatcher) - } } } 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 a7d69d6dec..aa82316aec 100644 --- a/akka-actor-tests/src/test/scala/akka/config/ConfigSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/config/ConfigSpec.scala @@ -6,39 +6,49 @@ package akka.config import akka.testkit.AkkaSpec import akka.actor.ActorSystem +import java.io.File +import com.typesafe.config.ConfigFactory +import com.typesafe.config.ConfigParseOptions +import scala.collection.JavaConverters._ +import java.util.concurrent.TimeUnit +import akka.util.Duration @org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) -class ConfigSpec extends AkkaSpec(ActorSystem("ConfigSpec", Configuration.fromFile("config/akka-reference.conf"))) { +class ConfigSpec extends AkkaSpec(ConfigFactory.parseResource(classOf[ConfigSpec], "/akka-actor-reference.conf", ConfigParseOptions.defaults)) { - "The default configuration file (i.e. akka-reference.conf)" must { + "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 { - val config = system.settings.config + val settings = system.settings + val config = settings.config import config._ - getList("akka.boot") must equal(Nil) - getString("akka.time-unit") must equal(Some("seconds")) - getString("akka.version") must equal(Some("2.0-SNAPSHOT")) + getList("akka.boot").asScala.toSeq must equal(Nil) + getString("akka.time-unit") must equal("seconds") + settings.DefaultTimeUnit must equal(TimeUnit.SECONDS) + getString("akka.version") must equal("2.0-SNAPSHOT") + settings.ConfigVersion must equal("2.0-SNAPSHOT") - getString("akka.actor.default-dispatcher.type") must equal(Some("Dispatcher")) - getInt("akka.actor.default-dispatcher.keep-alive-time") must equal(Some(60)) - getDouble("akka.actor.default-dispatcher.core-pool-size-factor") must equal(Some(8.0)) - getDouble("akka.actor.default-dispatcher.max-pool-size-factor") must equal(Some(8.0)) - getInt("akka.actor.default-dispatcher.task-queue-size") must equal(Some(-1)) - getString("akka.actor.default-dispatcher.task-queue-type") must equal(Some("linked")) - getBool("akka.actor.default-dispatcher.allow-core-timeout") must equal(Some(true)) - getInt("akka.actor.default-dispatcher.mailbox-capacity") must equal(Some(-1)) - getInt("akka.actor.default-dispatcher.mailbox-push-timeout-time") must equal(Some(10)) - getLong("akka.actor.dispatcher-shutdown-timeout") must equal(Some(1)) - getInt("akka.actor.default-dispatcher.throughput") must equal(Some(5)) - getInt("akka.actor.default-dispatcher.throughput-deadline-time") must equal(Some(-1)) - getBool("akka.actor.serialize-messages") must equal(Some(false)) - getInt("akka.actor.timeout") must equal(Some(5)) - getInt("akka.actor.throughput") must equal(Some(5)) - getInt("akka.actor.throughput-deadline-time") must equal(Some(-1)) + getString("akka.actor.default-dispatcher.type") must equal("Dispatcher") + getInt("akka.actor.default-dispatcher.keep-alive-time") must equal(60) + getDouble("akka.actor.default-dispatcher.core-pool-size-factor") must equal(8.0) + getDouble("akka.actor.default-dispatcher.max-pool-size-factor") must equal(8.0) + getInt("akka.actor.default-dispatcher.task-queue-size") must equal(-1) + getString("akka.actor.default-dispatcher.task-queue-type") must equal("linked") + getBoolean("akka.actor.default-dispatcher.allow-core-timeout") must equal(true) + getInt("akka.actor.default-dispatcher.mailbox-capacity") must equal(-1) + getInt("akka.actor.default-dispatcher.mailbox-push-timeout-time") must equal(10) + getLong("akka.actor.dispatcher-shutdown-timeout") must equal(1) + settings.DispatcherDefaultShutdown must equal(Duration(1, TimeUnit.SECONDS)) + getInt("akka.actor.default-dispatcher.throughput") must equal(5) + settings.DispatcherThroughput must equal(5) + getInt("akka.actor.default-dispatcher.throughput-deadline-time") must equal(-1) + settings.DispatcherThroughputDeadlineTime must equal(Duration(-1, TimeUnit.SECONDS)) + getBoolean("akka.actor.serialize-messages") must equal(false) + settings.SerializeAllMessages must equal(false) - getString("akka.remote.layer") must equal(Some("akka.cluster.netty.NettyRemoteSupport")) - getInt("akka.remote.server.port") must equal(Some(2552)) + getString("akka.remote.layer") must equal("akka.cluster.netty.NettyRemoteSupport") + getInt("akka.remote.server.port") must equal(2552) } } } 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 d5371af0b9..205c6d0e71 100644 --- a/akka-actor-tests/src/test/scala/akka/event/EventStreamSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/event/EventStreamSpec.scala @@ -4,11 +4,24 @@ package akka.event import akka.testkit.AkkaSpec -import akka.config.Configuration 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 +import java.io.StringReader object EventStreamSpec { + + val config = ConfigFactory.parseReader(new StringReader(""" + akka { + stdout-loglevel = WARNING + loglevel = INFO + event-handlers = ["akka.event.EventStreamSpec$MyLog", "%s"] + } + """.format(Logging.StandardOutLoggerName)), ConfigParseOptions.defaults) + case class M(i: Int) case class SetTarget(ref: ActorRef) @@ -29,10 +42,8 @@ object EventStreamSpec { class C extends B1 } -class EventStreamSpec extends AkkaSpec(Configuration( - "akka.stdout-loglevel" -> "WARNING", - "akka.loglevel" -> "INFO", - "akka.event-handlers" -> Seq("akka.event.EventStreamSpec$MyLog", Logging.StandardOutLoggerName))) { +@org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) +class EventStreamSpec extends AkkaSpec(EventStreamSpec.config) { import EventStreamSpec._ 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 83e8b182bf..a69aefb013 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 @@ -3,8 +3,6 @@ package akka.performance.workbench import java.lang.management.ManagementFactory import java.text.SimpleDateFormat import java.util.Date -import scala.collection.JavaConversions.asScalaBuffer -import scala.collection.JavaConversions.enumerationAsScalaIterator import akka.actor.ActorSystem import akka.event.Logging import scala.collection.immutable.TreeMap @@ -224,8 +222,8 @@ class Report( sb.append("Akka version: ").append(system.settings.ConfigVersion) sb.append("\n") sb.append("Akka config:") - for (key ← system.settings.config.keys) { - sb.append("\n ").append(key).append("=").append(system.settings.config(key)) + for ((key, value) ← system.settings.config.toObject) { + sb.append("\n ").append(key).append("=").append(value) } sb.toString diff --git a/akka-actor-tests/src/test/scala/akka/routing/ActorPoolSpec.scala b/akka-actor-tests/src/test/scala/akka/routing/ActorPoolSpec.scala index afcf7d6d39..39d8f2ff9e 100644 --- a/akka-actor-tests/src/test/scala/akka/routing/ActorPoolSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/routing/ActorPoolSpec.scala @@ -239,7 +239,7 @@ class ActorPoolSpec extends AkkaSpec { (pool ? ActorPool.Stat).as[ActorPool.Stats].get.size must be(2) - // send a bunch over the theshold and observe an increment + // send a bunch over the threshold and observe an increment loops = 15 loop(500) diff --git a/akka-actor/src/main/java/akka/actor/Actors.java b/akka-actor/src/main/java/akka/actor/Actors.java index d0cb8ccb21..dd8763aad0 100644 --- a/akka-actor/src/main/java/akka/actor/Actors.java +++ b/akka-actor/src/main/java/akka/actor/Actors.java @@ -4,51 +4,48 @@ package akka.actor; -import akka.japi.Creator; -import akka.remote.RemoteSupport; - -import com.eaio.uuid.UUID; - /** - * JAVA API for - * - creating actors, - * - creating remote actors, - * - locating actors + * JAVA API for - creating actors, - creating remote actors, - locating actors */ public class Actors { - /** - * The message that is sent when an Actor gets a receive timeout. - *
-     *  if( message == receiveTimeout() ) {
-     *    //Timed out
-     *  }
-     * 
- * @return the single instance of ReceiveTimeout - */ - public final static ReceiveTimeout$ receiveTimeout() { - return ReceiveTimeout$.MODULE$; - } + /** + * The message that is sent when an Actor gets a receive timeout. + * + *
+   * if (message == receiveTimeout()) {
+   *   // Timed out
+   * }
+   * 
+ * + * @return the single instance of ReceiveTimeout + */ + public final static ReceiveTimeout$ receiveTimeout() { + return ReceiveTimeout$.MODULE$; + } - /** - * The message that when sent to an Actor kills it by throwing an exception. - *
-     *  actor.tell(kill());
-     * 
- * @return the single instance of Kill - */ - public final static Kill$ kill() { - return Kill$.MODULE$; - } + /** + * The message that when sent to an Actor kills it by throwing an exception. + * + *
+   * actor.tell(kill());
+   * 
+ * + * @return the single instance of Kill + */ + public final static Kill$ kill() { + return Kill$.MODULE$; + } - - /** - * The message that when sent to an Actor shuts it down by calling 'stop'. - *
-     *  actor.tell(poisonPill());
-     * 
- * @return the single instance of PoisonPill - */ - public final static PoisonPill$ poisonPill() { - return PoisonPill$.MODULE$; - } + /** + * The message that when sent to an Actor shuts it down by calling 'stop'. + * + *
+   * actor.tell(poisonPill());
+   * 
+ * + * @return the single instance of PoisonPill + */ + public final static PoisonPill$ poisonPill() { + return PoisonPill$.MODULE$; + } } diff --git a/akka-actor/src/main/java/com/typesafe/config/Config.java b/akka-actor/src/main/java/com/typesafe/config/Config.java new file mode 100644 index 0000000000..94abf9d9f5 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/Config.java @@ -0,0 +1,265 @@ +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. + * + * 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. + * + * 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. + * + * 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. + * + * Config is an immutable object and thus safe to use from multiple threads. + * + * 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. + * + * 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. + * + */ +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). + * + * @return + */ + ConfigObject toObject(); + + ConfigOrigin origin(); + + @Override + Config withFallback(ConfigMergeable other); + + @Override + Config withFallbacks(ConfigMergeable... others); + + @Override + ConfigObject toValue(); + + /** + * 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. + * + * 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. + * + * @param path + * the path expression + * @return true if a non-null value is present at the path + * @throws ConfigException.BadPath + * if the path expression is invalid + */ + boolean hasPath(String path); + + boolean isEmpty(); + + /** + * + * @param path + * @return + * @throws ConfigException.Missing + * if value is absent or null + * @throws ConfigException.WrongType + * if value is not convertible to boolean + */ + boolean getBoolean(String path); + + /** + * @param path + * @return + * @throws ConfigException.Missing + * if value is absent or null + * @throws ConfigException.WrongType + * if value is not convertible to a number + */ + Number getNumber(String path); + + /** + * @param path + * @return + * @throws ConfigException.Missing + * if value is absent or null + * @throws ConfigException.WrongType + * if value is not convertible to an int + */ + int getInt(String path); + + /** + * @param path + * @return + * @throws ConfigException.Missing + * if value is absent or null + * @throws ConfigException.WrongType + * if value is not convertible to a long + */ + long getLong(String path); + + /** + * @param path + * @return + * @throws ConfigException.Missing + * if value is absent or null + * @throws ConfigException.WrongType + * if value is not convertible to a double + */ + double getDouble(String path); + + /** + * @param path + * @return + * @throws ConfigException.Missing + * if value is absent or null + * @throws ConfigException.WrongType + * if value is not convertible to a string + */ + String getString(String path); + + /** + * @param path + * @return + * @throws ConfigException.Missing + * if value is absent or null + * @throws ConfigException.WrongType + * if value is not convertible to an object + */ + ConfigObject getObject(String path); + + /** + * @param path + * @return + * @throws ConfigException.Missing + * if value is absent or null + * @throws ConfigException.WrongType + * if value is not convertible to a Config + */ + Config getConfig(String path); + + /** + * Gets the value at the path as an unwrapped Java boxed value (Boolean, + * Integer, Long, etc.) + * + * @throws ConfigException.Missing + * if value is absent or null + */ + Object getAnyRef(String path); + + /** + * 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. + * + * @param path + * @return + * @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. + * + * @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 + */ + Long getMemorySizeInBytes(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" + * + * @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 number of milliseconds + */ + Long getMilliseconds(String path); + + /** + * 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. + * + * @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 number of nanoseconds + */ + 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. + * + * @param path + * the path to the list value. + * @return the ConfigList at the path + * @throws ConfigException.Missing + * if value is absent or null + * @throws ConfigException.WrongType + * if value is not convertible to a ConfigList + */ + ConfigList getList(String path); + + List getBooleanList(String path); + + List getNumberList(String path); + + List getIntList(String path); + + List getLongList(String path); + + List getDoubleList(String path); + + List getStringList(String path); + + List getObjectList(String path); + + List getConfigList(String path); + + List getAnyRefList(String path); + + List getMemorySizeInBytesList(String path); + + List getMillisecondsList(String path); + + List getNanosecondsList(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 new file mode 100644 index 0000000000..894f6bc13a --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigException.java @@ -0,0 +1,230 @@ +package com.typesafe.config; + +/** + * All exceptions thrown by the library are subclasses of ConfigException. + */ +public class ConfigException extends RuntimeException { + private static final long serialVersionUID = 1L; + + protected ConfigException(ConfigOrigin origin, String message, + Throwable cause) { + super(origin.description() + ": " + message, cause); + } + + protected ConfigException(ConfigOrigin origin, String message) { + this(origin.description() + ": " + message, null); + } + + protected ConfigException(String message, Throwable cause) { + super(message, cause); + } + + protected ConfigException(String message) { + this(message, null); + } + + /** + * Exception indicating that the type of a value does not match the type you + * requested. + * + */ + public static class WrongType extends ConfigException { + private static final long serialVersionUID = 1L; + + public WrongType(ConfigOrigin origin, String path, String expected, + String actual, + Throwable cause) { + super(origin, path + " has type " + actual + " rather than " + + expected, + cause); + } + + public WrongType(ConfigOrigin origin, String path, String expected, + String actual) { + this(origin, path, expected, actual, null); + } + + WrongType(ConfigOrigin origin, String message, Throwable cause) { + super(origin, message, cause); + } + + WrongType(ConfigOrigin origin, String message) { + this(origin, message, null); + } + } + + /** + * Exception indicates that the setting was never set to anything, not even + * null. + */ + public static class Missing extends ConfigException { + private static final long serialVersionUID = 1L; + + public Missing(String path, Throwable cause) { + super("No configuration setting found for key '" + path + "'", + cause); + } + + public Missing(String path) { + this(path, null); + } + + protected Missing(ConfigOrigin origin, String message, Throwable cause) { + super(origin, message, cause); + } + + protected Missing(ConfigOrigin origin, String message) { + this(origin, message, null); + } + } + + /** + * Exception indicates that the setting was treated as missing because it + * was set to null. + */ + public static class Null extends Missing { + private static final long serialVersionUID = 1L; + + private static String makeMessage(String path, String expected) { + if (expected != null) { + return "Configuration key '" + path + + "' is set to null but expected " + expected; + } else { + return "Configuration key '" + path + "' is null"; + } + } + + public Null(ConfigOrigin origin, String path, String expected, + Throwable cause) { + super(origin, makeMessage(path, expected), cause); + } + + public Null(ConfigOrigin origin, String path, String expected) { + this(origin, path, expected, null); + } + } + + /** + * Exception indicating that a value was messed up, for example you may have + * asked for a duration and the value can't be sensibly parsed as a + * duration. + * + */ + public static class BadValue extends ConfigException { + private static final long serialVersionUID = 1L; + + public BadValue(ConfigOrigin origin, String path, String message, + Throwable cause) { + super(origin, "Invalid value at '" + path + "': " + message, cause); + } + + public BadValue(ConfigOrigin origin, String path, String message) { + this(origin, path, message, null); + } + + public BadValue(String path, String message, Throwable cause) { + super("Invalid value at '" + path + "': " + message, cause); + } + + public BadValue(String path, String message) { + this(path, message, null); + } + } + + public static class BadPath extends ConfigException { + private static final long serialVersionUID = 1L; + + public BadPath(ConfigOrigin origin, String path, String message, + Throwable cause) { + super(origin, + path != null ? ("Invalid path '" + path + "': " + message) + : message, cause); + } + + public BadPath(ConfigOrigin origin, String path, String message) { + this(origin, path, message, null); + } + + public BadPath(String path, String message, Throwable cause) { + super(path != null ? ("Invalid path '" + path + "': " + message) + : message, cause); + } + + public BadPath(String path, String message) { + this(path, message, null); + } + + public BadPath(ConfigOrigin origin, String message) { + this(origin, null, message); + } + } + + /** + * 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. + * + */ + public static class BugOrBroken extends ConfigException { + private static final long serialVersionUID = 1L; + + public BugOrBroken(String message, Throwable cause) { + super(message, cause); + } + + public BugOrBroken(String message) { + this(message, null); + } + } + + /** + * Exception indicating that there was an IO error. + * + */ + public static class IO extends ConfigException { + private static final long serialVersionUID = 1L; + + public IO(ConfigOrigin origin, String message, Throwable cause) { + super(origin, message, cause); + } + + public IO(ConfigOrigin origin, String message) { + this(origin, message, null); + } + } + + /** + * Exception indicating that there was a parse error. + * + */ + public static class Parse extends ConfigException { + private static final long serialVersionUID = 1L; + + public Parse(ConfigOrigin origin, String message, Throwable cause) { + super(origin, message, cause); + } + + public Parse(ConfigOrigin origin, String message) { + this(origin, message, 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. + */ + public static class NotResolved extends BugOrBroken { + private static final long serialVersionUID = 1L; + + public NotResolved(String message, Throwable cause) { + super(message, cause); + } + + public NotResolved(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 new file mode 100644 index 0000000000..cd82e3fa11 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigFactory.java @@ -0,0 +1,238 @@ +package com.typesafe.config; + +import java.io.File; +import java.io.Reader; +import java.net.URL; +import java.util.Map; +import java.util.Properties; + +import com.typesafe.config.impl.ConfigImpl; +import com.typesafe.config.impl.Parseable; + +/** + * This class contains static methods for creating Config objects. + * + * 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. + */ +public final class 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. + * + * 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(). + * + * @param rootPath + * the configuration "domain" + * @return configuration object for the requested root path + */ + 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); + } + + /** + * 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 + */ + public static ConfigRoot loadWithoutResolving(String rootPath) { + return loadWithoutResolving(rootPath, ConfigParseOptions.defaults()); + } + + 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.withFallbacks(mainFiles, referenceFiles); + } + + public static ConfigRoot emptyRoot(String rootPath) { + return emptyRoot(rootPath, null); + } + + public static Config empty() { + return empty(null); + } + + public static ConfigRoot emptyRoot(String rootPath, String originDescription) { + return ConfigImpl.emptyRoot(rootPath, originDescription); + } + + public static Config empty(String originDescription) { + return ConfigImpl.emptyConfig(originDescription); + } + + public static ConfigRoot systemPropertiesRoot(String rootPath) { + return ConfigImpl.systemPropertiesRoot(rootPath); + } + + public static Config systemProperties() { + return ConfigImpl.systemPropertiesAsConfig(); + } + + 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. + * + * 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. + * + * @param properties + * a Java Properties object + * @param options + * @return + */ + public static Config parseProperties(Properties properties, + ConfigParseOptions options) { + return Parseable.newProperties(properties, options).parse().toConfig(); + } + + public static Config parseReader(Reader reader, ConfigParseOptions options) { + return Parseable.newReader(reader, options).parse().toConfig(); + } + + public static Config parseURL(URL url, ConfigParseOptions options) { + return Parseable.newURL(url, options).parse().toConfig(); + } + + public static Config parseFile(File file, ConfigParseOptions options) { + return Parseable.newFile(file, options).parse().toConfig(); + } + + /** + * 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. + * + * @param fileBasename + * @param options + * @return + */ + 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(); + } + + /** + * Same behavior as parseFileAnySyntax() but for classpath resources + * instead. + * + * @param klass + * @param resourceBasename + * @param options + * @return + */ + public static Config parseResourceAnySyntax(Class klass, String resourceBasename, + ConfigParseOptions options) { + return ConfigImpl.parseResourceAnySyntax(klass, resourceBasename, + options).toConfig(); + } + + 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(); + } + + /** + * 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. + * + * 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 + * object of "b". The caller of this method should ensure that doesn't + * happen. + * + * @param values + * @param originDescription + * description of what this map represents, like a filename, or + * "default settings" (origin description is used in error + * messages) + * @return + */ + public static Config parseMap(Map values, + String originDescription) { + return ConfigImpl.fromPathMap(values, originDescription).toConfig(); + } + + /** + * See the other overload of parseMap() for details, this one just uses a + * default origin description. + * + * @param values + * @return + */ + 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 new file mode 100644 index 0000000000..a79fa6edbc --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigIncludeContext.java @@ -0,0 +1,26 @@ +package com.typesafe.config; + + +/** + * A ConfigIncludeContext is passed to a ConfigIncluder. This interface is not + * intended for apps to implement. + */ +public interface ConfigIncludeContext { + /** + * Tries to find a name relative to whatever is doing the including, for + * example in the same directory as the file doing the including. Returns + * 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.) + * + * @param filename + * the name to make relative to the resource doing the including + * @return parseable item relative to the resource doing the including, or + * null + */ + ConfigParseable relativeTo(String filename); +} diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigIncluder.java b/akka-actor/src/main/java/com/typesafe/config/ConfigIncluder.java new file mode 100644 index 0000000000..5f100a5292 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigIncluder.java @@ -0,0 +1,37 @@ +package com.typesafe.config; + +/** + * Interface you have to implement to customize "include" statements in config + * files. + */ +public interface ConfigIncluder { + /** + * Returns a new includer that falls back to the given includer. This is how + * you can obtain the default includer; it will be provided as a fallback. + * It's up to your includer to chain to it if you want to. You might want to + * merge any files found by the fallback includer with any objects you load + * yourself. + * + * It's important to handle the case where you already have the fallback + * with a "return this", i.e. this method should not create a new object if + * the fallback is the same one you already have. The same fallback may be + * added repeatedly. + * + * @param fallback + * @return a new includer + */ + ConfigIncluder withFallback(ConfigIncluder fallback); + + /** + * 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 + * the include statement's argument + * @return a non-null ConfigObject + */ + ConfigObject include(ConfigIncludeContext context, String 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 new file mode 100644 index 0000000000..5efe7014fe --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigList.java @@ -0,0 +1,20 @@ +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. + * + */ +public interface ConfigList extends List, ConfigValue { + + /** + * Recursively unwraps the list, returning a list of plain Java values such + * as Integer or String or whatever is in the list. + */ + @Override + List unwrapped(); + +} diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigMergeable.java b/akka-actor/src/main/java/com/typesafe/config/ConfigMergeable.java new file mode 100644 index 0000000000..c1b0b6fc32 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigMergeable.java @@ -0,0 +1,67 @@ +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. + */ +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 + * + * Prefer withFallbacks(), listing all your fallbacks at once, + * over this method. + * + * When using this method, there is an easy way to write a wrong + * loop. Even if you don't loop, it's easy to do the equivalent wrong + * thing. + * + * + * // WRONG + * for (ConfigMergeable fallback : fallbacks) { + * // DO NOT DO THIS + * result = result.withFallback(fallback); + * } + * + * + * This is wrong because when result is an object and + * fallback is a non-object, + * result.withFallback(fallback) returns an object. Then if + * there are more objects, they are merged into that object. But the correct + * semantics are that a non-object will block merging any more objects later + * in the list. To get it right, you need to iterate backward. Simplest + * solution: prefer withFallbacks() which is harder to get + * wrong, and merge all your fallbacks in one call to + * withFallbacks(). + * + * @param other + * an object whose keys should be used if the keys are not + * present in this one + * @return a new object (or the original one, if the fallback doesn't get + * used) + */ + ConfigMergeable withFallback(ConfigMergeable other); + + /** + * Convenience method just calls withFallback() on each of the values; + * earlier values in the list win over later ones. The semantics of merging + * are described in https://github.com/havocp/config/blob/master/HOCON.md + * + * @param fallbacks + * @return a version of the object with the requested fallbacks merged in + */ + ConfigMergeable withFallbacks(ConfigMergeable... others); +} diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigObject.java b/akka-actor/src/main/java/com/typesafe/config/ConfigObject.java new file mode 100644 index 0000000000..1763358496 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigObject.java @@ -0,0 +1,72 @@ +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. + * + * In most cases you want to use the Config interface rather than this one. Call + * toConfig() to convert a ConfigObject to a config. + * + * 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. + * + * 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. + * + * ConfigObject implements java.util.Map and all methods + * work with keys, not path expressions. + * + * While ConfigObject implements the standard Java Map interface, the mutator + * methods all throw UnsupportedOperationException. This Map is immutable. + * + * 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. + */ +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 + * operation (it is not proportional to the size of the object). + * + * @return + */ + Config toConfig(); + + /** + * Recursively unwraps the object, returning a map from String to whatever + * plain Java values are unwrapped from the object's values. + */ + @Override + Map unwrapped(); + + @Override + ConfigObject withFallback(ConfigMergeable other); + + @Override + ConfigObject withFallbacks(ConfigMergeable... others); + + /** + * 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. + */ + @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 new file mode 100644 index 0000000000..3935888060 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigOrigin.java @@ -0,0 +1,9 @@ +package com.typesafe.config; + +/** + * 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. + */ +public interface ConfigOrigin { + public String description(); +} diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigParseOptions.java b/akka-actor/src/main/java/com/typesafe/config/ConfigParseOptions.java new file mode 100644 index 0000000000..8b26fc45dd --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigParseOptions.java @@ -0,0 +1,129 @@ +package com.typesafe.config; + + +public final class ConfigParseOptions { + final ConfigSyntax syntax; + final String originDescription; + final boolean allowMissing; + final ConfigIncluder includer; + + protected ConfigParseOptions(ConfigSyntax syntax, String originDescription, + boolean allowMissing, ConfigIncluder includer) { + this.syntax = syntax; + this.originDescription = originDescription; + this.allowMissing = allowMissing; + this.includer = includer; + } + + public static ConfigParseOptions defaults() { + return new ConfigParseOptions(null, null, true, null); + } + + /** + * Set the file format. If set to null, try to guess from any available + * filename extension; if guessing fails, assume ConfigSyntax.CONF. + * + * @param syntax + * @return + */ + public ConfigParseOptions setSyntax(ConfigSyntax syntax) { + if (this.syntax == syntax) + return this; + else + return new ConfigParseOptions(syntax, this.originDescription, + this.allowMissing, this.includer); + } + + public ConfigSyntax getSyntax() { + return syntax; + } + + /** + * 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. + * + * @param originDescription + * @return + */ + public ConfigParseOptions setOriginDescription(String originDescription) { + if (this.originDescription == originDescription) + return this; + else if (this.originDescription != null && originDescription != null + && this.originDescription.equals(originDescription)) + return this; + else + return new ConfigParseOptions(this.syntax, originDescription, + this.allowMissing, this.includer); + } + + public String getOriginDescription() { + return originDescription; + } + + /** this is package-private, not public API */ + ConfigParseOptions withFallbackOriginDescription(String originDescription) { + if (this.originDescription == null) + return setOriginDescription(originDescription); + else + return this; + } + + /** + * Set to false to throw an exception if the item being parsed (for example + * a file) is missing. Set to true to just return an empty document in that + * case. + * + * @param allowMissing + * @return + */ + public ConfigParseOptions setAllowMissing(boolean allowMissing) { + if (this.allowMissing == allowMissing) + return this; + else + return new ConfigParseOptions(this.syntax, this.originDescription, + allowMissing, this.includer); + } + + public boolean getAllowMissing() { + return allowMissing; + } + + /** + * Set a ConfigIncluder which customizes how includes are handled. + * + * @param includer + * @return new version of the parse options with different includer + */ + public ConfigParseOptions setIncluder(ConfigIncluder includer) { + if (this.includer == includer) + return this; + else + return new ConfigParseOptions(this.syntax, this.originDescription, + this.allowMissing, includer); + } + + public ConfigParseOptions prependIncluder(ConfigIncluder includer) { + if (this.includer == includer) + return this; + else if (this.includer != null) + return setIncluder(includer.withFallback(this.includer)); + else + return setIncluder(includer); + } + + public ConfigParseOptions appendIncluder(ConfigIncluder includer) { + if (this.includer == includer) + return this; + else if (this.includer != null) + return setIncluder(this.includer.withFallback(includer)); + else + return setIncluder(includer); + } + + public ConfigIncluder getIncluder() { + return includer; + } + +} diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigParseable.java b/akka-actor/src/main/java/com/typesafe/config/ConfigParseable.java new file mode 100644 index 0000000000..9845d2acde --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigParseable.java @@ -0,0 +1,20 @@ +package com.typesafe.config; + +import java.net.URL; + +/** An opaque handle to something that can be parsed. */ +public interface ConfigParseable { + /** + * Parse whatever it is. + * + * @param options + * parse options, should be based on the ones from options() + */ + ConfigObject parse(ConfigParseOptions options); + + /** Possibly return a URL representing the resource; this may return null. */ + URL url(); + + /** Get the initial options, which can be modified then passed to parse(). */ + 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 new file mode 100644 index 0000000000..91e3320cd1 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigResolveOptions.java @@ -0,0 +1,36 @@ +package com.typesafe.config; + +public final class ConfigResolveOptions { + private final boolean useSystemProperties; + private final boolean useSystemEnvironment; + + private ConfigResolveOptions(boolean useSystemProperties, + boolean useSystemEnvironment) { + this.useSystemProperties = useSystemProperties; + this.useSystemEnvironment = useSystemEnvironment; + } + + public static ConfigResolveOptions defaults() { + return new ConfigResolveOptions(true, true); + } + + public static ConfigResolveOptions noSystem() { + return new ConfigResolveOptions(false, false); + } + + public ConfigResolveOptions setUseSystemProperties(boolean value) { + return new ConfigResolveOptions(value, useSystemEnvironment); + } + + public ConfigResolveOptions setUseSystemEnvironment(boolean value) { + return new ConfigResolveOptions(useSystemProperties, value); + } + + public boolean getUseSystemProperties() { + return useSystemProperties; + } + + 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 new file mode 100644 index 0000000000..7554280688 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigRoot.java @@ -0,0 +1,33 @@ +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); + + @Override + ConfigRoot withFallbacks(ConfigMergeable... fallbacks); + + /** + * 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 new file mode 100644 index 0000000000..c1c68d0783 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigSyntax.java @@ -0,0 +1,5 @@ +package com.typesafe.config; + +public enum ConfigSyntax { + JSON, CONF, 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 new file mode 100644 index 0000000000..006b89a9de --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigValue.java @@ -0,0 +1,35 @@ +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. + */ +public interface ConfigValue extends ConfigMergeable { + /** + * The origin of the value, for debugging and error messages. + * + * @return where the value came from + */ + ConfigOrigin origin(); + + /** + * The type 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. + */ + Object unwrapped(); + + @Override + ConfigValue withFallback(ConfigMergeable other); + + @Override + ConfigValue withFallbacks(ConfigMergeable... fallbacks); +} diff --git a/akka-actor/src/main/java/com/typesafe/config/ConfigValueFactory.java b/akka-actor/src/main/java/com/typesafe/config/ConfigValueFactory.java new file mode 100644 index 0000000000..05ffefa152 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigValueFactory.java @@ -0,0 +1,126 @@ +package com.typesafe.config; + +import java.util.Collection; +import java.util.Map; + +import com.typesafe.config.impl.ConfigImpl; + +/** + * This class holds some static factory methods for building ConfigValue. See + * also ConfigFactory which has methods for parsing files and certain in-memory + * data structures. + */ +public final class 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 + * String to more values that can be supplied to fromAnyRef(). An Iterable + * must iterate over more values that can be supplied to fromAnyRef(). A Map + * will become a ConfigObject and an Iterable will become a ConfigList. If + * 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 + * against this library). + * + * @param object + * object to convert to ConfigValue + * @param originDescription + * name of origin file or brief description of what the value is + * @return a new value + */ + public static ConfigValue fromAnyRef(Object object, String originDescription) { + return ConfigImpl.fromAnyRef(object, originDescription); + } + + /** + * See the fromAnyRef() documentation for details. This is a typesafe + * wrapper that only works on Map and returns ConfigObject rather than + * 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. + * + * There is a separate fromPathMap() that interprets the keys in the map as + * path expressions. + * + * @param values + * @param originDescription + * @return + */ + public static ConfigObject fromMap(Map values, + String originDescription) { + return (ConfigObject) fromAnyRef(values, originDescription); + } + + /** + * See the fromAnyRef() documentation for details. This is a typesafe + * wrapper that only works on Iterable and returns ConfigList rather than + * ConfigValue. + * + * @param values + * @param originDescription + * @return + */ + public static ConfigList fromIterable(Iterable values, + String originDescription) { + return (ConfigList) fromAnyRef(values, originDescription); + } + + /** + * See the other overload of fromAnyRef() for details, this one just uses a + * default origin description. + * + * @param object + * @return + */ + 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. + * + * @param values + * @return + */ + 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. + * + * @param values + * @return + */ + public static ConfigList fromIterable(Collection 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 new file mode 100644 index 0000000000..1de791698a --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/ConfigValueType.java @@ -0,0 +1,8 @@ +package com.typesafe.config; + +/** + * The type of a configuration value. Value types follow 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 new file mode 100644 index 0000000000..6e4ff47c5b --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java @@ -0,0 +1,355 @@ +package com.typesafe.config.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigMergeable; +import com.typesafe.config.ConfigObject; +import com.typesafe.config.ConfigOrigin; +import com.typesafe.config.ConfigResolveOptions; +import com.typesafe.config.ConfigValue; +import com.typesafe.config.ConfigValueType; + +abstract class AbstractConfigObject extends AbstractConfigValue implements + ConfigObject { + final private SimpleConfig config; + + protected AbstractConfigObject(ConfigOrigin origin) { + super(origin); + this.config = new SimpleConfig(this); + } + + @Override + public SimpleConfig toConfig() { + return config; + } + + /** + * This looks up the key with no transformation or type conversion of any + * kind, and returns null if the key is not present. + * + * @param key + * @return the unmodified raw value or null + */ + protected abstract AbstractConfigValue peek(String key); + + protected AbstractConfigValue peek(String key, + SubstitutionResolver resolver, int depth, + ConfigResolveOptions options) { + AbstractConfigValue v = peek(key); + + if (v != null && resolver != null) { + v = resolver.resolve(v, depth, options); + } + + return v; + } + + /** + * Looks up the path with no transformation, type conversion, or exceptions + * (just returns null if path not found). Does however resolve the path, if + * resolver != null. + */ + protected ConfigValue peekPath(Path path, SubstitutionResolver resolver, + int depth, ConfigResolveOptions options) { + return peekPath(this, path, resolver, depth, options); + } + + private static ConfigValue 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); + return v; + } else { + // it's important to ONLY resolve substitutions here, not + // all values, because if you resolve arrays or objects + // it creates unnecessary cycles as a side effect (any sibling + // of the object we want to follow could cause a cycle, not just + // the object we want to follow). + + ConfigValue v = self.peek(key); + + if (v instanceof ConfigSubstitution && resolver != null) { + v = resolver.resolve((AbstractConfigValue) v, depth, options); + } + + if (v instanceof AbstractConfigObject) { + return peekPath((AbstractConfigObject) v, next, resolver, + depth, options); + } else { + return null; + } + } + } + + @Override + public ConfigValueType valueType() { + return ConfigValueType.OBJECT; + } + + protected abstract AbstractConfigObject newCopy(ResolveStatus status); + + @Override + public AbstractConfigObject withFallbacks(ConfigMergeable... others) { + return (AbstractConfigObject) super.withFallbacks(others); + } + + @Override + public AbstractConfigObject withFallback(ConfigMergeable mergeable) { + ConfigValue other = mergeable.toValue(); + + if (other instanceof Unmergeable) { + List stack = new ArrayList(); + stack.add(this); + stack.addAll(((Unmergeable) other).unmergedValues()); + return new ConfigDelayedMergeObject(mergeOrigins(stack), stack); + } else if (other instanceof AbstractConfigObject) { + AbstractConfigObject fallback = (AbstractConfigObject) other; + if (fallback.isEmpty()) { + return this; // nothing to do + } else { + boolean allResolved = true; + Map merged = new HashMap(); + Set allKeys = new HashSet(); + allKeys.addAll(this.keySet()); + allKeys.addAll(fallback.keySet()); + for (String key : allKeys) { + AbstractConfigValue first = this.peek(key); + AbstractConfigValue second = fallback.peek(key); + AbstractConfigValue kept; + if (first == null) + kept = second; + else if (second == null) + kept = first; + else + kept = first.withFallback(second); + merged.put(key, kept); + if (kept.resolveStatus() == ResolveStatus.UNRESOLVED) + allResolved = false; + } + return new SimpleConfigObject(mergeOrigins(this, fallback), + merged, ResolveStatus.fromBoolean(allResolved)); + } + } else { + // falling back to a non-object has no effect, we just override + // primitive values. + return this; + } + } + + static ConfigOrigin mergeOrigins( + Collection stack) { + if (stack.isEmpty()) + throw new ConfigException.BugOrBroken( + "can't merge origins on empty list"); + final String prefix = "merge of "; + StringBuilder sb = new StringBuilder(); + 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 ConfigObject && ((ConfigObject) v).isEmpty()) { + // don't include empty files or the .empty() + // config in the description, since they are + // likely to be "implementation details" + } else { + sb.append(desc); + sb.append(","); + 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; + } + } + + static ConfigOrigin mergeOrigins(AbstractConfigObject... stack) { + return mergeOrigins(Arrays.asList(stack)); + } + + private AbstractConfigObject modify(Modifier modifier, + ResolveStatus newResolveStatus) { + Map changes = null; + for (String k : keySet()) { + AbstractConfigValue v = peek(k); + AbstractConfigValue modified = modifier.modifyChild(v); + if (modified != v) { + if (changes == null) + changes = new HashMap(); + changes.put(k, modified); + } + } + if (changes == null) { + return newCopy(newResolveStatus); + } else { + Map modified = new HashMap(); + for (String k : keySet()) { + if (changes.containsKey(k)) { + modified.put(k, changes.get(k)); + } else { + modified.put(k, peek(k)); + } + } + return new SimpleConfigObject(origin(), modified, + newResolveStatus); + } + } + + @Override + AbstractConfigObject resolveSubstitutions(final SubstitutionResolver resolver, + final int depth, + final ConfigResolveOptions options) { + if (resolveStatus() == ResolveStatus.RESOLVED) + return this; + + return modify(new Modifier() { + + @Override + public AbstractConfigValue modifyChild(AbstractConfigValue v) { + return resolver.resolve(v, depth, options); + } + + }, ResolveStatus.RESOLVED); + } + + @Override + AbstractConfigObject relativized(final Path prefix) { + return modify(new Modifier() { + + @Override + public AbstractConfigValue modifyChild(AbstractConfigValue v) { + return v.relativized(prefix); + } + + }, resolveStatus()); + } + + @Override + public AbstractConfigValue get(Object key) { + if (key instanceof String) + return peek((String) key); + else + return null; + } + + @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(","); + } + if (!keySet().isEmpty()) + sb.setLength(sb.length() - 1); // chop comma + sb.append(")"); + return sb.toString(); + } + + private static boolean mapEquals(Map a, + Map b) { + Set aKeys = a.keySet(); + Set bKeys = b.keySet(); + + if (!aKeys.equals(bKeys)) + return false; + + for (String key : aKeys) { + if (!a.get(key).equals(b.get(key))) + return false; + } + return true; + } + + private static int mapHash(Map m) { + // the keys have to be sorted, otherwise we could be equal + // to another map but have a different hashcode. + List keys = new ArrayList(); + keys.addAll(m.keySet()); + Collections.sort(keys); + + int valuesHash = 0; + for (String k : keys) { + valuesHash += m.get(k).hashCode(); + } + return 41 * (41 + keys.hashCode()) + valuesHash; + } + + @Override + protected boolean canEqual(Object other) { + return other instanceof ConfigObject; + } + + @Override + public boolean equals(Object other) { + // note that "origin" is deliberately NOT part of equality + if (other instanceof ConfigObject) { + // optimization to avoid unwrapped() for two ConfigObject, + // which is what AbstractConfigValue does. + return canEqual(other) && mapEquals(this, ((ConfigObject) other)); + } else { + return false; + } + } + + @Override + public int hashCode() { + // note that "origin" is deliberately NOT part of equality + return mapHash(this); + } + + private static UnsupportedOperationException weAreImmutable(String method) { + return new UnsupportedOperationException( + "ConfigObject is immutable, you can't call Map.'" + method + + "'"); + } + + @Override + public void clear() { + throw weAreImmutable("clear"); + } + + @Override + public ConfigValue put(String arg0, ConfigValue arg1) { + throw weAreImmutable("put"); + } + + @Override + public void putAll(Map arg0) { + throw weAreImmutable("putAll"); + } + + @Override + public ConfigValue remove(Object arg0) { + throw weAreImmutable("remove"); + } +} 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 new file mode 100644 index 0000000000..c4e5c3bf51 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java @@ -0,0 +1,129 @@ +package com.typesafe.config.impl; + +import com.typesafe.config.ConfigMergeable; +import com.typesafe.config.ConfigOrigin; +import com.typesafe.config.ConfigResolveOptions; +import com.typesafe.config.ConfigValue; + +/** + * + * Trying very hard to avoid a parent reference in config values; when you have + * a tree like this, the availability of parent() tends to result in a lot of + * improperly-factored and non-modular code. Please don't add parent(). + * + */ +abstract class AbstractConfigValue implements ConfigValue { + + final private ConfigOrigin origin; + + AbstractConfigValue(ConfigOrigin origin) { + this.origin = origin; + } + + @Override + public ConfigOrigin origin() { + return this.origin; + } + + /** + * Called only by SubstitutionResolver object. + * + * @param resolver + * the resolver doing the resolving + * @param depth + * the number of substitutions followed in resolving the current + * one + * @param options + * whether to look at system props and env vars + * @return a new value if there were changes, or this if no changes + */ + AbstractConfigValue resolveSubstitutions(SubstitutionResolver resolver, + int depth, + ConfigResolveOptions options) { + return this; + } + + ResolveStatus resolveStatus() { + return ResolveStatus.RESOLVED; + } + + /** + * This is used when including one file in another; the included file is + * relativized to the path it's included into in the parent file. The point + * is that if you include a file at foo.bar in the parent, and the included + * file as a substitution ${a.b.c}, the included substitution now needs to + * be ${foo.bar.a.b.c} because we resolve substitutions globally only after + * parsing everything. + * + * @param prefix + * @return value relativized to the given path or the same value if nothing + * to do + */ + AbstractConfigValue relativized(Path prefix) { + return this; + } + + protected interface Modifier { + AbstractConfigValue modifyChild(AbstractConfigValue v); + } + + @Override + public AbstractConfigValue toValue() { + return this; + } + + @Override + public AbstractConfigValue withFallback(ConfigMergeable other) { + return this; + } + + @Override + public AbstractConfigValue withFallbacks(ConfigMergeable... fallbacks) { + // note: this is a no-op unless the subclass overrides withFallback(). + // But we need to do this because subclass withFallback() may not + // just "return this" + return ConfigImpl.merge(AbstractConfigValue.class, this, fallbacks); + } + + protected boolean canEqual(Object other) { + return other instanceof ConfigValue; + } + + @Override + public boolean equals(Object other) { + // note that "origin" is deliberately NOT part of equality + if (other instanceof ConfigValue) { + return canEqual(other) + && (this.valueType() == + ((ConfigValue) other).valueType()) + && ConfigUtil.equalsHandlingNull(this.unwrapped(), + ((ConfigValue) other).unwrapped()); + } else { + return false; + } + } + + @Override + public int hashCode() { + // note that "origin" is deliberately NOT part of equality + Object o = this.unwrapped(); + if (o == null) + return 0; + else + return o.hashCode(); + } + + @Override + public String toString() { + return valueType().name() + "(" + unwrapped() + ")"; + } + + // toString() is a debugging-oriented string but this is defined + // to create a string that would parse back to the value in JSON. + // It only works for primitive values (that would be a single token) + // which are auto-converted to strings when concatenating with + // other strings or by the DefaultTransformer. + String transformToString() { + return null; + } +} diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigBoolean.java b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigBoolean.java new file mode 100644 index 0000000000..614a12e683 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigBoolean.java @@ -0,0 +1,29 @@ +package com.typesafe.config.impl; + +import com.typesafe.config.ConfigOrigin; +import com.typesafe.config.ConfigValueType; + +final class ConfigBoolean extends AbstractConfigValue { + + final private boolean value; + + ConfigBoolean(ConfigOrigin origin, boolean value) { + super(origin); + this.value = value; + } + + @Override + public ConfigValueType valueType() { + return ConfigValueType.BOOLEAN; + } + + @Override + public Boolean unwrapped() { + return value; + } + + @Override + String transformToString() { + return value ? "true" : "false"; + } +} 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 new file mode 100644 index 0000000000..3215958d22 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java @@ -0,0 +1,151 @@ +package com.typesafe.config.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigMergeable; +import com.typesafe.config.ConfigOrigin; +import com.typesafe.config.ConfigResolveOptions; +import com.typesafe.config.ConfigValue; +import com.typesafe.config.ConfigValueType; + +/** + * The issue here is that we want to first merge our stack of config files, and + * then we want to evaluate substitutions. But if two substitutions both expand + * to an object, we might need to merge those two objects. Thus, we can't ever + * "override" a substitution when we do a merge; instead we have to save the + * stack of values that should be merged, and resolve the merge when we evaluate + * substitutions. + */ +final class ConfigDelayedMerge extends AbstractConfigValue implements + Unmergeable { + + // earlier items in the stack win + final private List stack; + + ConfigDelayedMerge(ConfigOrigin origin, List stack) { + super(origin); + this.stack = stack; + if (stack.isEmpty()) + throw new ConfigException.BugOrBroken( + "creating empty delayed merge value"); + + } + + @Override + public ConfigValueType valueType() { + throw new ConfigException.NotResolved( + "called valueType() on value with unresolved substitutions, need to resolve first"); + } + + @Override + public Object unwrapped() { + throw new ConfigException.NotResolved( + "called unwrapped() on value with unresolved substitutions, need to resolve first"); + } + + @Override + AbstractConfigValue resolveSubstitutions(SubstitutionResolver resolver, + int depth, ConfigResolveOptions options) { + return resolveSubstitutions(stack, resolver, depth, options); + } + + // static method also used by ConfigDelayedMergeObject + static AbstractConfigValue resolveSubstitutions( + List stack, SubstitutionResolver resolver, + int depth, ConfigResolveOptions options) { + // to resolve substitutions, we need to recursively resolve + // the stack of stuff to merge, and then merge the stack. + List toMerge = new ArrayList(); + + for (AbstractConfigValue v : stack) { + AbstractConfigValue resolved = resolver.resolve(v, depth, options); + toMerge.add(resolved); + } + + // we shouldn't have a delayed merge object with an empty stack, so + // it should be safe to ignore the toMerge.isEmpty case. + return ConfigImpl.merge(AbstractConfigValue.class, toMerge.get(0), + toMerge.subList(1, toMerge.size())); + } + + @Override + ResolveStatus resolveStatus() { + return ResolveStatus.UNRESOLVED; + } + + @Override + ConfigDelayedMerge relativized(Path prefix) { + List newStack = new ArrayList(); + for (AbstractConfigValue o : stack) { + newStack.add(o.relativized(prefix)); + } + return new ConfigDelayedMerge(origin(), newStack); + } + + @Override + public AbstractConfigValue withFallback(ConfigMergeable mergeable) { + ConfigValue other = mergeable.toValue(); + + if (other instanceof AbstractConfigObject + || other instanceof Unmergeable) { + // if we turn out to be an object, and the fallback also does, + // then a merge may be required; delay until we resolve. + List newStack = new ArrayList(); + newStack.addAll(stack); + if (other instanceof Unmergeable) + newStack.addAll(((Unmergeable) other).unmergedValues()); + else + newStack.add((AbstractConfigValue) other); + return new ConfigDelayedMerge( + AbstractConfigObject.mergeOrigins(newStack), newStack); + } else { + // if the other is not an object, there won't be anything + // to merge with, so we are it even if we are an object. + return this; + } + } + + @Override + public Collection unmergedValues() { + return stack; + } + + @Override + protected boolean canEqual(Object other) { + return other instanceof ConfigDelayedMerge; + } + + @Override + public boolean equals(Object other) { + // note that "origin" is deliberately NOT part of equality + if (other instanceof ConfigDelayedMerge) { + return canEqual(other) + && this.stack.equals(((ConfigDelayedMerge) other).stack); + } else { + return false; + } + } + + @Override + public int hashCode() { + // note that "origin" is deliberately NOT part of equality + return stack.hashCode(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("DELAYED_MERGE"); + sb.append("("); + for (Object s : stack) { + sb.append(s.toString()); + sb.append(","); + } + 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 new file mode 100644 index 0000000000..5a05403b9e --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigDelayedMergeObject.java @@ -0,0 +1,190 @@ +package com.typesafe.config.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigMergeable; +import com.typesafe.config.ConfigOrigin; +import com.typesafe.config.ConfigResolveOptions; +import com.typesafe.config.ConfigValue; + +// This is just like ConfigDelayedMerge except we know statically +// that it will turn out to be an object. +class ConfigDelayedMergeObject extends AbstractConfigObject implements + Unmergeable { + + final private List stack; + + ConfigDelayedMergeObject(ConfigOrigin origin, + List stack) { + super(origin); + this.stack = stack; + if (stack.isEmpty()) + throw new ConfigException.BugOrBroken( + "creating empty delayed merge object"); + if (!(stack.get(0) instanceof AbstractConfigObject)) + throw new ConfigException.BugOrBroken( + "created a delayed merge object not guaranteed to be an object"); + } + + @Override + public ConfigDelayedMergeObject newCopy(ResolveStatus status) { + if (status != resolveStatus()) + throw new ConfigException.BugOrBroken( + "attempt to create resolved ConfigDelayedMergeObject"); + return new ConfigDelayedMergeObject(origin(), stack); + } + + @Override + AbstractConfigObject resolveSubstitutions(SubstitutionResolver resolver, + int depth, ConfigResolveOptions options) { + AbstractConfigValue merged = ConfigDelayedMerge.resolveSubstitutions( + stack, resolver, depth, + options); + if (merged instanceof AbstractConfigObject) { + return (AbstractConfigObject) merged; + } else { + throw new ConfigException.BugOrBroken( + "somehow brokenly merged an object and didn't get an object"); + } + } + + @Override + ResolveStatus resolveStatus() { + return ResolveStatus.UNRESOLVED; + } + + @Override + ConfigDelayedMergeObject relativized(Path prefix) { + List newStack = new ArrayList(); + for (AbstractConfigValue o : stack) { + newStack.add(o.relativized(prefix)); + } + return new ConfigDelayedMergeObject(origin(), newStack); + } + + @Override + public ConfigDelayedMergeObject withFallback(ConfigMergeable mergeable) { + ConfigValue other = mergeable.toValue(); + + if (other instanceof AbstractConfigObject + || other instanceof Unmergeable) { + // since we are an object, and the fallback could be, + // then a merge may be required; delay until we resolve. + List newStack = new ArrayList(); + newStack.addAll(stack); + if (other instanceof Unmergeable) + newStack.addAll(((Unmergeable) other).unmergedValues()); + else + newStack.add((AbstractConfigValue) other); + return new ConfigDelayedMergeObject( + AbstractConfigObject.mergeOrigins(newStack), + newStack); + } else { + // if the other is not an object, there won't be anything + // to merge with. + return this; + } + } + + @Override + public ConfigDelayedMergeObject withFallbacks(ConfigMergeable... others) { + return (ConfigDelayedMergeObject) super.withFallbacks(others); + } + + @Override + public Collection unmergedValues() { + return stack; + } + + @Override + protected boolean canEqual(Object other) { + return other instanceof ConfigDelayedMergeObject; + } + + @Override + public boolean equals(Object other) { + // note that "origin" is deliberately NOT part of equality + if (other instanceof ConfigDelayedMergeObject) { + return canEqual(other) + && this.stack + .equals(((ConfigDelayedMergeObject) other).stack); + } else { + return false; + } + } + + @Override + public int hashCode() { + // note that "origin" is deliberately NOT part of equality + return stack.hashCode(); + } + + @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(); + } + + private static ConfigException notResolved() { + return new ConfigException.NotResolved( + "bug: this object has not had substitutions resolved, so can't be used"); + } + + @Override + public Map unwrapped() { + throw notResolved(); + } + + @Override + public boolean containsKey(Object key) { + throw notResolved(); + } + + @Override + public boolean containsValue(Object value) { + throw notResolved(); + } + + @Override + public Set> entrySet() { + throw notResolved(); + } + + @Override + public boolean isEmpty() { + throw notResolved(); + } + + @Override + public Set keySet() { + throw notResolved(); + } + + @Override + public int size() { + throw notResolved(); + } + + @Override + public Collection values() { + throw notResolved(); + } + + @Override + protected AbstractConfigValue peek(String key) { + throw notResolved(); + } +} diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigDouble.java b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigDouble.java new file mode 100644 index 0000000000..483cf9cd79 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigDouble.java @@ -0,0 +1,43 @@ +package com.typesafe.config.impl; + +import com.typesafe.config.ConfigOrigin; +import com.typesafe.config.ConfigValueType; + +final class ConfigDouble extends ConfigNumber { + + final private double value; + + ConfigDouble(ConfigOrigin origin, double value, String originalText) { + super(origin, originalText); + this.value = value; + } + + @Override + public ConfigValueType valueType() { + return ConfigValueType.NUMBER; + } + + @Override + public Double unwrapped() { + return value; + } + + @Override + String transformToString() { + String s = super.transformToString(); + if (s == null) + return Double.toString(value); + else + return s; + } + + @Override + protected long longValue() { + return (long) value; + } + + @Override + protected double doubleValue() { + return value; + } +} 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 new file mode 100644 index 0000000000..9f45cb37cd --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigImpl.java @@ -0,0 +1,435 @@ +package com.typesafe.config.impl; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigIncludeContext; +import com.typesafe.config.ConfigIncluder; +import com.typesafe.config.ConfigMergeable; +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; + +/** This is public but is only supposed to be used by the "config" package */ +public class ConfigImpl { + + static T merge(Class klass, T first, + ConfigMergeable... others) { + List stack = Arrays.asList(others); + return merge(klass, first, stack); + } + + static T merge(Class klass, T first, + List stack) { + if (stack.isEmpty()) { + return first; + } else { + // to be consistent with the semantics of duplicate keys + // in the same file, we have to go backward like this. + // importantly, a primitive value always permanently + // hides a previous object value. + ListIterator i = stack + .listIterator(stack.size()); + ConfigMergeable merged = i.previous(); + while (i.hasPrevious()) { + merged = i.previous().withFallback(merged); + } + merged = first.withFallback(merged); + return klass.cast(merged); + } + } + + private interface NameSource { + ConfigParseable nameToParseable(String name); + } + + // this function is a little tricky because there are three places we're + // trying to use it; for 'include "basename"' in a .conf file, for + // loading app.{conf,json,properties} from classpath, and for + // loading app.{conf,json,properties} from the filesystem. + private static ConfigObject fromBasename(NameSource source, String name, + ConfigParseOptions options) { + ConfigObject obj; + if (name.endsWith(".conf") || name.endsWith(".json") + || name.endsWith(".properties")) { + ConfigParseable p = source.nameToParseable(name); + + if (p != null) { + obj = p.parse(p.options().setAllowMissing( + options.getAllowMissing())); + } else { + obj = SimpleConfigObject.emptyMissing(new SimpleConfigOrigin( + name)); + } + } else { + ConfigParseable confHandle = source.nameToParseable(name + ".conf"); + ConfigParseable jsonHandle = source.nameToParseable(name + ".json"); + ConfigParseable propsHandle = source.nameToParseable(name + + ".properties"); + + if (!options.getAllowMissing() && confHandle == null + && jsonHandle == null && propsHandle == null) { + throw new ConfigException.IO(new SimpleConfigOrigin(name), + "No config files {.conf,.json,.properties} found"); + } + + ConfigSyntax syntax = options.getSyntax(); + + obj = SimpleConfigObject.empty(new SimpleConfigOrigin(name)); + if (confHandle != null + && (syntax == null || syntax == ConfigSyntax.CONF)) { + obj = confHandle.parse(confHandle.options() + .setAllowMissing(true).setSyntax(ConfigSyntax.CONF)); + } + + if (jsonHandle != null + && (syntax == null || syntax == ConfigSyntax.JSON)) { + ConfigObject parsed = jsonHandle.parse(jsonHandle + .options().setAllowMissing(true) + .setSyntax(ConfigSyntax.JSON)); + obj = obj.withFallback(parsed); + } + + if (propsHandle != null + && (syntax == null || syntax == ConfigSyntax.PROPERTIES)) { + ConfigObject parsed = propsHandle.parse(propsHandle.options() + .setAllowMissing(true) + .setSyntax(ConfigSyntax.PROPERTIES)); + obj = obj.withFallback(parsed); + } + } + + 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, + String resourceBasename, final ConfigParseOptions baseOptions) { + NameSource source = new NameSource() { + @Override + public ConfigParseable nameToParseable(String name) { + return Parseable.newResource(klass, name, baseOptions); + } + }; + return fromBasename(source, resourceBasename, baseOptions); + } + + /** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */ + public static ConfigObject parseFileAnySyntax(final File basename, + final ConfigParseOptions baseOptions) { + NameSource source = new NameSource() { + @Override + public ConfigParseable nameToParseable(String name) { + return Parseable.newFile(new File(name), baseOptions); + } + }; + 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; + return emptyObject(origin); + } + + /** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */ + public static Config emptyConfig(String originDescription) { + return emptyObject(originDescription).toConfig(); + } + + static AbstractConfigObject empty(ConfigOrigin origin) { + return emptyObject(origin); + } + + // default origin for values created with fromAnyRef and no origin specified + final private static ConfigOrigin defaultValueOrigin = new SimpleConfigOrigin( + "hardcoded value"); + final private static ConfigBoolean defaultTrueValue = new ConfigBoolean( + defaultValueOrigin, true); + final private static ConfigBoolean defaultFalseValue = new ConfigBoolean( + defaultValueOrigin, false); + final private static ConfigNull defaultNullValue = new ConfigNull( + defaultValueOrigin); + final private static SimpleConfigList defaultEmptyList = new SimpleConfigList( + defaultValueOrigin, Collections. emptyList()); + final private static SimpleConfigObject defaultEmptyObject = SimpleConfigObject + .empty(defaultValueOrigin); + + private static SimpleConfigList emptyList(ConfigOrigin origin) { + if (origin == null || origin == defaultValueOrigin) + return defaultEmptyList; + else + return new SimpleConfigList(origin, + Collections. emptyList()); + } + + private static AbstractConfigObject emptyObject(ConfigOrigin origin) { + // we want null origin to go to SimpleConfigObject.empty() to get the + // origin "empty config" rather than "hardcoded value" + if (origin == defaultValueOrigin) + return defaultEmptyObject; + else + return SimpleConfigObject.empty(origin); + } + + private static ConfigOrigin valueOrigin(String originDescription) { + if (originDescription == null) + return defaultValueOrigin; + else + return new SimpleConfigOrigin(originDescription); + } + + /** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */ + public static ConfigValue fromAnyRef(Object object, String originDescription) { + ConfigOrigin origin = valueOrigin(originDescription); + return fromAnyRef(object, origin, FromMapMode.KEYS_ARE_KEYS); + } + + /** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */ + public static ConfigObject fromPathMap( + Map pathMap, String originDescription) { + ConfigOrigin origin = valueOrigin(originDescription); + return (ConfigObject) fromAnyRef(pathMap, origin, + FromMapMode.KEYS_ARE_PATHS); + } + + static AbstractConfigValue fromAnyRef(Object object, ConfigOrigin origin, + FromMapMode mapMode) { + if (origin == null) + throw new ConfigException.BugOrBroken( + "origin not supposed to be null"); + + if (object == null) { + if (origin != defaultValueOrigin) + return new ConfigNull(origin); + else + return defaultNullValue; + } else if (object instanceof Boolean) { + if (origin != defaultValueOrigin) { + return new ConfigBoolean(origin, (Boolean) object); + } else if ((Boolean) object) { + return defaultTrueValue; + } else { + return defaultFalseValue; + } + } else if (object instanceof String) { + return new ConfigString(origin, (String) object); + } else if (object instanceof Number) { + // here we always keep the same type that was passed to us, + // rather than figuring out if a Long would fit in an Int + // or a Double has no fractional part. i.e. deliberately + // not using ConfigNumber.newNumber() when we have a + // Double, Integer, or Long. + if (object instanceof Double) { + return new ConfigDouble(origin, (Double) object, null); + } else if (object instanceof Integer) { + return new ConfigInt(origin, (Integer) object, null); + } else if (object instanceof Long) { + return new ConfigLong(origin, (Long) object, null); + } else { + return ConfigNumber.newNumber(origin, + ((Number) object).doubleValue(), null); + } + } else if (object instanceof Map) { + if (((Map) object).isEmpty()) + return emptyObject(origin); + + if (mapMode == FromMapMode.KEYS_ARE_KEYS) { + Map values = new HashMap(); + for (Map.Entry entry : ((Map) object).entrySet()) { + Object key = entry.getKey(); + if (!(key instanceof String)) + throw new ConfigException.BugOrBroken( + "bug in method caller: not valid to create ConfigObject from map with non-String key: " + + key); + AbstractConfigValue value = fromAnyRef(entry.getValue(), + origin, mapMode); + values.put((String) key, value); + } + + return new SimpleConfigObject(origin, values); + } else { + return PropertiesParser.fromPathMap(origin, (Map) object); + } + } else if (object instanceof Iterable) { + Iterator i = ((Iterable) object).iterator(); + if (!i.hasNext()) + return emptyList(origin); + + List values = new ArrayList(); + while (i.hasNext()) { + AbstractConfigValue v = fromAnyRef(i.next(), origin, mapMode); + values.add(v); + } + + return new SimpleConfigList(origin, values); + } else { + throw new ConfigException.BugOrBroken( + "bug in method caller: not valid to create ConfigValue from: " + + object); + } + } + + /** 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; + + SimpleIncluder(ConfigIncluder fallback) { + this.fallback = fallback; + } + + @Override + public ConfigObject include(final ConfigIncludeContext context, + String name) { + NameSource source = new NameSource() { + @Override + public ConfigParseable nameToParseable(String name) { + return context.relativeTo(name); + } + }; + + ConfigObject obj = fromBasename(source, name, ConfigParseOptions + .defaults().setAllowMissing(true)); + + // now use the fallback includer if any and merge + // its result. + if (fallback != null) { + return obj.withFallback(fallback.include(context, name)); + } else { + return obj; + } + } + + @Override + public ConfigIncluder withFallback(ConfigIncluder fallback) { + if (this == fallback) { + throw new ConfigException.BugOrBroken( + "trying to create includer cycle"); + } else if (this.fallback == fallback) { + return this; + } else if (this.fallback != null) { + return new SimpleIncluder(this.fallback.withFallback(fallback)); + } else { + return new SimpleIncluder(fallback); + } + } + } + + private static ConfigIncluder defaultIncluder = null; + + synchronized static ConfigIncluder defaultIncluder() { + if (defaultIncluder == null) { + defaultIncluder = new SimpleIncluder(null); + } + return defaultIncluder; + } + + private static AbstractConfigObject systemProperties = null; + + synchronized static AbstractConfigObject systemPropertiesAsConfigObject() { + if (systemProperties == null) { + systemProperties = loadSystemProperties(); + } + return systemProperties; + } + + private static AbstractConfigObject loadSystemProperties() { + return (AbstractConfigObject) Parseable.newProperties( + System.getProperties(), + ConfigParseOptions.defaults().setOriginDescription( + "system properties")).parse(); + } + + /** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */ + public static Config systemPropertiesAsConfig() { + 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; + } + + private static AbstractConfigObject loadEnvVariables() { + Map env = System.getenv(); + 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())); + } + return new SimpleConfigObject(new SimpleConfigOrigin("env variables"), + m, ResolveStatus.RESOLVED); + } + + /** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */ + public static Config envVariablesAsConfig() { + return envVariablesAsConfigObject().toConfig(); + } +} diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigInt.java b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigInt.java new file mode 100644 index 0000000000..192abbde03 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigInt.java @@ -0,0 +1,43 @@ +package com.typesafe.config.impl; + +import com.typesafe.config.ConfigOrigin; +import com.typesafe.config.ConfigValueType; + +final class ConfigInt extends ConfigNumber { + + final private int value; + + ConfigInt(ConfigOrigin origin, int value, String originalText) { + super(origin, originalText); + this.value = value; + } + + @Override + public ConfigValueType valueType() { + return ConfigValueType.NUMBER; + } + + @Override + public Integer unwrapped() { + return value; + } + + @Override + String transformToString() { + String s = super.transformToString(); + if (s == null) + return Integer.toString(value); + else + return s; + } + + @Override + protected long longValue() { + return value; + } + + @Override + protected double doubleValue() { + return value; + } +} diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigLong.java b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigLong.java new file mode 100644 index 0000000000..f949f10b87 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigLong.java @@ -0,0 +1,43 @@ +package com.typesafe.config.impl; + +import com.typesafe.config.ConfigOrigin; +import com.typesafe.config.ConfigValueType; + +final class ConfigLong extends ConfigNumber { + + final private long value; + + ConfigLong(ConfigOrigin origin, long value, String originalText) { + super(origin, originalText); + this.value = value; + } + + @Override + public ConfigValueType valueType() { + return ConfigValueType.NUMBER; + } + + @Override + public Long unwrapped() { + return value; + } + + @Override + String transformToString() { + String s = super.transformToString(); + if (s == null) + return Long.toString(value); + else + return s; + } + + @Override + protected long longValue() { + return value; + } + + @Override + protected double doubleValue() { + return value; + } +} 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 new file mode 100644 index 0000000000..84a7a41568 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigNull.java @@ -0,0 +1,34 @@ +package com.typesafe.config.impl; + +import com.typesafe.config.ConfigOrigin; +import com.typesafe.config.ConfigValueType; + +/** + * This exists because sometimes null is not the same as missing. Specifically, + * if a value is set to null we can give a better error message (indicating + * where it was set to null) in case someone asks for the value. Also, null + * overrides values set "earlier" in the search path, while missing values do + * not. + * + */ +final class ConfigNull extends AbstractConfigValue { + + ConfigNull(ConfigOrigin origin) { + super(origin); + } + + @Override + public ConfigValueType valueType() { + return ConfigValueType.NULL; + } + + @Override + public Object unwrapped() { + return null; + } + + @Override + String transformToString() { + return "null"; + } +} diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/ConfigNumber.java b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigNumber.java new file mode 100644 index 0000000000..eec0ff5008 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigNumber.java @@ -0,0 +1,83 @@ +package com.typesafe.config.impl; + +import com.typesafe.config.ConfigOrigin; + +abstract class ConfigNumber extends AbstractConfigValue { + // This is so when we concatenate a number into a string (say it appears in + // a sentence) we always have it exactly as the person typed it into the + // config file. It's purely cosmetic; equals/hashCode don't consider this + // for example. + final private String originalText; + + protected ConfigNumber(ConfigOrigin origin, String originalText) { + super(origin); + this.originalText = originalText; + } + + @Override + String transformToString() { + return originalText; + } + + protected abstract long longValue(); + + protected abstract double doubleValue(); + + private boolean isWhole() { + long asLong = longValue(); + return asLong == doubleValue(); + } + + @Override + protected boolean canEqual(Object other) { + return other instanceof ConfigNumber; + } + + @Override + public boolean equals(Object other) { + // note that "origin" is deliberately NOT part of equality + if (canEqual(other)) { + ConfigNumber n = (ConfigNumber) other; + if (isWhole()) { + return n.isWhole() && this.longValue() == n.longValue(); + } else { + return (!n.isWhole()) && this.doubleValue() == n.doubleValue(); + } + } else { + return false; + } + } + + @Override + public int hashCode() { + // note that "origin" is deliberately NOT part of equality + + // this matches what standard Long.hashCode and Double.hashCode + // do, though I don't think it really matters. + long asLong; + if (isWhole()) { + asLong = longValue(); + } else { + asLong = Double.doubleToLongBits(doubleValue()); + } + return (int) (asLong ^ (asLong >>> 32)); + } + + static ConfigNumber newNumber(ConfigOrigin origin, long number, + String originalText) { + if (number <= Integer.MAX_VALUE && number >= Integer.MIN_VALUE) + return new ConfigInt(origin, (int) number, originalText); + else + return new ConfigLong(origin, number, originalText); + } + + static ConfigNumber newNumber(ConfigOrigin origin, double number, + String originalText) { + long asLong = (long) number; + if (asLong == number) { + return newNumber(origin, asLong, originalText); + } else { + return new ConfigDouble(origin, number, originalText); + } + } +} 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 new file mode 100644 index 0000000000..522d5bf5a7 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigString.java @@ -0,0 +1,29 @@ +package com.typesafe.config.impl; + +import com.typesafe.config.ConfigOrigin; +import com.typesafe.config.ConfigValueType; + +final class ConfigString extends AbstractConfigValue { + + final private String value; + + ConfigString(ConfigOrigin origin, String value) { + super(origin); + this.value = value; + } + + @Override + public ConfigValueType valueType() { + return ConfigValueType.STRING; + } + + @Override + public String unwrapped() { + return value; + } + + @Override + String transformToString() { + return 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 new file mode 100644 index 0000000000..ae894e03a1 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigSubstitution.java @@ -0,0 +1,239 @@ +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; +import com.typesafe.config.ConfigMergeable; +import com.typesafe.config.ConfigOrigin; +import com.typesafe.config.ConfigResolveOptions; +import com.typesafe.config.ConfigValue; +import com.typesafe.config.ConfigValueType; + +/** + * A ConfigSubstitution represents a value with one or more substitutions in it; + * it can resolve to a value of any type, though if the substitution has more + * than one piece it always resolves to a string via value concatenation. + */ +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 + // than one piece everything is stringified and concatenated + final private List pieces; + // the length of any prefixes added with relativized() + final int prefixLength; + + ConfigSubstitution(ConfigOrigin origin, List pieces) { + this(origin, pieces, 0); + } + + private ConfigSubstitution(ConfigOrigin origin, List pieces, + int prefixLength) { + super(origin); + this.pieces = pieces; + this.prefixLength = prefixLength; + } + + @Override + public ConfigValueType valueType() { + throw new ConfigException.NotResolved( + "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); + } + + @Override + public AbstractConfigValue withFallback(ConfigMergeable mergeable) { + ConfigValue other = mergeable.toValue(); + + if (other instanceof AbstractConfigObject + || other instanceof Unmergeable) { + // if we turn out to be an object, and the fallback also does, + // then a merge may be required; delay until we resolve. + List newStack = new ArrayList(); + newStack.add(this); + if (other instanceof Unmergeable) + newStack.addAll(((Unmergeable) other).unmergedValues()); + else + newStack.add((AbstractConfigValue) other); + return new ConfigDelayedMerge( + AbstractConfigObject.mergeOrigins(newStack), newStack); + } else { + // if the other is not an object, there won't be anything + // to merge with, so we are it even if we are an object. + return this; + } + } + + @Override + public Collection unmergedValues() { + return Collections.singleton(this); + } + + List pieces() { + return pieces; + } + + // larger than anyone would ever want + private static final int MAX_DEPTH = 100; + + private ConfigValue findInObject(AbstractConfigObject root, + SubstitutionResolver resolver, /* null if we should not have refs */ + Path subst, int depth, ConfigResolveOptions options) { + if (depth > MAX_DEPTH) { + throw new ConfigException.BadValue(origin(), subst.render(), + "Substitution ${" + subst.render() + + "} is part of a cycle of substitutions"); + } + + ConfigValue result = root.peekPath(subst, resolver, depth, options); + + if (result instanceof ConfigSubstitution) { + throw new ConfigException.BugOrBroken( + "peek or peekPath returned an unresolved substitution"); + } + + return result; + } + + private ConfigValue resolve(SubstitutionResolver resolver, Path subst, + int depth, ConfigResolveOptions options) { + ConfigValue result = findInObject(resolver.root(), resolver, subst, + 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()); + } + + return result; + } + + private ConfigValue resolve(SubstitutionResolver resolver, int depth, + ConfigResolveOptions options) { + if (pieces.size() > 1) { + // need to concat everything into a string + StringBuilder sb = new StringBuilder(); + for (Object p : pieces) { + 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()); + } + } + } + return new ConfigString(origin(), sb.toString()); + } else { + if (!(pieces.get(0) instanceof Path)) + throw new ConfigException.BugOrBroken( + "ConfigSubstitution should never contain a single String piece"); + return resolve(resolver, (Path) pieces.get(0), depth, options); + } + } + + @Override + AbstractConfigValue resolveSubstitutions(SubstitutionResolver resolver, + int depth, + ConfigResolveOptions options) { + // only ConfigSubstitution adds to depth here, because the depth + // is the substitution depth not the recursion depth + AbstractConfigValue resolved = (AbstractConfigValue) resolve(resolver, + depth + 1, options); + return resolved; + } + + @Override + ResolveStatus resolveStatus() { + return ResolveStatus.UNRESOLVED; + } + + // when you graft a substitution into another object, + // you have to prefix it with the location in that object + // where you grafted it; but save prefixLength so + // system property and env variable lookups don't get + // broken. + @Override + ConfigSubstitution relativized(Path prefix) { + List newPieces = new ArrayList(); + for (Object p : pieces) { + if (p instanceof Path) { + newPieces.add(((Path) p).prepend(prefix)); + } else { + newPieces.add(p); + } + } + return new ConfigSubstitution(origin(), newPieces, prefixLength + + prefix.length()); + } + + @Override + protected boolean canEqual(Object other) { + return other instanceof ConfigSubstitution; + } + + @Override + public boolean equals(Object other) { + // note that "origin" is deliberately NOT part of equality + if (other instanceof ConfigSubstitution) { + return canEqual(other) + && this.pieces.equals(((ConfigSubstitution) other).pieces); + } else { + return false; + } + } + + @Override + public int hashCode() { + // note that "origin" is deliberately NOT part of equality + return pieces.hashCode(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("SUBST"); + sb.append("("); + for (Object p : pieces) { + sb.append(p.toString()); + sb.append(","); + } + 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 new file mode 100644 index 0000000000..fff540e1bf --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ConfigUtil.java @@ -0,0 +1,118 @@ +package com.typesafe.config.impl; + + +/** This is public just for the "config" package to use, don't touch it */ +final public class ConfigUtil { + static boolean equalsHandlingNull(Object a, Object b) { + if (a == null && b != null) + return false; + else if (a != null && b == null) + return false; + else if (a == b) // catches null == null plus optimizes identity case + return true; + else + return a.equals(b); + } + + static String renderJsonString(String s) { + StringBuilder sb = new StringBuilder(); + sb.append('"'); + for (int i = 0; i < s.length(); ++i) { + char c = s.charAt(i); + switch (c) { + case '"': + sb.append("\\\""); + break; + case '\\': + sb.append("\\\\"); + break; + case '\n': + sb.append("\\n"); + break; + case '\b': + sb.append("\\b"); + break; + case '\f': + sb.append("\\f"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + default: + if (Character.isISOControl(c)) + sb.append(String.format("\\u%04x", (int) c)); + else + sb.append(c); + } + } + sb.append('"'); + return sb.toString(); + } + + static boolean isWhitespace(int codepoint) { + switch (codepoint) { + // try to hit the most common ASCII ones first, then the nonbreaking + // spaces that Java brokenly leaves out of isWhitespace. + case ' ': + case '\n': + case '\u00A0': + case '\u2007': + case '\u202F': + return true; + default: + return Character.isWhitespace(codepoint); + } + } + + /** This is public just for the "config" package to use, don't touch it! */ + public static String unicodeTrim(String s) { + // this is dumb because it looks like there aren't any whitespace + // characters that need surrogate encoding. But, points for + // pedantic correctness! It's future-proof or something. + // String.trim() actually is broken, since there are plenty of + // non-ASCII whitespace characters. + final int length = s.length(); + if (length == 0) + return s; + + int start = 0; + while (start < length) { + char c = s.charAt(start); + if (c == ' ' || c == '\n') { + start += 1; + } else { + int cp = s.codePointAt(start); + if (isWhitespace(cp)) + start += Character.charCount(cp); + else + break; + } + } + + int end = length; + while (end > start) { + char c = s.charAt(end - 1); + if (c == ' ' || c == '\n') { + --end; + } else { + int cp; + int delta; + if (Character.isLowSurrogate(c)) { + cp = s.codePointAt(end - 2); + delta = 2; + } else { + cp = s.codePointAt(end - 1); + delta = 1; + } + if (isWhitespace(cp)) + end -= delta; + else + break; + } + } + return s.substring(start, end); + } +} diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/DefaultTransformer.java b/akka-actor/src/main/java/com/typesafe/config/impl/DefaultTransformer.java new file mode 100644 index 0000000000..62913a1c3b --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/DefaultTransformer.java @@ -0,0 +1,78 @@ +package com.typesafe.config.impl; + +import com.typesafe.config.ConfigValueType; + +/** + * Default automatic type transformations. + */ +final class DefaultTransformer { + + static AbstractConfigValue transform(AbstractConfigValue value, + ConfigValueType requested) { + if (value.valueType() == ConfigValueType.STRING) { + String s = (String) value.unwrapped(); + switch (requested) { + case NUMBER: + try { + Long v = Long.parseLong(s); + return new ConfigLong(value.origin(), v, s); + } catch (NumberFormatException e) { + // try Double + } + try { + Double v = Double.parseDouble(s); + return new ConfigDouble(value.origin(), v, s); + } catch (NumberFormatException e) { + // oh well. + } + break; + case NULL: + if (s.equals("null")) + return new ConfigNull(value.origin()); + break; + case BOOLEAN: + if (s.equals("true") || s.equals("yes") || s.equals("on")) { + return new ConfigBoolean(value.origin(), true); + } else if (s.equals("false") || s.equals("no") + || s.equals("off")) { + return new ConfigBoolean(value.origin(), false); + } + break; + case LIST: + // can't go STRING to LIST automatically + break; + case OBJECT: + // can't go STRING to OBJECT automatically + break; + case STRING: + // no-op STRING to STRING + break; + } + } else if (requested == ConfigValueType.STRING) { + // if we converted null to string here, then you wouldn't properly + // get a missing-value error if you tried to get a null value + // as a string. + switch (value.valueType()) { + case NUMBER: // FALL THROUGH + case BOOLEAN: + return new ConfigString(value.origin(), + value.transformToString()); + case NULL: + // want to be sure this throws instead of returning "null" as a + // string + break; + case OBJECT: + // no OBJECT to STRING automatically + break; + case LIST: + // no LIST to STRING automatically + break; + case STRING: + // no-op STRING to STRING + break; + } + } + + return value; + } +} diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/FromMapMode.java b/akka-actor/src/main/java/com/typesafe/config/impl/FromMapMode.java new file mode 100644 index 0000000000..0a646b2e90 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/FromMapMode.java @@ -0,0 +1,5 @@ +package com.typesafe.config.impl; + +enum FromMapMode { + KEYS_ARE_PATHS, KEYS_ARE_KEYS +} 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 new file mode 100644 index 0000000000..f97bb8f5f0 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/Parseable.java @@ -0,0 +1,525 @@ +package com.typesafe.config.impl; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FilterReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Iterator; +import java.util.Properties; + +import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigIncludeContext; +import com.typesafe.config.ConfigObject; +import com.typesafe.config.ConfigOrigin; +import com.typesafe.config.ConfigParseOptions; +import com.typesafe.config.ConfigParseable; +import com.typesafe.config.ConfigSyntax; +import com.typesafe.config.ConfigValue; + +/** + * This is public but it's only for use by the config package; DO NOT TOUCH. The + * point of this class is to avoid "propagating" each overload on + * "thing which can be parsed" through multiple interfaces. Most interfaces can + * have just one overload that takes a Parseable. Also it's used as an abstract + * "resource handle" in the ConfigIncluder interface. + */ +public abstract class Parseable implements ConfigParseable { + private ConfigIncludeContext includeContext; + private ConfigParseOptions initialOptions; + + protected Parseable() { + + } + + private ConfigParseOptions fixupOptions(ConfigParseOptions baseOptions) { + ConfigSyntax syntax = baseOptions.getSyntax(); + if (syntax == null) { + syntax = guessSyntax(); + } + if (syntax == null) { + syntax = ConfigSyntax.CONF; + } + ConfigParseOptions modified = baseOptions.setSyntax(syntax); + + if (modified.getOriginDescription() == null) + modified = modified.setOriginDescription(originDescription()); + + modified = modified.appendIncluder(ConfigImpl.defaultIncluder()); + + return modified; + } + + protected void postConstruct(ConfigParseOptions baseOptions) { + this.initialOptions = fixupOptions(baseOptions); + + this.includeContext = new ConfigIncludeContext() { + @Override + public ConfigParseable relativeTo(String filename) { + return Parseable.this.relativeTo(filename); + } + }; + } + + // the general idea is that any work should be in here, not in the + // constructor, + // so that exceptions are thrown from the public parse() function and not + // from the creation of the Parseable. Essentially this is a lazy field. + // The parser should close the reader when it's done with it. + // ALSO, IMPORTANT: if the file or URL is not found, this must throw. + // to support the "allow missing" feature. + protected abstract Reader reader() throws IOException; + + ConfigSyntax guessSyntax() { + return null; + } + + ConfigParseable relativeTo(String filename) { + return null; + } + + ConfigIncludeContext includeContext() { + return includeContext; + } + + static AbstractConfigObject forceParsedToObject(ConfigValue value) { + if (value instanceof AbstractConfigObject) { + return (AbstractConfigObject) value; + } else { + throw new ConfigException.WrongType(value.origin(), "", + "object at file root", value.valueType().name()); + } + } + + @Override + public ConfigObject parse(ConfigParseOptions baseOptions) { + return forceParsedToObject(parseValue(baseOptions)); + } + + AbstractConfigValue parseValue(ConfigParseOptions baseOptions) { + // note that we are NOT using our "options" and "origin" fields, + // 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()); + return parseValue(origin, options); + } + + protected 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(); + } + } catch (IOException e) { + if (finalOptions.getAllowMissing()) { + return SimpleConfigObject.emptyMissing(origin); + } else { + throw new ConfigException.IO(origin, e.getMessage(), e); + } + } + } + + public ConfigObject parse() { + return forceParsedToObject(parseValue(options())); + } + + AbstractConfigValue parseValue() { + return parseValue(options()); + } + + abstract String originDescription(); + + @Override + public URL url() { + return null; + } + + @Override + public ConfigParseOptions options() { + return initialOptions; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + + private static ConfigSyntax syntaxFromExtension(String name) { + if (name.endsWith(".json")) + return ConfigSyntax.JSON; + else if (name.endsWith(".conf")) + return ConfigSyntax.CONF; + else if (name.endsWith(".properties")) + return ConfigSyntax.PROPERTIES; + else + return null; + } + + private static Reader readerFromStream(InputStream input) { + try { + // well, this is messed up. If we aren't going to close + // the passed-in InputStream then we have no way to + // close these readers. So maybe we should not have an + // InputStream version, only a Reader version. + Reader reader = new InputStreamReader(input, "UTF-8"); + return new BufferedReader(reader); + } catch (UnsupportedEncodingException e) { + throw new ConfigException.BugOrBroken( + "Java runtime does not support UTF-8", e); + } + } + + private static Reader doNotClose(Reader input) { + return new FilterReader(input) { + @Override + public void close() { + // NOTHING. + } + }; + } + + static URL relativeTo(URL url, String filename) { + // I'm guessing this completely fails on Windows, help wanted + if (new File(filename).isAbsolute()) + return null; + + try { + URI siblingURI = url.toURI(); + URI relative = new URI(filename); + + // this seems wrong, but it's documented that the last + // element of the path in siblingURI gets stripped out, + // so to get something in the same directory as + // siblingURI we just call resolve(). + URL resolved = siblingURI.resolve(relative).toURL(); + + return resolved; + } catch (MalformedURLException e) { + return null; + } catch (URISyntaxException e) { + return null; + } catch (IllegalArgumentException e) { + return null; + } + } + + private final static class ParseableInputStream extends Parseable { + final private InputStream input; + + ParseableInputStream(InputStream input, ConfigParseOptions options) { + this.input = input; + postConstruct(options); + } + + @Override + protected Reader reader() { + return doNotClose(readerFromStream(input)); + } + + @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); + } + + private final static class ParseableReader extends Parseable { + final private Reader reader; + + ParseableReader(Reader reader, ConfigParseOptions options) { + this.reader = reader; + postConstruct(options); + } + + @Override + protected Reader reader() { + return reader; + } + + @Override + String originDescription() { + return "Reader"; + } + } + + /** + * note that we will never close this reader; you have to do it when parsing + * is complete. + */ + public static Parseable newReader(Reader reader, ConfigParseOptions options) { + return new ParseableReader(doNotClose(reader), options); + } + + private final static class ParseableString extends Parseable { + final private String input; + + ParseableString(String input, ConfigParseOptions options) { + this.input = input; + postConstruct(options); + } + + @Override + protected Reader reader() { + return new StringReader(input); + } + + @Override + String originDescription() { + return "String"; + } + } + + public static Parseable newString(String input, ConfigParseOptions options) { + return new ParseableString(input, options); + } + + private final static class ParseableURL extends Parseable { + final private URL input; + + ParseableURL(URL input, ConfigParseOptions options) { + this.input = input; + postConstruct(options); + } + + @Override + protected Reader reader() throws IOException { + InputStream stream = input.openStream(); + return readerFromStream(stream); + } + + @Override + ConfigSyntax guessSyntax() { + return syntaxFromExtension(input.getPath()); + } + + @Override + ConfigParseable relativeTo(String filename) { + URL url = relativeTo(input, filename); + if (url == null) + return null; + return newURL(url, options() + .setOriginDescription(null)); + } + + @Override + String originDescription() { + return input.toExternalForm(); + } + + @Override + public URL url() { + return input; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + input.toExternalForm() + + ")"; + } + } + + public static Parseable newURL(URL input, ConfigParseOptions options) { + return new ParseableURL(input, options); + } + + private final static class ParseableFile extends Parseable { + final private File input; + + ParseableFile(File input, ConfigParseOptions options) { + this.input = input; + postConstruct(options); + } + + @Override + protected Reader reader() throws IOException { + InputStream stream = new FileInputStream(input); + return readerFromStream(stream); + } + + @Override + ConfigSyntax guessSyntax() { + return syntaxFromExtension(input.getName()); + } + + @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) { + return null; + } + } + + @Override + String originDescription() { + return input.getPath(); + } + + @Override + public URL url() { + try { + return input.toURI().toURL(); + } catch (MalformedURLException e) { + return null; + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + input.getPath() + ")"; + } + } + + public static Parseable newFile(File input, ConfigParseOptions options) { + return new ParseableFile(input, options); + } + + private final static class ParseableResource extends Parseable { + final private Class klass; + final private String resource; + + ParseableResource(Class klass, String resource, + ConfigParseOptions options) { + this.klass = klass; + 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); + } + return readerFromStream(stream); + } + + @Override + ConfigSyntax guessSyntax() { + return syntaxFromExtension(resource); + } + + @Override + ConfigParseable relativeTo(String filename) { + // not using File.isAbsolute because resource paths always use '/' + // (?) + if (filename.startsWith("/")) + return null; + + // 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(), + options().setOriginDescription(null)); + } + + @Override + String originDescription() { + return resource + " on classpath"; + } + + @Override + public URL url() { + return klass.getResource(resource); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + resource + "," + + klass.getName() + + ")"; + } + } + + public static Parseable newResource(Class klass, String resource, + ConfigParseOptions options) { + return new ParseableResource(klass, resource, options); + } + + private final static class ParseableProperties extends Parseable { + final private Properties props; + + ParseableProperties(Properties props, ConfigParseOptions options) { + this.props = props; + postConstruct(options); + } + + @Override + protected Reader reader() throws IOException { + throw new ConfigException.BugOrBroken( + "reader() should not be called on props"); + } + + @Override + protected AbstractConfigObject parseValue(ConfigOrigin origin, + ConfigParseOptions finalOptions) { + return PropertiesParser.fromProperties(origin, props); + } + + @Override + ConfigSyntax guessSyntax() { + return ConfigSyntax.PROPERTIES; + } + + @Override + String originDescription() { + return "properties"; + } + + @Override + public URL url() { + return null; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + props.size() + " props)"; + } + } + + public static Parseable newProperties(Properties properties, + ConfigParseOptions options) { + return new ParseableProperties(properties, options); + } +} 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 new file mode 100644 index 0000000000..b0c68b10ac --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/Parser.java @@ -0,0 +1,738 @@ +package com.typesafe.config.impl; + +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Stack; + +import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigIncludeContext; +import com.typesafe.config.ConfigIncluder; +import com.typesafe.config.ConfigOrigin; +import com.typesafe.config.ConfigParseOptions; +import com.typesafe.config.ConfigSyntax; +import com.typesafe.config.ConfigValueType; + +final class Parser { + + static AbstractConfigValue parse(Iterator tokens, + ConfigOrigin origin, ConfigParseOptions options, + ConfigIncludeContext includeContext) { + ParseContext context = new ParseContext(options.getSyntax(), origin, + tokens, options.getIncluder(), includeContext); + return context.parse(); + } + + static private final class ParseContext { + private int lineNumber; + final private Stack buffer; + final private Iterator tokens; + final private ConfigIncluder includer; + final private ConfigIncludeContext includeContext; + final private ConfigSyntax flavor; + final private ConfigOrigin baseOrigin; + final private LinkedList pathStack; + + ParseContext(ConfigSyntax flavor, ConfigOrigin origin, + Iterator tokens, ConfigIncluder includer, + ConfigIncludeContext includeContext) { + lineNumber = 1; + buffer = new Stack(); + this.tokens = tokens; + this.flavor = flavor; + this.baseOrigin = origin; + this.includer = includer; + this.includeContext = includeContext; + this.pathStack = new LinkedList(); + } + + private Token nextToken() { + Token t = null; + if (buffer.isEmpty()) { + t = tokens.next(); + } else { + t = buffer.pop(); + } + + if (flavor == ConfigSyntax.JSON) { + if (Tokens.isUnquotedText(t)) { + throw parseError("Token not allowed in valid JSON: '" + + Tokens.getUnquotedText(t) + "'"); + } else if (Tokens.isSubstitution(t)) { + throw parseError("Substitutions (${} syntax) not allowed in JSON"); + } + } + + return t; + } + + private void putBack(Token token) { + buffer.push(token); + } + + private Token nextTokenIgnoringNewline() { + Token t = nextToken(); + while (Tokens.isNewline(t)) { + // line number tokens have the line that was _ended_ by the + // newline, so we have to add one. + lineNumber = Tokens.getLineNumber(t) + 1; + t = nextToken(); + } + return t; + } + + // In arrays and objects, comma can be omitted + // as long as there's at least one newline instead. + // this skips any newlines in front of a comma, + // skips the comma, and returns true if it found + // either a newline or a comma. The iterator + // is left just after the comma or the newline. + private boolean checkElementSeparator() { + if (flavor == ConfigSyntax.JSON) { + Token t = nextTokenIgnoringNewline(); + if (t == Tokens.COMMA) { + return true; + } else { + putBack(t); + return false; + } + } else { + boolean sawSeparatorOrNewline = false; + Token t = nextToken(); + while (true) { + if (Tokens.isNewline(t)) { + lineNumber = Tokens.getLineNumber(t); + sawSeparatorOrNewline = true; + // we want to continue to also eat + // a comma if there is one. + } else if (t == Tokens.COMMA) { + return true; + } else { + // non-newline-or-comma + putBack(t); + return sawSeparatorOrNewline; + } + t = nextToken(); + } + } + } + + // merge a bunch of adjacent values into one + // value; change unquoted text into a string + // value. + private void consolidateValueTokens() { + // this trick is not done in JSON + if (flavor == ConfigSyntax.JSON) + return; + + List values = null; // create only if we have value tokens + Token t = nextTokenIgnoringNewline(); // ignore a newline up front + while (Tokens.isValue(t) || Tokens.isUnquotedText(t) + || Tokens.isSubstitution(t)) { + if (values == null) + values = new ArrayList(); + values.add(t); + t = nextToken(); // but don't consolidate across a newline + } + // the last one wasn't a value token + putBack(t); + + if (values == null) + return; + + if (values.size() == 1 && Tokens.isValue(values.get(0))) { + // a single value token requires no consolidation + putBack(values.get(0)); + return; + } + + // this will be a list of String and Path + List minimized = new ArrayList(); + + // we have multiple value tokens or one unquoted text token; + // collapse into a string token. + StringBuilder sb = new StringBuilder(); + ConfigOrigin firstOrigin = null; + for (Token valueToken : values) { + if (Tokens.isValue(valueToken)) { + AbstractConfigValue v = Tokens.getValue(valueToken); + sb.append(v.transformToString()); + if (firstOrigin == null) + firstOrigin = v.origin(); + } else if (Tokens.isUnquotedText(valueToken)) { + String text = Tokens.getUnquotedText(valueToken); + if (firstOrigin == null) + firstOrigin = Tokens.getUnquotedTextOrigin(valueToken); + sb.append(text); + } else if (Tokens.isSubstitution(valueToken)) { + if (firstOrigin == null) + firstOrigin = Tokens.getSubstitutionOrigin(valueToken); + + if (sb.length() > 0) { + // save string so far + minimized.add(sb.toString()); + sb.setLength(0); + } + // now save substitution + List expression = Tokens + .getSubstitutionPathExpression(valueToken); + Path path = parsePathExpression(expression.iterator(), + Tokens.getSubstitutionOrigin(valueToken)); + minimized.add(path); + } else { + throw new ConfigException.BugOrBroken( + "should not be trying to consolidate token: " + + valueToken); + } + } + + if (sb.length() > 0) { + // save string so far + minimized.add(sb.toString()); + } + + if (minimized.isEmpty()) + throw new ConfigException.BugOrBroken( + "trying to consolidate values to nothing"); + + Token consolidated = null; + + if (minimized.size() == 1 && minimized.get(0) instanceof String) { + consolidated = Tokens.newString(firstOrigin, + (String) minimized.get(0)); + } else { + // there's some substitution to do later (post-parse step) + consolidated = Tokens.newValue(new ConfigSubstitution( + firstOrigin, minimized)); + } + + putBack(consolidated); + } + + private ConfigOrigin lineOrigin() { + return new SimpleConfigOrigin(baseOrigin.description() + ": line " + + lineNumber); + } + + private ConfigException parseError(String message) { + return parseError(message, null); + } + + private ConfigException parseError(String message, Throwable cause) { + return new ConfigException.Parse(lineOrigin(), message, cause); + } + + private AbstractConfigValue parseValue(Token token) { + if (Tokens.isValue(token)) { + return Tokens.getValue(token); + } else if (token == Tokens.OPEN_CURLY) { + return parseObject(true); + } else if (token == Tokens.OPEN_SQUARE) { + return parseArray(); + } else { + throw parseError("Expecting a value but got wrong token: " + + token); + } + } + + private static AbstractConfigObject createValueUnderPath(Path path, + AbstractConfigValue value) { + // for path foo.bar, we are creating + // { "foo" : { "bar" : value } } + List keys = new ArrayList(); + + String key = path.first(); + Path remaining = path.remainder(); + while (key != null) { + keys.add(key); + if (remaining == null) { + break; + } else { + key = remaining.first(); + remaining = remaining.remainder(); + } + } + ListIterator i = keys.listIterator(keys.size()); + String deepest = i.previous(); + AbstractConfigObject o = new SimpleConfigObject(value.origin(), + Collections. singletonMap( + deepest, value)); + while (i.hasPrevious()) { + Map m = Collections. singletonMap( + i.previous(), o); + o = new SimpleConfigObject(value.origin(), m); + } + + return o; + } + + private Path parseKey(Token token) { + if (flavor == ConfigSyntax.JSON) { + if (Tokens.isValueWithType(token, ConfigValueType.STRING)) { + String key = (String) Tokens.getValue(token).unwrapped(); + return Path.newKey(key); + } else { + throw parseError("Expecting close brace } or a field name, got " + + token); + } + } else { + List expression = new ArrayList(); + Token t = token; + while (Tokens.isValue(t) || Tokens.isUnquotedText(t)) { + expression.add(t); + t = nextToken(); // note: don't cross a newline + } + putBack(t); // put back the token we ended with + return parsePathExpression(expression.iterator(), lineOrigin()); + } + } + + private static boolean isIncludeKeyword(Token t) { + return Tokens.isUnquotedText(t) + && Tokens.getUnquotedText(t).equals("include"); + } + + private static boolean isUnquotedWhitespace(Token t) { + if (!Tokens.isUnquotedText(t)) + return false; + + String s = Tokens.getUnquotedText(t); + + for (int i = 0; i < s.length(); ++i) { + char c = s.charAt(i); + if (!ConfigUtil.isWhitespace(c)) + return false; + } + return true; + } + + private void parseInclude(Map values) { + Token t = nextTokenIgnoringNewline(); + while (isUnquotedWhitespace(t)) { + t = nextTokenIgnoringNewline(); + } + + if (Tokens.isValueWithType(t, ConfigValueType.STRING)) { + String name = (String) Tokens.getValue(t).unwrapped(); + AbstractConfigObject obj = (AbstractConfigObject) includer + .include(includeContext, name); + + if (!pathStack.isEmpty()) { + Path prefix = new Path(pathStack); + obj = obj.relativized(prefix); + } + + for (String key : obj.keySet()) { + AbstractConfigValue v = obj.get(key); + AbstractConfigValue existing = values.get(key); + if (existing != null) { + values.put(key, v.withFallback(existing)); + } else { + values.put(key, v); + } + } + + } else { + throw parseError("include keyword is not followed by a quoted string, but by: " + + t); + } + } + + private boolean isKeyValueSeparatorToken(Token t) { + if (flavor == ConfigSyntax.JSON) { + return t == Tokens.COLON; + } else { + return t == Tokens.COLON || t == Tokens.EQUALS; + } + } + + private AbstractConfigObject parseObject(boolean hadOpenCurly) { + // invoked just after the OPEN_CURLY (or START, if !hadOpenCurly) + Map values = new HashMap(); + ConfigOrigin objectOrigin = lineOrigin(); + boolean afterComma = false; + while (true) { + Token t = nextTokenIgnoringNewline(); + if (t == Tokens.CLOSE_CURLY) { + if (flavor == ConfigSyntax.JSON && afterComma) { + throw parseError("expecting a field name after comma, got a close brace }"); + } else if (!hadOpenCurly) { + throw parseError("unbalanced close brace '}' with no open brace"); + } + break; + } else if (t == Tokens.END && !hadOpenCurly) { + putBack(t); + break; + } else if (flavor != ConfigSyntax.JSON && isIncludeKeyword(t)) { + parseInclude(values); + + afterComma = false; + } else { + Path path = parseKey(t); + Token afterKey = nextTokenIgnoringNewline(); + + // path must be on-stack while we parse the value + pathStack.push(path); + + Token valueToken; + AbstractConfigValue newValue; + if (flavor == ConfigSyntax.CONF + && afterKey == Tokens.OPEN_CURLY) { + // can omit the ':' or '=' before an object value + valueToken = afterKey; + newValue = parseObject(true); + } else { + if (!isKeyValueSeparatorToken(afterKey)) { + throw parseError("Key may not be followed by token: " + + afterKey); + } + + consolidateValueTokens(); + valueToken = nextTokenIgnoringNewline(); + newValue = parseValue(valueToken); + } + + pathStack.pop(); + + String key = path.first(); + Path remaining = path.remainder(); + + if (remaining == null) { + AbstractConfigValue existing = values.get(key); + if (existing != null) { + // In strict JSON, dups should be an error; while in + // our custom config language, they should be merged + // if the value is an object (or substitution that + // could become an object). + + if (flavor == ConfigSyntax.JSON) { + throw parseError("JSON does not allow duplicate fields: '" + + key + + "' was already seen at " + + existing.origin().description()); + } else { + newValue = newValue.withFallback(existing); + } + } + values.put(key, newValue); + } else { + if (flavor == ConfigSyntax.JSON) { + throw new ConfigException.BugOrBroken( + "somehow got multi-element path in JSON mode"); + } + + AbstractConfigObject obj = createValueUnderPath( + remaining, newValue); + AbstractConfigValue existing = values.get(key); + if (existing != null) { + obj = obj.withFallback(existing); + } + values.put(key, obj); + } + + afterComma = false; + } + + if (checkElementSeparator()) { + // continue looping + afterComma = true; + } else { + t = nextTokenIgnoringNewline(); + if (t == Tokens.CLOSE_CURLY) { + if (!hadOpenCurly) { + throw parseError("unbalanced close brace '}' with no open brace"); + } + break; + } else if (hadOpenCurly) { + throw parseError("Expecting close brace } or a comma, got " + + t); + } else { + if (t == Tokens.END) { + putBack(t); + break; + } else { + throw parseError("Expecting end of input or a comma, got " + + t); + } + } + } + } + return new SimpleConfigObject(objectOrigin, + values); + } + + private SimpleConfigList parseArray() { + // invoked just after the OPEN_SQUARE + ConfigOrigin arrayOrigin = lineOrigin(); + List values = new ArrayList(); + + consolidateValueTokens(); + + Token t = nextTokenIgnoringNewline(); + + // special-case the first element + if (t == Tokens.CLOSE_SQUARE) { + return new SimpleConfigList(arrayOrigin, + Collections. emptyList()); + } else if (Tokens.isValue(t)) { + values.add(parseValue(t)); + } else if (t == Tokens.OPEN_CURLY) { + values.add(parseObject(true)); + } else if (t == Tokens.OPEN_SQUARE) { + values.add(parseArray()); + } else { + throw parseError("List should have ] or a first element after the open [, instead had token: " + + t); + } + + // now remaining elements + while (true) { + // just after a value + if (checkElementSeparator()) { + // comma (or newline equivalent) consumed + } else { + t = nextTokenIgnoringNewline(); + if (t == Tokens.CLOSE_SQUARE) { + return new SimpleConfigList(arrayOrigin, values); + } else { + throw parseError("List should have ended with ] or had a comma, instead had token: " + + t); + } + } + + // now just after a comma + consolidateValueTokens(); + + t = nextTokenIgnoringNewline(); + if (Tokens.isValue(t)) { + values.add(parseValue(t)); + } else if (t == Tokens.OPEN_CURLY) { + values.add(parseObject(true)); + } else if (t == Tokens.OPEN_SQUARE) { + values.add(parseArray()); + } else if (flavor != ConfigSyntax.JSON + && t == Tokens.CLOSE_SQUARE) { + // we allow one trailing comma + putBack(t); + } else { + throw parseError("List should have had new element after a comma, instead had token: " + + t); + } + } + } + + AbstractConfigValue parse() { + Token t = nextTokenIgnoringNewline(); + if (t == Tokens.START) { + // OK + } else { + throw new ConfigException.BugOrBroken( + "token stream did not begin with START, had " + t); + } + + t = nextTokenIgnoringNewline(); + AbstractConfigValue result = null; + if (t == Tokens.OPEN_CURLY) { + result = parseObject(true); + } else if (t == Tokens.OPEN_SQUARE) { + result = parseArray(); + } else { + if (flavor == ConfigSyntax.JSON) { + if (t == Tokens.END) { + throw parseError("Empty document"); + } else { + throw parseError("Document must have an object or array at root, unexpected token: " + + t); + } + } else { + // the root object can omit the surrounding braces. + // this token should be the first field's key, or part + // of it, so put it back. + putBack(t); + result = parseObject(false); + } + } + + t = nextTokenIgnoringNewline(); + if (t == Tokens.END) { + return result; + } else { + throw parseError("Document has trailing tokens after first object or array: " + + t); + } + } + } + + static class Element { + StringBuilder sb; + // an element can be empty if it has a quoted empty string "" in it + boolean canBeEmpty; + + Element(String initial, boolean canBeEmpty) { + this.canBeEmpty = canBeEmpty; + this.sb = new StringBuilder(initial); + } + + @Override + public String toString() { + return "Element(" + sb.toString() + "," + canBeEmpty + ")"; + } + } + + private static void addPathText(List buf, boolean wasQuoted, + String newText) { + int i = wasQuoted ? -1 : newText.indexOf('.'); + Element current = buf.get(buf.size() - 1); + if (i < 0) { + // add to current path element + current.sb.append(newText); + // any empty quoted string means this element can + // now be empty. + if (wasQuoted && current.sb.length() == 0) + current.canBeEmpty = true; + } else { + // "buf" plus up to the period is an element + current.sb.append(newText.substring(0, i)); + // then start a new element + buf.add(new Element("", false)); + // recurse to consume remainder of newText + addPathText(buf, false, newText.substring(i + 1)); + } + } + + private static Path parsePathExpression(Iterator expression, + ConfigOrigin origin) { + return parsePathExpression(expression, origin, null); + } + + // originalText may be null if not available + private static Path parsePathExpression(Iterator expression, + ConfigOrigin origin, String originalText) { + // each builder in "buf" is an element in the path. + List buf = new ArrayList(); + buf.add(new Element("", false)); + + if (!expression.hasNext()) { + throw new ConfigException.BadPath(origin, originalText, + "Expecting a field name or path here, but got nothing"); + } + + while (expression.hasNext()) { + Token t = expression.next(); + if (Tokens.isValueWithType(t, ConfigValueType.STRING)) { + AbstractConfigValue v = Tokens.getValue(t); + // this is a quoted string; so any periods + // in here don't count as path separators + String s = v.transformToString(); + + addPathText(buf, true, s); + } else if (t == Tokens.END) { + // ignore this; when parsing a file, it should not happen + // since we're parsing a token list rather than the main + // token iterator, and when parsing a path expression from the + // API, it's expected to have an END. + } else { + // any periods outside of a quoted string count as + // separators + String text; + if (Tokens.isValue(t)) { + // appending a number here may add + // a period, but we _do_ count those as path + // separators, because we basically want + // "foo 3.0bar" to parse as a string even + // though there's a number in it. The fact that + // we tokenize non-string values is largely an + // implementation detail. + AbstractConfigValue v = Tokens.getValue(t); + text = v.transformToString(); + } else if (Tokens.isUnquotedText(t)) { + text = Tokens.getUnquotedText(t); + } else { + throw new ConfigException.BadPath(origin, originalText, + "Token not allowed in path expression: " + + t); + } + + addPathText(buf, false, text); + } + } + + PathBuilder pb = new PathBuilder(); + for (Element e : buf) { + if (e.sb.length() == 0 && !e.canBeEmpty) { + throw new ConfigException.BadPath( + origin, + originalText, + "path has a leading, trailing, or two adjacent period '.' (use quoted \"\" empty string if you want an empty element)"); + } else { + pb.appendKey(e.sb.toString()); + } + } + + return pb.result(); + } + + static ConfigOrigin apiOrigin = new SimpleConfigOrigin("path parameter"); + + static Path parsePath(String path) { + Path speculated = speculativeFastParsePath(path); + if (speculated != null) + return speculated; + + StringReader reader = new StringReader(path); + + try { + Iterator tokens = Tokenizer.tokenize(apiOrigin, reader, + ConfigSyntax.CONF); + tokens.next(); // drop START + return parsePathExpression(tokens, apiOrigin, path); + } finally { + reader.close(); + } + } + + // the idea is to see if the string has any chars that might require the + // full parser to deal with. + private static boolean hasUnsafeChars(String s) { + for (int i = 0; i < s.length(); ++i) { + char c = s.charAt(i); + if (Character.isLetter(c) || c == '.') + continue; + else + return true; + } + return false; + } + + private static void appendPathString(PathBuilder pb, String s) { + int splitAt = s.indexOf('.'); + if (splitAt < 0) { + pb.appendKey(s); + } else { + pb.appendKey(s.substring(0, splitAt)); + appendPathString(pb, s.substring(splitAt + 1)); + } + } + + // do something much faster than the full parser if + // we just have something like "foo" or "foo.bar" + private static Path speculativeFastParsePath(String path) { + String s = ConfigUtil.unicodeTrim(path); + if (s.isEmpty()) + return null; + if (hasUnsafeChars(s)) + return null; + if (s.startsWith(".") || s.endsWith(".") || s.contains("..")) + return null; // let the full parser throw the error + + PathBuilder pb = new PathBuilder(); + appendPathString(pb, s); + return pb.result(); + } +} 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 new file mode 100644 index 0000000000..cf6d959f32 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/Path.java @@ -0,0 +1,188 @@ +package com.typesafe.config.impl; + +import java.util.Iterator; +import java.util.List; + +import com.typesafe.config.ConfigException; + +final class Path { + + final private String first; + final private Path remainder; + + Path(String first, Path remainder) { + this.first = first; + this.remainder = remainder; + } + + Path(String... elements) { + if (elements.length == 0) + throw new ConfigException.BugOrBroken("empty path"); + this.first = elements[0]; + if (elements.length > 1) { + PathBuilder pb = new PathBuilder(); + for (int i = 1; i < elements.length; ++i) { + pb.appendKey(elements[i]); + } + this.remainder = pb.result(); + } else { + this.remainder = null; + } + } + + // append all the paths in the list together into one path + Path(List pathsToConcat) { + if (pathsToConcat.isEmpty()) + throw new ConfigException.BugOrBroken("empty path"); + + Iterator i = pathsToConcat.iterator(); + Path firstPath = i.next(); + this.first = firstPath.first; + + PathBuilder pb = new PathBuilder(); + if (firstPath.remainder != null) { + pb.appendPath(firstPath.remainder); + } + while (i.hasNext()) { + pb.appendPath(i.next()); + } + this.remainder = pb.result(); + } + + String first() { + return first; + } + + /** + * + * @return path minus the first element or null if no more elements + */ + Path remainder() { + return remainder; + } + + /** + * + * @return path minus the last element or null if we have just one element + */ + Path parent() { + if (remainder == null) + return null; + + PathBuilder pb = new PathBuilder(); + Path p = this; + while (p.remainder != null) { + pb.appendKey(p.first); + p = p.remainder; + } + return pb.result(); + } + + /** + * + * @return last element in the path + */ + String last() { + Path p = this; + while (p.remainder != null) { + p = p.remainder; + } + return p.first; + } + + Path prepend(Path toPrepend) { + PathBuilder pb = new PathBuilder(); + pb.appendPath(toPrepend); + pb.appendPath(this); + return pb.result(); + } + + int length() { + int count = 1; + Path p = remainder; + while (p != null) { + count += 1; + p = p.remainder; + } + return count; + } + + Path subPath(int removeFromFront) { + int count = removeFromFront; + Path p = this; + while (p != null && count > 0) { + count -= 1; + p = p.remainder; + } + return p; + } + + @Override + public boolean equals(Object other) { + if (other instanceof Path) { + Path that = (Path) other; + return this.first.equals(that.first) + && ConfigUtil.equalsHandlingNull(this.remainder, + that.remainder); + } else { + return false; + } + } + + @Override + public int hashCode() { + return 41 * (41 + first.hashCode()) + + (remainder == null ? 0 : remainder.hashCode()); + } + + // this doesn't have a very precise meaning, just to reduce + // noise from quotes in the rendered path + static boolean hasFunkyChars(String s) { + for (int i = 0; i < s.length(); ++i) { + char c = s.charAt(i); + if (Character.isLetterOrDigit(c) || c == ' ') + continue; + else + return true; + } + return false; + } + + private void appendToStringBuilder(StringBuilder sb) { + if (hasFunkyChars(first)) + sb.append(ConfigUtil.renderJsonString(first)); + else + sb.append(first); + if (remainder != null) { + sb.append("."); + remainder.appendToStringBuilder(sb); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Path("); + appendToStringBuilder(sb); + sb.append(")"); + return sb.toString(); + } + + /** + * toString() is a debugging-oriented version while this is an + * error-message-oriented human-readable one. + */ + String render() { + StringBuilder sb = new StringBuilder(); + appendToStringBuilder(sb); + return sb.toString(); + } + + static Path newKey(String key) { + return new Path(key, null); + } + + static Path newPath(String path) { + return Parser.parsePath(path); + } +} diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/PathBuilder.java b/akka-actor/src/main/java/com/typesafe/config/impl/PathBuilder.java new file mode 100644 index 0000000000..3f3643e8ad --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/PathBuilder.java @@ -0,0 +1,57 @@ +package com.typesafe.config.impl; + +import java.util.Stack; + +import com.typesafe.config.ConfigException; + +final class PathBuilder { + // the keys are kept "backward" (top of stack is end of path) + final private Stack keys; + private Path result; + + PathBuilder() { + keys = new Stack(); + } + + private void checkCanAppend() { + if (result != null) + throw new ConfigException.BugOrBroken( + "Adding to PathBuilder after getting result"); + } + + void appendKey(String key) { + checkCanAppend(); + + keys.push(key); + } + + void appendPath(Path path) { + checkCanAppend(); + + String first = path.first(); + Path remainder = path.remainder(); + while (true) { + keys.push(first); + if (remainder != null) { + first = remainder.first(); + remainder = remainder.remainder(); + } else { + break; + } + } + } + + Path result() { + // note: if keys is empty, we want to return null, which is a valid + // empty path + if (result == null) { + Path remainder = null; + while (!keys.isEmpty()) { + String key = keys.pop(); + remainder = new Path(key, remainder); + } + result = remainder; + } + return result; + } +} diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/PropertiesParser.java b/akka-actor/src/main/java/com/typesafe/config/impl/PropertiesParser.java new file mode 100644 index 0000000000..c73293d9c4 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/PropertiesParser.java @@ -0,0 +1,187 @@ +package com.typesafe.config.impl; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigOrigin; + +final class PropertiesParser { + static AbstractConfigObject parse(Reader reader, + ConfigOrigin origin) throws IOException { + Properties props = new Properties(); + props.load(reader); + return fromProperties(origin, props); + } + + static String lastElement(String path) { + int i = path.lastIndexOf('.'); + if (i < 0) + return path; + else + return path.substring(i + 1); + } + + static String exceptLastElement(String path) { + int i = path.lastIndexOf('.'); + if (i < 0) + return null; + else + return path.substring(0, i); + } + + static Path pathFromPropertyKey(String key) { + String last = lastElement(key); + String exceptLast = exceptLastElement(key); + Path path = new Path(last, null); + while (exceptLast != null) { + last = lastElement(exceptLast); + exceptLast = exceptLastElement(exceptLast); + path = new Path(last, path); + } + return path; + } + + static AbstractConfigObject fromProperties(ConfigOrigin origin, + Properties props) { + Map pathMap = new HashMap(); + for (Map.Entry entry : props.entrySet()) { + Object key = entry.getKey(); + if (key instanceof String) { + Path path = pathFromPropertyKey((String) key); + pathMap.put(path, entry.getValue()); + } + } + return fromPathMap(origin, pathMap, true /* from properties */); + } + + static AbstractConfigObject fromPathMap(ConfigOrigin origin, + Map pathExpressionMap) { + Map pathMap = new HashMap(); + for (Map.Entry entry : pathExpressionMap.entrySet()) { + Object keyObj = entry.getKey(); + if (!(keyObj instanceof String)) { + throw new ConfigException.BugOrBroken( + "Map has a non-string as a key, expecting a path expression as a String"); + } + Path path = Path.newPath((String) keyObj); + pathMap.put(path, entry.getValue()); + } + return fromPathMap(origin, pathMap, false /* from properties */); + } + + private static AbstractConfigObject fromPathMap(ConfigOrigin origin, + Map pathMap, boolean convertedFromProperties) { + /* + * First, build a list of paths that will have values, either string or + * object values. + */ + Set scopePaths = new HashSet(); + Set valuePaths = new HashSet(); + for (Path path : pathMap.keySet()) { + // add value's path + valuePaths.add(path); + + // all parent paths are objects + Path next = path.parent(); + while (next != null) { + scopePaths.add(next); + next = next.parent(); + } + } + + if (convertedFromProperties) { + /* + * If any string values are also objects containing other values, + * drop those string values - objects "win". + */ + valuePaths.removeAll(scopePaths); + } else { + /* If we didn't start out as properties, then this is an error. */ + for (Path path : valuePaths) { + if (scopePaths.contains(path)) { + throw new ConfigException.BugOrBroken( + "In the map, path '" + + path.render() + + "' occurs as both the parent object of a value and as a value. " + + "Because Map has no defined ordering, this is a broken situation."); + } + } + } + + /* + * Create maps for the object-valued values. + */ + Map root = new HashMap(); + Map> scopes = new HashMap>(); + + for (Path path : scopePaths) { + Map scope = new HashMap(); + scopes.put(path, scope); + } + + /* Store string values in the associated scope maps */ + for (Path path : valuePaths) { + Path parentPath = path.parent(); + Map parent = parentPath != null ? scopes + .get(parentPath) : root; + + String last = path.last(); + Object rawValue = pathMap.get(path); + AbstractConfigValue value; + if (convertedFromProperties) { + value = new ConfigString(origin, (String) rawValue); + } else { + value = ConfigImpl.fromAnyRef(pathMap.get(path), origin, + FromMapMode.KEYS_ARE_PATHS); + } + parent.put(last, value); + } + + /* + * Make a list of scope paths from longest to shortest, so children go + * before parents. + */ + List sortedScopePaths = new ArrayList(); + sortedScopePaths.addAll(scopePaths); + // sort descending by length + Collections.sort(sortedScopePaths, new Comparator() { + @Override + public int compare(Path a, Path b) { + // Path.length() is O(n) so in theory this sucks + // but in practice we can make Path precompute length + // if it ever matters. + return b.length() - a.length(); + } + }); + + /* + * Create ConfigObject for each scope map, working from children to + * parents to avoid modifying any already-created ConfigObject. This is + * where we need the sorted list. + */ + for (Path scopePath : sortedScopePaths) { + Map scope = scopes.get(scopePath); + + Path parentPath = scopePath.parent(); + Map parent = parentPath != null ? scopes + .get(parentPath) : root; + + AbstractConfigObject o = new SimpleConfigObject(origin, scope, + ResolveStatus.RESOLVED); + parent.put(scopePath.last(), o); + } + + // return root config object + return new SimpleConfigObject(origin, root, ResolveStatus.RESOLVED); + } +} diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/ResolveStatus.java b/akka-actor/src/main/java/com/typesafe/config/impl/ResolveStatus.java new file mode 100644 index 0000000000..3b6a7535f6 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/ResolveStatus.java @@ -0,0 +1,23 @@ +package com.typesafe.config.impl; + +import java.util.Collection; + +/** + * Status of substitution resolution. + */ +enum ResolveStatus { + UNRESOLVED, RESOLVED; + + final static ResolveStatus fromValues( + Collection values) { + for (AbstractConfigValue v : values) { + if (v.resolveStatus() == ResolveStatus.UNRESOLVED) + return ResolveStatus.UNRESOLVED; + } + return ResolveStatus.RESOLVED; + } + + final static ResolveStatus fromBoolean(boolean resolved) { + return resolved ? ResolveStatus.RESOLVED : ResolveStatus.UNRESOLVED; + } +} 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 new file mode 100644 index 0000000000..d8f042b961 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/RootConfig.java @@ -0,0 +1,64 @@ +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. + SimpleConfig resolved = resolvedObject(options).toConfig(); + return resolved.asRoot(rootPath); + } + + @Override + public RootConfig withFallback(ConfigMergeable value) { + // this can return "this" if the withFallback does nothing + return super.withFallback(value).asRoot(rootPath); + } + + @Override + public RootConfig withFallbacks(ConfigMergeable... values) { + // this can return "this" if the withFallbacks does nothing + return super.withFallbacks(values).asRoot(rootPath); + } + + 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 new file mode 100644 index 0000000000..02d6351d63 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfig.java @@ -0,0 +1,589 @@ +package com.typesafe.config.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigList; +import com.typesafe.config.ConfigMergeable; +import com.typesafe.config.ConfigObject; +import com.typesafe.config.ConfigOrigin; +import com.typesafe.config.ConfigResolveOptions; +import com.typesafe.config.ConfigValue; +import com.typesafe.config.ConfigValueType; + +/** + * One thing to keep in mind in the future: if any Collection-like APIs are + * added here, including iterators or size() or anything, then we'd have to + * grapple with whether ConfigNull values are "in" the Config (probably not) and + * we'd probably want to make the collection look flat - not like a tree. So the + * key-value pairs would be all the tree's leaf values, in a big flat list with + * their full paths. + */ +class SimpleConfig implements Config { + + AbstractConfigObject object; + + SimpleConfig(AbstractConfigObject object) { + this.object = object; + } + + @Override + public AbstractConfigObject toObject() { + return object; + } + + @Override + public ConfigOrigin origin() { + 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); + } + + // RootConfig overrides this to avoid a new object on unchanged path. + protected RootConfig asRoot(AbstractConfigObject underlying, + Path newRootPath) { + return new RootConfig(underlying, newRootPath); + } + + protected AbstractConfigObject resolvedObject(ConfigResolveOptions options) { + AbstractConfigValue resolved = SubstitutionResolver.resolve(object, + object, options); + return (AbstractConfigObject) resolved; + } + + @Override + public boolean hasPath(String pathExpression) { + Path path = Path.newPath(pathExpression); + ConfigValue peeked = object.peekPath(path, null, 0, null); + return peeked != null && peeked.valueType() != ConfigValueType.NULL; + } + + @Override + public boolean isEmpty() { + return object.isEmpty(); + } + + static private AbstractConfigValue find(AbstractConfigObject self, + String pathExpression, ConfigValueType expected, String originalPath) { + Path path = Path.newPath(pathExpression); + return find(self, path, expected, originalPath); + } + + static private AbstractConfigValue findKey(AbstractConfigObject self, + String key, ConfigValueType expected, String originalPath) { + AbstractConfigValue v = self.peek(key); + if (v == null) + throw new ConfigException.Missing(originalPath); + + if (expected != null) + v = DefaultTransformer.transform(v, expected); + + if (v.valueType() == ConfigValueType.NULL) + throw new ConfigException.Null(v.origin(), originalPath, + expected != null ? expected.name() : null); + else if (expected != null && v.valueType() != expected) + throw new ConfigException.WrongType(v.origin(), originalPath, + expected.name(), v.valueType().name()); + else + return v; + } + + static private AbstractConfigValue find(AbstractConfigObject self, + Path path, ConfigValueType expected, String originalPath) { + String key = path.first(); + Path next = path.remainder(); + if (next == null) { + return findKey(self, key, expected, originalPath); + } else { + AbstractConfigObject o = (AbstractConfigObject) findKey(self, key, + ConfigValueType.OBJECT, originalPath); + assert (o != null); // missing was supposed to throw + return find(o, next, expected, originalPath); + } + } + + AbstractConfigValue find(String pathExpression, ConfigValueType expected, + String originalPath) { + return find(object, pathExpression, expected, originalPath); + } + + @Override + public AbstractConfigValue getValue(String path) { + return find(path, null, path); + } + + @Override + public boolean getBoolean(String path) { + ConfigValue v = find(path, ConfigValueType.BOOLEAN, path); + return (Boolean) v.unwrapped(); + } + + @Override + public Number getNumber(String path) { + ConfigValue v = find(path, ConfigValueType.NUMBER, path); + return (Number) v.unwrapped(); + } + + @Override + public int getInt(String path) { + return getNumber(path).intValue(); + } + + @Override + public long getLong(String path) { + return getNumber(path).longValue(); + } + + @Override + public double getDouble(String path) { + return getNumber(path).doubleValue(); + } + + @Override + public String getString(String path) { + ConfigValue v = find(path, ConfigValueType.STRING, path); + return (String) v.unwrapped(); + } + + @Override + public ConfigList getList(String path) { + AbstractConfigValue v = find(path, ConfigValueType.LIST, path); + return (ConfigList) v; + } + + @Override + public AbstractConfigObject getObject(String path) { + AbstractConfigObject obj = (AbstractConfigObject) find(path, + ConfigValueType.OBJECT, path); + return obj; + } + + @Override + public SimpleConfig getConfig(String path) { + return getObject(path).toConfig(); + } + + @Override + public Object getAnyRef(String path) { + ConfigValue v = find(path, null, path); + return v.unwrapped(); + } + + @Override + public Long getMemorySizeInBytes(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(), + v.origin(), path); + } + return size; + } + + @Override + public Long getMilliseconds(String path) { + long ns = getNanoseconds(path); + long ms = TimeUnit.NANOSECONDS.toMillis(ns); + return ms; + } + + @Override + public Long getNanoseconds(String path) { + Long ns = null; + try { + ns = TimeUnit.MILLISECONDS.toNanos(getLong(path)); + } catch (ConfigException.WrongType e) { + ConfigValue v = find(path, ConfigValueType.STRING, path); + ns = parseDuration((String) v.unwrapped(), v.origin(), path); + } + return ns; + } + + @SuppressWarnings("unchecked") + private List getHomogeneousUnwrappedList(String path, + ConfigValueType expected) { + List l = new ArrayList(); + List list = getList(path); + for (ConfigValue cv : list) { + // variance would be nice, but stupid cast will do + AbstractConfigValue v = (AbstractConfigValue) cv; + if (expected != null) { + v = DefaultTransformer.transform(v, expected); + } + if (v.valueType() != expected) + throw new ConfigException.WrongType(v.origin(), path, + "list of " + expected.name(), "list of " + + v.valueType().name()); + l.add((T) v.unwrapped()); + } + return l; + } + + @Override + public List getBooleanList(String path) { + return getHomogeneousUnwrappedList(path, ConfigValueType.BOOLEAN); + } + + @Override + public List getNumberList(String path) { + return getHomogeneousUnwrappedList(path, ConfigValueType.NUMBER); + } + + @Override + public List getIntList(String path) { + List l = new ArrayList(); + List numbers = getNumberList(path); + for (Number n : numbers) { + l.add(n.intValue()); + } + return l; + } + + @Override + public List getLongList(String path) { + List l = new ArrayList(); + List numbers = getNumberList(path); + for (Number n : numbers) { + l.add(n.longValue()); + } + return l; + } + + @Override + public List getDoubleList(String path) { + List l = new ArrayList(); + List numbers = getNumberList(path); + for (Number n : numbers) { + l.add(n.doubleValue()); + } + return l; + } + + @Override + public List getStringList(String path) { + return getHomogeneousUnwrappedList(path, ConfigValueType.STRING); + } + + @SuppressWarnings("unchecked") + private List getHomogeneousWrappedList( + String path, ConfigValueType expected) { + List l = new ArrayList(); + List list = getList(path); + for (ConfigValue cv : list) { + // variance would be nice, but stupid cast will do + AbstractConfigValue v = (AbstractConfigValue) cv; + if (expected != null) { + v = DefaultTransformer.transform(v, expected); + } + if (v.valueType() != expected) + throw new ConfigException.WrongType(v.origin(), path, + "list of " + expected.name(), "list of " + + v.valueType().name()); + l.add((T) v); + } + return l; + } + + @Override + public List getObjectList(String path) { + return getHomogeneousWrappedList(path, ConfigValueType.OBJECT); + } + + @Override + public List getConfigList(String path) { + List objects = getObjectList(path); + List l = new ArrayList(); + for (ConfigObject o : objects) { + l.add(o.toConfig()); + } + return l; + } + + @Override + public List getAnyRefList(String path) { + List l = new ArrayList(); + List list = getList(path); + for (ConfigValue v : list) { + l.add(v.unwrapped()); + } + return l; + } + + @Override + public List getMemorySizeInBytesList(String path) { + List l = new ArrayList(); + List list = getList(path); + for (ConfigValue v : list) { + if (v.valueType() == ConfigValueType.NUMBER) { + l.add(((Number) v.unwrapped()).longValue()); + } else if (v.valueType() == ConfigValueType.STRING) { + String s = (String) v.unwrapped(); + Long n = parseMemorySizeInBytes(s, v.origin(), path); + l.add(n); + } else { + throw new ConfigException.WrongType(v.origin(), path, + "memory size string or number of bytes", v.valueType() + .name()); + } + } + return l; + } + + @Override + public List getMillisecondsList(String path) { + List nanos = getNanosecondsList(path); + List l = new ArrayList(); + for (Long n : nanos) { + l.add(TimeUnit.NANOSECONDS.toMillis(n)); + } + return l; + } + + @Override + public List getNanosecondsList(String path) { + List l = new ArrayList(); + List list = getList(path); + for (ConfigValue v : list) { + if (v.valueType() == ConfigValueType.NUMBER) { + l.add(TimeUnit.MILLISECONDS.toNanos(((Number) v.unwrapped()) + .longValue())); + } else if (v.valueType() == ConfigValueType.STRING) { + String s = (String) v.unwrapped(); + Long n = parseDuration(s, v.origin(), path); + l.add(n); + } else { + throw new ConfigException.WrongType(v.origin(), path, + "duration string or number of nanoseconds", v + .valueType().name()); + } + } + return l; + } + + @Override + public AbstractConfigObject toValue() { + return object; + } + + @Override + public SimpleConfig withFallback(ConfigMergeable other) { + // this can return "this" if the withFallback doesn't need a new + // ConfigObject + return object.withFallback(other).toConfig(); + } + + @Override + public SimpleConfig withFallbacks(ConfigMergeable... others) { + // this can return "this" if the withFallbacks doesn't need a new + // ConfigObject + return object.withFallbacks(others).toConfig(); + } + + @Override + public final boolean equals(Object other) { + if (other instanceof SimpleConfig) { + return object.equals(((SimpleConfig) other).object); + } else { + return false; + } + } + + @Override + public final int hashCode() { + // we do the "41*" just so our hash code won't match that of the + // underlying object. there's no real reason it can't match, but + // making it not match might catch some kinds of bug. + return 41 * object.hashCode(); + } + + @Override + public String toString() { + return "Config(" + object.toString() + ")"; + } + + private static String getUnits(String s) { + int i = s.length() - 1; + while (i >= 0) { + char c = s.charAt(i); + if (!Character.isLetter(c)) + break; + i -= 1; + } + return s.substring(i + 1); + } + + /** + * Parses a duration string. If no units are specified in the string, it is + * assumed to be in milliseconds. The returned duration is in nanoseconds. + * The purpose of this function is to implement the duration-related methods + * in the ConfigObject interface. + * + * @param input + * the string to parse + * @param originForException + * origin of the value being parsed + * @param pathForException + * path to include in exceptions + * @return duration in nanoseconds + * @throws ConfigException + * if string is invalid + */ + public static long parseDuration(String input, + ConfigOrigin originForException, String pathForException) { + String s = ConfigUtil.unicodeTrim(input); + String originalUnitString = getUnits(s); + String unitString = originalUnitString; + String numberString = ConfigUtil.unicodeTrim(s.substring(0, s.length() + - unitString.length())); + TimeUnit units = null; + + // this would be caught later anyway, but the error message + // is more helpful if we check it here. + if (numberString.length() == 0) + throw new ConfigException.BadValue(originForException, + pathForException, "No number in duration value '" + input + + "'"); + + if (unitString.length() > 2 && !unitString.endsWith("s")) + unitString = unitString + "s"; + + // note that this is deliberately case-sensitive + if (unitString.equals("") || unitString.equals("ms") + || unitString.equals("milliseconds")) { + units = TimeUnit.MILLISECONDS; + } else if (unitString.equals("us") || unitString.equals("microseconds")) { + units = TimeUnit.MICROSECONDS; + } else if (unitString.equals("ns") || unitString.equals("nanoseconds")) { + units = TimeUnit.NANOSECONDS; + } else if (unitString.equals("d") || unitString.equals("days")) { + units = TimeUnit.DAYS; + } else if (unitString.equals("h") || unitString.equals("hours")) { + units = TimeUnit.HOURS; + } else if (unitString.equals("s") || unitString.equals("seconds")) { + units = TimeUnit.SECONDS; + } else if (unitString.equals("m") || unitString.equals("minutes")) { + units = TimeUnit.MINUTES; + } else { + throw new ConfigException.BadValue(originForException, + pathForException, "Could not parse time unit '" + + originalUnitString + + "' (try ns, us, ms, s, m, d)"); + } + + try { + // if the string is purely digits, parse as an integer to avoid + // possible precision loss; + // otherwise as a double. + if (numberString.matches("[0-9]+")) { + return units.toNanos(Long.parseLong(numberString)); + } else { + long nanosInUnit = units.toNanos(1); + return (long) (Double.parseDouble(numberString) * nanosInUnit); + } + } catch (NumberFormatException e) { + throw new ConfigException.BadValue(originForException, + pathForException, "Could not parse duration number '" + + numberString + "'"); + } + } + + private static enum MemoryUnit { + BYTES(1), KILOBYTES(1024), MEGABYTES(1024 * 1024), GIGABYTES( + 1024 * 1024 * 1024), TERABYTES(1024 * 1024 * 1024 * 1024); + + int bytes; + + MemoryUnit(int bytes) { + this.bytes = bytes; + } + } + + /** + * 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. + * + * @param input + * the string to parse + * @param originForException + * origin of the value being parsed + * @param pathForException + * path to include in exceptions + * @return size in bytes + * @throws ConfigException + * if string is invalid + */ + public static long parseMemorySizeInBytes(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())); + + // this would be caught later anyway, but the error message + // is more helpful if we check it here. + if (numberString.length() == 0) + throw new ConfigException.BadValue(originForException, + pathForException, "No number in size-in-bytes value '" + + input + "'"); + + MemoryUnit units = null; + + // 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)"); + } + + try { + // if the string is purely digits, parse as an integer to avoid + // 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 + "'"); + } + } +} 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 new file mode 100644 index 0000000000..aa341eb500 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfigList.java @@ -0,0 +1,340 @@ +package com.typesafe.config.impl; + +import java.util.ArrayList; +import java.util.Collection; +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; +import com.typesafe.config.ConfigValue; +import com.typesafe.config.ConfigValueType; + +final class SimpleConfigList extends AbstractConfigValue implements ConfigList { + + final private List value; + final private boolean resolved; + + SimpleConfigList(ConfigOrigin origin, List value) { + this(origin, value, ResolveStatus.fromValues(value)); + } + + SimpleConfigList(ConfigOrigin origin, List value, + ResolveStatus status) { + super(origin); + this.value = value; + this.resolved = status == ResolveStatus.RESOLVED; + } + + @Override + public ConfigValueType valueType() { + return ConfigValueType.LIST; + } + + @Override + public List unwrapped() { + List list = new ArrayList(); + for (AbstractConfigValue v : value) { + list.add(v.unwrapped()); + } + return list; + } + + @Override + ResolveStatus resolveStatus() { + return ResolveStatus.fromBoolean(resolved); + } + + private SimpleConfigList modify(Modifier modifier, + ResolveStatus newResolveStatus) { + // lazy-create for optimization + List changed = null; + int i = 0; + for (AbstractConfigValue v : value) { + AbstractConfigValue modified = modifier.modifyChild(v); + + // lazy-create the new list if required + if (changed == null && modified != v) { + changed = new ArrayList(); + for (int j = 0; j < i; ++j) { + changed.add(value.get(j)); + } + } + + // once the new list is created, all elements + // have to go in it. + if (changed != null) { + changed.add(modified); + } + + i += 1; + } + + 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; + } + } + + @Override + SimpleConfigList resolveSubstitutions(final SubstitutionResolver resolver, + final int depth, final ConfigResolveOptions options) { + if (resolved) + return this; + + return modify(new Modifier() { + @Override + public AbstractConfigValue modifyChild(AbstractConfigValue v) { + return resolver.resolve(v, depth, options); + } + + }, ResolveStatus.RESOLVED); + } + + @Override + SimpleConfigList relativized(final Path prefix) { + return modify(new Modifier() { + @Override + public AbstractConfigValue modifyChild(AbstractConfigValue v) { + return v.relativized(prefix); + } + + }, resolveStatus()); + } + + @Override + protected boolean canEqual(Object other) { + return other instanceof SimpleConfigList; + } + + @Override + public boolean equals(Object other) { + // note that "origin" is deliberately NOT part of equality + if (other instanceof SimpleConfigList) { + // optimization to avoid unwrapped() for two ConfigList + return canEqual(other) && value.equals(((SimpleConfigList) other).value); + } else { + return false; + } + } + + @Override + public int hashCode() { + // note that "origin" is deliberately NOT part of equality + return value.hashCode(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(valueType().name()); + sb.append("("); + for (ConfigValue e : value) { + sb.append(e.toString()); + sb.append(","); + } + if (!value.isEmpty()) + sb.setLength(sb.length() - 1); // chop comma + sb.append(")"); + return sb.toString(); + } + + @Override + public boolean contains(Object o) { + return value.contains(o); + } + + @Override + public boolean containsAll(Collection c) { + return value.containsAll(c); + } + + @Override + public ConfigValue get(int index) { + return value.get(index); + } + + @Override + public int indexOf(Object o) { + return value.indexOf(o); + } + + @Override + public boolean isEmpty() { + return value.isEmpty(); + } + + @Override + public Iterator iterator() { + final Iterator i = value.iterator(); + + return new Iterator() { + @Override + public boolean hasNext() { + return i.hasNext(); + } + + @Override + public ConfigValue next() { + return i.next(); + } + + @Override + public void remove() { + throw weAreImmutable("iterator().remove"); + } + }; + } + + @Override + public int lastIndexOf(Object o) { + return value.lastIndexOf(o); + } + + private static ListIterator wrapListIterator( + final ListIterator i) { + return new ListIterator() { + @Override + public boolean hasNext() { + return i.hasNext(); + } + + @Override + public ConfigValue next() { + return i.next(); + } + + @Override + public void remove() { + throw weAreImmutable("listIterator().remove"); + } + + @Override + public void add(ConfigValue arg0) { + throw weAreImmutable("listIterator().add"); + } + + @Override + public boolean hasPrevious() { + return i.hasPrevious(); + } + + @Override + public int nextIndex() { + return i.nextIndex(); + } + + @Override + public ConfigValue previous() { + return i.previous(); + } + + @Override + public int previousIndex() { + return i.previousIndex(); + } + + @Override + public void set(ConfigValue arg0) { + throw weAreImmutable("listIterator().set"); + } + }; + } + + @Override + public ListIterator listIterator() { + return wrapListIterator(value.listIterator()); + } + + @Override + public ListIterator listIterator(int index) { + return wrapListIterator(value.listIterator(index)); + } + + @Override + public int size() { + return value.size(); + } + + @Override + public List subList(int fromIndex, int toIndex) { + List list = new ArrayList(); + // yay bloat caused by lack of type variance + for (AbstractConfigValue v : value.subList(fromIndex, toIndex)) { + list.add(v); + } + return list; + } + + @Override + public Object[] toArray() { + return value.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return value.toArray(a); + } + + private static UnsupportedOperationException weAreImmutable(String method) { + return new UnsupportedOperationException( + "ConfigList is immutable, you can't call List.'" + method + "'"); + } + + @Override + public boolean add(ConfigValue e) { + throw weAreImmutable("add"); + } + + @Override + public void add(int index, ConfigValue element) { + throw weAreImmutable("add"); + } + + @Override + public boolean addAll(Collection c) { + throw weAreImmutable("addAll"); + } + + @Override + public boolean addAll(int index, Collection c) { + throw weAreImmutable("addAll"); + } + + @Override + public void clear() { + throw weAreImmutable("clear"); + } + + @Override + public boolean remove(Object o) { + throw weAreImmutable("remove"); + } + + @Override + public ConfigValue remove(int index) { + throw weAreImmutable("remove"); + } + + @Override + public boolean removeAll(Collection c) { + throw weAreImmutable("removeAll"); + } + + @Override + public boolean retainAll(Collection c) { + throw weAreImmutable("retainAll"); + } + + @Override + public ConfigValue set(int index, ConfigValue element) { + throw weAreImmutable("set"); + } +} 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 new file mode 100644 index 0000000000..b1b5140943 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java @@ -0,0 +1,126 @@ +package com.typesafe.config.impl; + +import java.util.AbstractMap; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigOrigin; +import com.typesafe.config.ConfigValue; + +final class SimpleConfigObject extends AbstractConfigObject { + + // this map should never be modified - assume immutable + final private Map value; + final private boolean resolved; + + SimpleConfigObject(ConfigOrigin origin, + Map value, ResolveStatus status) { + super(origin); + if (value == null) + throw new ConfigException.BugOrBroken( + "creating config object with null map"); + this.value = value; + this.resolved = status == ResolveStatus.RESOLVED; + } + + SimpleConfigObject(ConfigOrigin origin, + Map value) { + this(origin, value, ResolveStatus.fromValues(value + .values())); + } + + @Override + protected AbstractConfigValue peek(String key) { + return value.get(key); + } + + @Override + public SimpleConfigObject newCopy(ResolveStatus newStatus) { + return new SimpleConfigObject(origin(), value, + newStatus); + } + + @Override + ResolveStatus resolveStatus() { + return ResolveStatus.fromBoolean(resolved); + } + + @Override + public Map unwrapped() { + Map m = new HashMap(); + for (Map.Entry e : value.entrySet()) { + m.put(e.getKey(), e.getValue().unwrapped()); + } + return m; + } + + @Override + public boolean containsKey(Object key) { + return value.containsKey(key); + } + + @Override + public Set keySet() { + return value.keySet(); + } + + @Override + public boolean containsValue(Object v) { + return value.containsValue(v); + } + + @Override + public Set> entrySet() { + // total bloat just to work around lack of type variance + + HashSet> entries = new HashSet>(); + for (Map.Entry e : value.entrySet()) { + entries.add(new AbstractMap.SimpleImmutableEntry( + e.getKey(), e + .getValue())); + } + return entries; + } + + @Override + public boolean isEmpty() { + return value.isEmpty(); + } + + @Override + public int size() { + return value.size(); + } + + @Override + public Collection values() { + return new HashSet(value.values()); + } + + final private static String EMPTY_NAME = "empty config"; + final private static SimpleConfigObject emptyInstance = empty(new SimpleConfigOrigin( + EMPTY_NAME)); + + final static SimpleConfigObject empty() { + return emptyInstance; + } + + final static SimpleConfigObject empty(ConfigOrigin origin) { + if (origin == null) + return empty(); + else + return new SimpleConfigObject(origin, + Collections. emptyMap()); + } + + final static SimpleConfigObject emptyMissing(ConfigOrigin baseOrigin) { + return new SimpleConfigObject(new SimpleConfigOrigin( + 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 new file mode 100644 index 0000000000..ed23e69863 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/SimpleConfigOrigin.java @@ -0,0 +1,37 @@ +package com.typesafe.config.impl; + +import com.typesafe.config.ConfigOrigin; + +final class SimpleConfigOrigin implements ConfigOrigin { + + final private String description; + + SimpleConfigOrigin(String description) { + this.description = description; + } + + @Override + public String description() { + return description; + } + + @Override + public boolean equals(Object other) { + if (other instanceof SimpleConfigOrigin) { + return this.description + .equals(((SimpleConfigOrigin) other).description); + } else { + return false; + } + } + + @Override + public int hashCode() { + return description.hashCode(); + } + + @Override + public String toString() { + return "ConfigOrigin(" + description + ")"; + } +} 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 new file mode 100644 index 0000000000..1ec58ce6ea --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/SubstitutionResolver.java @@ -0,0 +1,48 @@ +package com.typesafe.config.impl; + +import java.util.IdentityHashMap; +import java.util.Map; + +import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigResolveOptions; + +/** + * This exists because we have to memoize resolved substitutions as we go + * through the config tree; otherwise we could end up creating multiple copies + * of values or whole trees of values as we follow chains of substitutions. + */ +final class SubstitutionResolver { + final private AbstractConfigObject root; + final private Map memos; + + SubstitutionResolver(AbstractConfigObject root) { + this.root = root; + // note: the memoization is by object identity, not object value + this.memos = new IdentityHashMap(); + } + + AbstractConfigValue resolve(AbstractConfigValue original, int depth, + ConfigResolveOptions options) { + if (memos.containsKey(original)) { + return memos.get(original); + } 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"); + memos.put(original, resolved); + return resolved; + } + } + + AbstractConfigObject root() { + return this.root; + } + + static AbstractConfigValue resolve(AbstractConfigValue value, + AbstractConfigObject root, ConfigResolveOptions options) { + SubstitutionResolver resolver = new SubstitutionResolver(root); + return resolver.resolve(value, 0, options); + } +} diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/Token.java b/akka-actor/src/main/java/com/typesafe/config/impl/Token.java new file mode 100644 index 0000000000..1c863c870a --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/Token.java @@ -0,0 +1,37 @@ +package com.typesafe.config.impl; + +class Token { + final private TokenType tokenType; + + Token(TokenType tokenType) { + this.tokenType = tokenType; + } + + public TokenType tokenType() { + return tokenType; + } + + @Override + public String toString() { + return tokenType.name(); + } + + protected boolean canEqual(Object other) { + return other instanceof Token; + } + + @Override + public boolean equals(Object other) { + if (other instanceof Token) { + return canEqual(other) + && this.tokenType == ((Token) other).tokenType; + } else { + return false; + } + } + + @Override + public int hashCode() { + return tokenType.hashCode(); + } +} diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/TokenType.java b/akka-actor/src/main/java/com/typesafe/config/impl/TokenType.java new file mode 100644 index 0000000000..72700dbaa4 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/TokenType.java @@ -0,0 +1,5 @@ +package com.typesafe.config.impl; + +enum TokenType { + START, END, COMMA, EQUALS, COLON, OPEN_CURLY, CLOSE_CURLY, OPEN_SQUARE, CLOSE_SQUARE, VALUE, NEWLINE, UNQUOTED_TEXT, SUBSTITUTION; +} 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 new file mode 100644 index 0000000000..1298f3c1bd --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/Tokenizer.java @@ -0,0 +1,532 @@ +package com.typesafe.config.impl; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigOrigin; +import com.typesafe.config.ConfigSyntax; + +final class Tokenizer { + /** + * Tokenizes a Reader. Does not close the reader; you have to arrange to do + * that after you're done with the returned iterator. + */ + static Iterator tokenize(ConfigOrigin origin, Reader input, ConfigSyntax flavor) { + return new TokenIterator(origin, input, flavor != ConfigSyntax.JSON); + } + + private static class TokenIterator implements Iterator { + + private static class WhitespaceSaver { + // has to be saved inside value concatenations + private StringBuilder whitespace; + // may need to value-concat with next value + private boolean lastTokenWasSimpleValue; + + WhitespaceSaver() { + whitespace = new StringBuilder(); + lastTokenWasSimpleValue = false; + } + + void add(int c) { + if (lastTokenWasSimpleValue) + whitespace.appendCodePoint(c); + } + + Token check(Token t, ConfigOrigin baseOrigin, int lineNumber) { + if (isSimpleValue(t)) { + return nextIsASimpleValue(baseOrigin, lineNumber); + } else { + nextIsNotASimpleValue(); + return null; + } + } + + // called if the next token is not a simple value; + // discards any whitespace we were saving between + // simple values. + private void nextIsNotASimpleValue() { + lastTokenWasSimpleValue = false; + whitespace.setLength(0); + } + + // called if the next token IS a simple value, + // so creates a whitespace token if the previous + // token also was. + private Token nextIsASimpleValue(ConfigOrigin baseOrigin, + int lineNumber) { + if (lastTokenWasSimpleValue) { + // need to save whitespace between the two so + // the parser has the option to concatenate it. + if (whitespace.length() > 0) { + Token t = Tokens.newUnquotedText( + lineOrigin(baseOrigin, lineNumber), + whitespace.toString()); + whitespace.setLength(0); // reset + return t; + } else { + // lastTokenWasSimpleValue = true still + return null; + } + } else { + lastTokenWasSimpleValue = true; + whitespace.setLength(0); + return null; + } + } + } + + final private ConfigOrigin origin; + final private Reader input; + final private LinkedList buffer; + private int lineNumber; + final private Queue tokens; + final private WhitespaceSaver whitespaceSaver; + final private boolean allowComments; + + TokenIterator(ConfigOrigin origin, Reader input, boolean allowComments) { + this.origin = origin; + this.input = input; + this.allowComments = allowComments; + this.buffer = new LinkedList(); + lineNumber = 1; + tokens = new LinkedList(); + tokens.add(Tokens.START); + whitespaceSaver = new WhitespaceSaver(); + } + + + // this should ONLY be called from nextCharSkippingComments + // or when inside a quoted string, everything else should + // use nextCharSkippingComments(). + private int nextCharRaw() { + if (buffer.isEmpty()) { + try { + return input.read(); + } catch (IOException e) { + throw new ConfigException.IO(origin, "read error: " + + e.getMessage(), e); + } + } else { + int c = buffer.pop(); + return c; + } + } + + private void putBack(int c) { + if (buffer.size() > 2) { + throw new ConfigException.BugOrBroken( + "bug: putBack() three times, undesirable look-ahead"); + } + buffer.push(c); + } + + static boolean isWhitespace(int c) { + return ConfigUtil.isWhitespace(c); + } + + static boolean isWhitespaceNotNewline(int c) { + return c != '\n' && ConfigUtil.isWhitespace(c); + } + + private int slurpComment() { + for (;;) { + int c = nextCharRaw(); + if (c == -1 || c == '\n') { + return c; + } + } + } + + // get next char, skipping comments + private int nextCharSkippingComments() { + for (;;) { + int c = nextCharRaw(); + + if (c == -1) { + return -1; + } else { + if (allowComments) { + if (c == '#') { + return slurpComment(); + } else if (c == '/') { + int maybeSecondSlash = nextCharRaw(); + if (maybeSecondSlash == '/') { + return slurpComment(); + } else { + putBack(maybeSecondSlash); + return c; + } + } else { + return c; + } + } else { + return c; + } + } + } + } + + // get next char, skipping non-newline whitespace + private int nextCharAfterWhitespace(WhitespaceSaver saver) { + for (;;) { + int c = nextCharSkippingComments(); + + if (c == -1) { + return -1; + } else { + if (isWhitespaceNotNewline(c)) { + saver.add(c); + continue; + } else { + return c; + } + } + } + } + + private ConfigException parseError(String message) { + return parseError(message, null); + } + + private ConfigException parseError(String message, Throwable cause) { + return parseError(lineOrigin(), message, cause); + } + + private static ConfigException parseError(ConfigOrigin origin, + String message, + Throwable cause) { + return new ConfigException.Parse(origin, message, cause); + } + + private static ConfigException parseError(ConfigOrigin origin, + String message) { + return parseError(origin, message, null); + } + + private ConfigOrigin lineOrigin() { + return lineOrigin(origin, lineNumber); + } + + private static ConfigOrigin lineOrigin(ConfigOrigin baseOrigin, + int lineNumber) { + return new SimpleConfigOrigin(baseOrigin.description() + ": line " + + lineNumber); + } + + // chars JSON allows a number to start with + static final String firstNumberChars = "0123456789-"; + // chars JSON allows to be part of a number + static final String numberChars = "0123456789eE+-."; + // chars that stop an unquoted string + static final String notInUnquotedText = "$\"{}[]:=,\\+#"; + + // The rules here are intended to maximize convenience while + // avoiding confusion with real valid JSON. Basically anything + // that parses as JSON is treated the JSON way and otherwise + // we assume it's a string and let the parser sort it out. + private Token pullUnquotedText() { + ConfigOrigin origin = lineOrigin(); + StringBuilder sb = new StringBuilder(); + int c = nextCharSkippingComments(); + while (true) { + if (c == -1) { + break; + } else if (notInUnquotedText.indexOf(c) >= 0) { + break; + } else if (isWhitespace(c)) { + break; + } else { + sb.appendCodePoint(c); + } + + // we parse true/false/null tokens as such no matter + // what is after them, as long as they are at the + // start of the unquoted token. + if (sb.length() == 4) { + String s = sb.toString(); + if (s.equals("true")) + return Tokens.newBoolean(origin, true); + else if (s.equals("null")) + return Tokens.newNull(origin); + } else if (sb.length() == 5) { + String s = sb.toString(); + if (s.equals("false")) + return Tokens.newBoolean(origin, false); + } + + c = nextCharSkippingComments(); + } + + // put back the char that ended the unquoted text + putBack(c); + + String s = sb.toString(); + return Tokens.newUnquotedText(origin, s); + } + + private Token pullNumber(int firstChar) { + StringBuilder sb = new StringBuilder(); + sb.appendCodePoint(firstChar); + boolean containedDecimalOrE = false; + int c = nextCharSkippingComments(); + while (c != -1 && numberChars.indexOf(c) >= 0) { + if (c == '.' || c == 'e' || c == 'E') + containedDecimalOrE = true; + sb.appendCodePoint(c); + c = nextCharSkippingComments(); + } + // the last character we looked at wasn't part of the number, put it + // back + putBack(c); + String s = sb.toString(); + try { + if (containedDecimalOrE) { + // force floating point representation + return Tokens.newDouble(lineOrigin(), + Double.parseDouble(s), s); + } else { + // this should throw if the integer is too large for Long + return Tokens.newLong(lineOrigin(), Long.parseLong(s), s); + } + } catch (NumberFormatException e) { + throw parseError("Invalid number: '" + s + + "' (if this is in a path, try quoting it with double quotes)", + e); + } + } + + private void pullEscapeSequence(StringBuilder sb) { + int escaped = nextCharRaw(); + if (escaped == -1) + throw parseError("End of input but backslash in string had nothing after it"); + + switch (escaped) { + case '"': + sb.append('"'); + break; + case '\\': + sb.append('\\'); + break; + case '/': + sb.append('/'); + break; + case 'b': + sb.append('\b'); + break; + case 'f': + sb.append('\f'); + break; + case 'n': + sb.append('\n'); + break; + case 'r': + sb.append('\r'); + break; + case 't': + sb.append('\t'); + break; + case 'u': { + // kind of absurdly slow, but screw it for now + char[] a = new char[4]; + for (int i = 0; i < 4; ++i) { + int c = nextCharSkippingComments(); + if (c == -1) + throw parseError("End of input but expecting 4 hex digits for \\uXXXX escape"); + a[i] = (char) c; + } + String digits = new String(a); + try { + sb.appendCodePoint(Integer.parseInt(digits, 16)); + } catch (NumberFormatException e) { + throw parseError( + String.format( + "Malformed hex digits after \\u escape in string: '%s'", + digits), e); + } + } + break; + default: + throw parseError(String + .format("backslash followed by '%c', this is not a valid escape sequence", + escaped)); + } + } + + private ConfigException controlCharacterError(int c) { + String asString; + if (c == '\n') + asString = "newline"; + else if (c == '\t') + asString = "tab"; + else + asString = String.format("control character 0x%x", c); + return parseError("JSON does not allow unescaped " + asString + + " in quoted strings, use a backslash escape"); + } + + private Token pullQuotedString() { + // the open quote has already been consumed + StringBuilder sb = new StringBuilder(); + int c = '\0'; // value doesn't get used + do { + c = nextCharRaw(); + if (c == -1) + throw parseError("End of input but string quote was still open"); + + if (c == '\\') { + pullEscapeSequence(sb); + } else if (c == '"') { + // end the loop, done! + } else if (Character.isISOControl(c)) { + throw controlCharacterError(c); + } else { + sb.appendCodePoint(c); + } + } while (c != '"'); + return Tokens.newString(lineOrigin(), sb.toString()); + } + + private Token pullSubstitution() { + // the initial '$' has already been consumed + ConfigOrigin origin = lineOrigin(); + int c = nextCharSkippingComments(); + if (c != '{') { + throw parseError("'$' not followed by {"); + } + + WhitespaceSaver saver = new WhitespaceSaver(); + List expression = new ArrayList(); + + Token t; + do { + t = pullNextToken(saver); + + // note that we avoid validating the allowed tokens inside + // the substitution here; we even allow nested substitutions + // in the tokenizer. The parser sorts it out. + if (t == Tokens.CLOSE_CURLY) { + // end the loop, done! + break; + } else if (t == Tokens.END) { + throw parseError(origin, + "Substitution ${ was not closed with a }"); + } else { + Token whitespace = saver.check(t, origin, lineNumber); + if (whitespace != null) + expression.add(whitespace); + expression.add(t); + } + } while (true); + + return Tokens.newSubstitution(origin, expression); + } + + private Token pullNextToken(WhitespaceSaver saver) { + int c = nextCharAfterWhitespace(saver); + if (c == -1) { + return Tokens.END; + } else if (c == '\n') { + // newline tokens have the just-ended line number + lineNumber += 1; + return Tokens.newLine(lineNumber - 1); + } else { + Token t = null; + switch (c) { + case '"': + t = pullQuotedString(); + break; + case '$': + t = pullSubstitution(); + break; + case ':': + t = Tokens.COLON; + break; + case ',': + t = Tokens.COMMA; + break; + case '=': + t = Tokens.EQUALS; + break; + case '{': + t = Tokens.OPEN_CURLY; + break; + case '}': + t = Tokens.CLOSE_CURLY; + break; + case '[': + t = Tokens.OPEN_SQUARE; + break; + case ']': + t = Tokens.CLOSE_SQUARE; + break; + } + + if (t == null) { + if (firstNumberChars.indexOf(c) >= 0) { + t = pullNumber(c); + } else if (notInUnquotedText.indexOf(c) >= 0) { + throw parseError(String + .format("Character '%c' is not the start of any valid token", + c)); + } else { + putBack(c); + t = pullUnquotedText(); + } + } + + if (t == null) + throw new ConfigException.BugOrBroken( + "bug: failed to generate next token"); + + return t; + } + } + + private static boolean isSimpleValue(Token t) { + if (Tokens.isSubstitution(t) || Tokens.isUnquotedText(t) + || Tokens.isValue(t)) { + return true; + } else { + return false; + } + } + + private void queueNextToken() { + Token t = pullNextToken(whitespaceSaver); + Token whitespace = whitespaceSaver.check(t, origin, lineNumber); + if (whitespace != null) + tokens.add(whitespace); + tokens.add(t); + } + + @Override + public boolean hasNext() { + return !tokens.isEmpty(); + } + + @Override + public Token next() { + Token t = tokens.remove(); + if (tokens.isEmpty() && t != Tokens.END) { + queueNextToken(); + if (tokens.isEmpty()) + throw new ConfigException.BugOrBroken( + "bug: tokens queue should not be empty here"); + } + return t; + } + + @Override + public void remove() { + throw new UnsupportedOperationException( + "Does not make sense to remove items from token stream"); + } + } +} 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 new file mode 100644 index 0000000000..8d66bcb575 --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/Tokens.java @@ -0,0 +1,290 @@ +package com.typesafe.config.impl; + +import java.util.List; + +import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigOrigin; +import com.typesafe.config.ConfigValueType; + +final class Tokens { + static private class Value extends Token { + + final private AbstractConfigValue value; + + Value(AbstractConfigValue value) { + super(TokenType.VALUE); + this.value = value; + } + + AbstractConfigValue value() { + return value; + } + + @Override + public String toString() { + String s = tokenType().name() + "(" + value.valueType().name() + + ")"; + + return s + "='" + value().unwrapped() + "'"; + } + + @Override + protected boolean canEqual(Object other) { + return other instanceof Value; + } + + @Override + public boolean equals(Object other) { + return super.equals(other) && ((Value) other).value.equals(value); + } + + @Override + public int hashCode() { + return 41 * (41 + super.hashCode()) + value.hashCode(); + } + } + + static private class Line extends Token { + final private int lineNumber; + + Line(int lineNumber) { + super(TokenType.NEWLINE); + this.lineNumber = lineNumber; + } + + int lineNumber() { + return lineNumber; + } + + @Override + public String toString() { + return "NEWLINE@" + lineNumber; + } + + @Override + protected boolean canEqual(Object other) { + return other instanceof Line; + } + + @Override + public boolean equals(Object other) { + return super.equals(other) + && ((Line) other).lineNumber == lineNumber; + } + + @Override + public int hashCode() { + return 41 * (41 + super.hashCode()) + lineNumber; + } + } + + // This is not a Value, because it requires special processing + static private class UnquotedText extends Token { + final private ConfigOrigin origin; + final private String value; + + UnquotedText(ConfigOrigin origin, String s) { + super(TokenType.UNQUOTED_TEXT); + this.origin = origin; + this.value = s; + } + + ConfigOrigin origin() { + return origin; + } + + String value() { + return value; + } + + @Override + public String toString() { + return tokenType().name() + "(" + value + ")"; + } + + @Override + protected boolean canEqual(Object other) { + return other instanceof UnquotedText; + } + + @Override + public boolean equals(Object other) { + return super.equals(other) + && ((UnquotedText) other).value.equals(value); + } + + @Override + public int hashCode() { + return 41 * (41 + super.hashCode()) + value.hashCode(); + } + } + + // This is not a Value, because it requires special processing + static private class Substitution extends Token { + final private ConfigOrigin origin; + final private List value; + + Substitution(ConfigOrigin origin, List expression) { + super(TokenType.SUBSTITUTION); + this.origin = origin; + this.value = expression; + } + + ConfigOrigin origin() { + return origin; + } + + List value() { + return value; + } + + @Override + public String toString() { + return tokenType().name() + "(" + value.toString() + ")"; + } + + @Override + protected boolean canEqual(Object other) { + return other instanceof Substitution; + } + + @Override + public boolean equals(Object other) { + return super.equals(other) + && ((Substitution) other).value.equals(value); + } + + @Override + public int hashCode() { + return 41 * (41 + super.hashCode()) + value.hashCode(); + } + } + + static boolean isValue(Token token) { + return token instanceof Value; + } + + static AbstractConfigValue getValue(Token token) { + if (token instanceof Value) { + return ((Value) token).value(); + } else { + throw new ConfigException.BugOrBroken( + "tried to get value of non-value token " + token); + } + } + + static boolean isValueWithType(Token t, ConfigValueType valueType) { + return isValue(t) && getValue(t).valueType() == valueType; + } + + static boolean isNewline(Token token) { + return token instanceof Line; + } + + static int getLineNumber(Token token) { + if (token instanceof Line) { + return ((Line) token).lineNumber(); + } else { + throw new ConfigException.BugOrBroken( + "tried to get line number from non-newline " + token); + } + } + + static boolean isUnquotedText(Token token) { + return token instanceof UnquotedText; + } + + static String getUnquotedText(Token token) { + if (token instanceof UnquotedText) { + return ((UnquotedText) token).value(); + } else { + throw new ConfigException.BugOrBroken( + "tried to get unquoted text from " + token); + } + } + + static ConfigOrigin getUnquotedTextOrigin(Token token) { + if (token instanceof UnquotedText) { + return ((UnquotedText) token).origin(); + } else { + throw new ConfigException.BugOrBroken( + "tried to get unquoted text from " + token); + } + } + + static boolean isSubstitution(Token token) { + return token instanceof Substitution; + } + + static List getSubstitutionPathExpression(Token token) { + if (token instanceof Substitution) { + return ((Substitution) token).value(); + } else { + throw new ConfigException.BugOrBroken( + "tried to get substitution from " + token); + } + } + + static ConfigOrigin getSubstitutionOrigin(Token token) { + if (token instanceof Substitution) { + return ((Substitution) token).origin(); + } else { + throw new ConfigException.BugOrBroken( + "tried to get substitution origin 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); + final static Token EQUALS = new Token(TokenType.EQUALS); + final static Token COLON = new Token(TokenType.COLON); + final static Token OPEN_CURLY = new Token(TokenType.OPEN_CURLY); + final static Token CLOSE_CURLY = new Token(TokenType.CLOSE_CURLY); + final static Token OPEN_SQUARE = new Token(TokenType.OPEN_SQUARE); + final static Token CLOSE_SQUARE = new Token(TokenType.CLOSE_SQUARE); + + static Token newLine(int lineNumberJustEnded) { + return new Line(lineNumberJustEnded); + } + + static Token newUnquotedText(ConfigOrigin origin, String s) { + return new UnquotedText(origin, s); + } + + static Token newSubstitution(ConfigOrigin origin, List expression) { + return new Substitution(origin, expression); + } + + static Token newValue(AbstractConfigValue value) { + return new Value(value); + } + + static Token newString(ConfigOrigin origin, String value) { + return newValue(new ConfigString(origin, value)); + } + + static Token newInt(ConfigOrigin origin, int value, String originalText) { + return newValue(ConfigNumber.newNumber(origin, value, + originalText)); + } + + static Token newDouble(ConfigOrigin origin, double value, + String originalText) { + return newValue(ConfigNumber.newNumber(origin, value, + originalText)); + } + + static Token newLong(ConfigOrigin origin, long value, String originalText) { + return newValue(ConfigNumber.newNumber(origin, value, + originalText)); + } + + static Token newNull(ConfigOrigin origin) { + return newValue(new ConfigNull(origin)); + } + + static Token newBoolean(ConfigOrigin origin, boolean value) { + return newValue(new ConfigBoolean(origin, value)); + } +} diff --git a/akka-actor/src/main/java/com/typesafe/config/impl/Unmergeable.java b/akka-actor/src/main/java/com/typesafe/config/impl/Unmergeable.java new file mode 100644 index 0000000000..82dc203eef --- /dev/null +++ b/akka-actor/src/main/java/com/typesafe/config/impl/Unmergeable.java @@ -0,0 +1,13 @@ +package com.typesafe.config.impl; + +import java.util.Collection; + +/** + * Interface that tags a ConfigValue that is not mergeable until after + * substitutions are resolved. Basically these are special ConfigValue that + * never appear in a resolved tree, like {@link ConfigSubstitution} and + * {@link ConfigDelayedMerge}. + */ +interface Unmergeable { + Collection unmergedValues(); +} diff --git a/akka-actor/src/main/java/org/jboss/netty/akka/util/MapBackedSet.java b/akka-actor/src/main/java/org/jboss/netty/akka/util/MapBackedSet.java index a40e72cead..2bc1bc25e0 100644 --- a/akka-actor/src/main/java/org/jboss/netty/akka/util/MapBackedSet.java +++ b/akka-actor/src/main/java/org/jboss/netty/akka/util/MapBackedSet.java @@ -19,56 +19,55 @@ import java.io.Serializable; import java.util.AbstractSet; import java.util.Iterator; import java.util.Map; -import java.util.Set; /** * A {@link java.util.Map}-backed {@link java.util.Set}. - * + * * @author The Netty Project * @author Trustin Lee - * + * * @version $Rev: 2080 $, $Date: 2010-01-26 18:04:19 +0900 (Tue, 26 Jan 2010) $ */ final class MapBackedSet extends AbstractSet implements Serializable { - private static final long serialVersionUID = -6761513279741915432L; + private static final long serialVersionUID = -6761513279741915432L; - private final Map map; + private final Map map; - /** - * Creates a new instance which wraps the specified {@code map}. - */ - MapBackedSet(Map map) { - this.map = map; - } + /** + * Creates a new instance which wraps the specified {@code map}. + */ + MapBackedSet(Map map) { + this.map = map; + } - @Override - public int size() { - return map.size(); - } + @Override + public int size() { + return map.size(); + } - @Override - public boolean contains(Object o) { - return map.containsKey(o); - } + @Override + public boolean contains(Object o) { + return map.containsKey(o); + } - @Override - public boolean add(E o) { - return map.put(o, Boolean.TRUE) == null; - } + @Override + public boolean add(E o) { + return map.put(o, Boolean.TRUE) == null; + } - @Override - public boolean remove(Object o) { - return map.remove(o) != null; - } + @Override + public boolean remove(Object o) { + return map.remove(o) != null; + } - @Override - public void clear() { - map.clear(); - } + @Override + public void clear() { + map.clear(); + } - @Override - public Iterator iterator() { - return map.keySet().iterator(); - } + @Override + public Iterator iterator() { + return map.keySet().iterator(); + } } diff --git a/akka-actor/src/main/java/org/jboss/netty/akka/util/TimerTask.java b/akka-actor/src/main/java/org/jboss/netty/akka/util/TimerTask.java index 341f43ad68..3d0190d8f5 100644 --- a/akka-actor/src/main/java/org/jboss/netty/akka/util/TimerTask.java +++ b/akka-actor/src/main/java/org/jboss/netty/akka/util/TimerTask.java @@ -15,23 +15,24 @@ */ package org.jboss.netty.akka.util; -import java.util.concurrent.TimeUnit; - /** * A task which is executed after the delay specified with - * {@link Timer#newTimeout(org.jboss.netty.akka.util.TimerTask, long, java.util.concurrent.TimeUnit)}. - * + * {@link Timer#newTimeout(org.jboss.netty.akka.util.TimerTask, long, java.util.concurrent.TimeUnit)} + * . + * * @author The Netty Project * @author Trustin Lee * @version $Rev: 2080 $, $Date: 2010-01-26 18:04:19 +0900 (Tue, 26 Jan 2010) $ */ public interface TimerTask { - /** - * Executed after the delay specified with - * {@link Timer#newTimeout(org.jboss.netty.akka.util.TimerTask, long, java.util.concurrent.TimeUnit)}. - * - * @param timeout a handle which is associated with this task - */ - void run(Timeout timeout) throws Exception; + /** + * Executed after the delay specified with + * {@link Timer#newTimeout(org.jboss.netty.akka.util.TimerTask, long, java.util.concurrent.TimeUnit)} + * . + * + * @param timeout + * a handle which is associated with this task + */ + void run(Timeout timeout) throws Exception; } diff --git a/config/akka-reference.conf b/akka-actor/src/main/resources/akka-actor-reference.conf similarity index 82% rename from config/akka-reference.conf rename to akka-actor/src/main/resources/akka-actor-reference.conf index 695bdb04cf..a08c9916f1 100644 --- a/config/akka-reference.conf +++ b/akka-actor/src/main/resources/akka-actor-reference.conf @@ -3,9 +3,7 @@ ############################## # This the reference config file has all the default settings. -# All these could be removed with no visible effect. -# Modify as needed. -# This file is imported in the 'akka.conf' file. Make your edits/overrides there. +# Make your edits/overrides in your akka.conf. akka { version = "2.0-SNAPSHOT" # Akka version, checked against the runtime version of Akka. @@ -31,7 +29,7 @@ akka { executor-bounds = -1 # Makes the Executor bounded, -1 is unbounded task-queue-size = -1 # Specifies the bounded capacity of the task queue (< 1 == unbounded) task-queue-type = "linked" # Specifies which type of task queue will be used, can be "array" or "linked" (default) - allow-core-timeout = on # Allow core threads to time out + allow-core-timeout = true # Allow core threads to time out throughput = 5 # Throughput for Dispatcher, set to 1 for complete fairness throughput-deadline-time = -1 # Throughput deadline for Dispatcher, set to 0 or negative for no deadline mailbox-capacity = -1 # If negative (or zero) then an unbounded mailbox is used (default) @@ -52,18 +50,17 @@ akka { boot = [] actor { + provider = "akka.actor.LocalActorRefProvider" timeout = 5 # Default timeout for Future based invocations # - Actor: ask && ? # - UntypedActor: ask # - TypedActor: methods with non-void return type - serialize-messages = off # Does a deep clone of (non-primitive) messages to ensure immutability - throughput = 5 # Default throughput for all Dispatcher, set to 1 for complete fairness - throughput-deadline-time = -1 # Default throughput deadline for all Dispatcher, set to 0 or negative for no deadline + serialize-messages = false # Does a deep clone of (non-primitive) messages to ensure immutability dispatcher-shutdown-timeout = 1 # Using the akka.time-unit, how long dispatchers by default will wait for new actors until they shut down deployment { - - /app/service-ping { # deployment id pattern + + default { # deployment id pattern, e.g. /app/service-ping router = "round-robin" # routing (load-balance) scheme to use # available: "direct", "round-robin", "random", "scatter-gather" @@ -72,46 +69,48 @@ akka { # default is "direct"; # if 'replication' is used then the only available router is "direct" - nr-of-instances = 3 # number of actor instances in the cluster + nr-of-instances = 1 # number of actor instances in the cluster # available: positive integer (1-N) or the string "auto" for auto-scaling # default is '1' # if the "direct" router is used then this element is ignored (always '1') - #create-as { - # class = "com.biz.app.MyActor" # FIXME document 'create-as' - #} - + + # optional + create-as { # FIXME document 'create-as' + class = "" # fully qualified class name of recipe implementation + } + remote { - nodes = ["wallace:2552", "gromit:2552"] # A list of hostnames and ports for instantiating the remote actor instances + nodes = [] # A list of hostnames and ports for instantiating the remote actor instances # The format should be on "hostname:port", where: # - hostname can be either hostname or IP address the remote actor should connect to # - port should be the port for the remote server on the other node } - - #cluster { # defines the actor as a clustered actor + + cluster { # defines the actor as a clustered actor # default (if omitted) is local non-clustered actor - # preferred-nodes = ["node:node1"] # a list of preferred nodes for instantiating the actor instances on + preferred-nodes = [] # a list of preferred nodes for instantiating the actor instances on # defined as node name - # available: "node:" - - - # replication { # use replication or not? only makes sense for a stateful actor + # on format "host:", "ip:" or "node:" + + # 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 = off # should the actor mailbox be part of the serialized snapshot? + serialize-mailbox = false # should the actor mailbox be part of the serialized snapshot? # default is 'off' - # storage = "transaction-log" # storage model for replication + storage = "transaction-log" # storage model for replication # available: "transaction-log" and "data-grid" # default is "transaction-log" - # strategy = "write-through" # guaranteees for replication + strategy = "write-through" # guaranteees for replication # available: "write-through" and "write-behind" # default is "write-through" - - # } - #} + + } + } } } @@ -119,13 +118,13 @@ akka { type = "Dispatcher" # Must be one of the following # Dispatcher, (BalancingDispatcher, only valid when all actors using it are of the same type), # A FQCN to a class inheriting MessageDispatcherConfigurator with a no-arg visible constructor - name = "MyDispatcher" # Optional, will be a generated UUID if omitted + name = "DefaultDispatcher" # Optional, will be a generated UUID if omitted keep-alive-time = 60 # Keep alive time for threads core-pool-size-factor = 8.0 # No of core threads ... ceil(available processors * factor) max-pool-size-factor = 8.0 # Max no of threads ... ceil(available processors * factor) task-queue-size = -1 # Specifies the bounded capacity of the task queue (< 1 == unbounded) task-queue-type = "linked" # Specifies which type of task queue will be used, can be "array" or "linked" (default) - allow-core-timeout = on # Allow core threads to time out + allow-core-timeout = true # Allow core threads to time out throughput = 5 # Throughput for Dispatcher, set to 1 for complete fairness throughput-deadline-time = -1 # Throughput deadline for Dispatcher, set to 0 or negative for no deadline mailbox-capacity = -1 # If negative (or zero) then an unbounded mailbox is used (default) @@ -137,11 +136,11 @@ akka { } debug { - receive = off # enable function of Actor.loggable(), which is to log any received message at DEBUG level - autoreceive = off # enable DEBUG logging of all AutoReceiveMessages (Kill, PoisonPill and the like) - lifecycle = off # enable DEBUG logging of actor lifecycle changes - 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 + receive = false # enable function of Actor.loggable(), which is to log any received message at DEBUG level + autoreceive = false # enable DEBUG logging of all AutoReceiveMessages (Kill, PoisonPill and the like) + lifecycle = false # enable DEBUG logging of actor lifecycle changes + fsm = false # enable DEBUG logging of all LoggingFSMs for events, transitions and timers + event-stream = false # enable DEBUG logging of subscription changes on the eventStream } mailbox { @@ -151,14 +150,15 @@ akka { max-items = 2147483647 max-size = 2147483647 max-items = 2147483647 + max-item-size = 2147483647 max-age = 0 max-journal-size = 16777216 # 16 * 1024 * 1024 max-memory-size = 134217728 # 128 * 1024 * 1024 max-journal-overflow = 10 max-journal-size-absolute = 9223372036854775807 - discard-old-when-full = on - keep-journal = on - sync-journal = off + discard-old-when-full = true + keep-journal = true + sync-journal = false } redis { @@ -181,11 +181,11 @@ akka { server-addresses = "localhost:2181" session-timeout = 60 connection-timeout = 60 - blocking-queue = on + blocking-queue = true } beanstalk { - hostname = "127.0.0.1" + hostname = "0.0.0.0" port = 11300 reconnect-window = 5 message-submit-delay = 0 @@ -197,12 +197,12 @@ akka { # 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" - # } + serializers { + # java = "akka.serialization.JavaSerializer" + # proto = "akka.testing.ProtobufSerializer" + # sjson = "akka.testing.SJSONSerializer" + default = "akka.serialization.JavaSerializer" + } # serialization-bindings { # java = ["akka.serialization.SerializeSpec$Address", @@ -218,13 +218,15 @@ akka { remote { # FIXME rename to transport layer = "akka.cluster.netty.NettyRemoteSupport" + + use-compression = false secure-cookie = "" # Generate your own with '$AKKA_HOME/scripts/generate_config_with_secure_cookie.sh' # or using 'akka.util.Crypt.generateSecureCookie' remote-daemon-ack-timeout = 30 # Timeout for ACK of cluster operations, lik checking actor out etc. - use-passive-connections = on # Reuse inbound connections for outbound messages + use-passive-connections = true # Reuse inbound connections for outbound messages failure-detector { # accrual failure detection config threshold = 8 # defines the failure detector threshold @@ -238,8 +240,8 @@ akka { 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 connection-timeout = 120 # Length in time-unit - 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. + require-cookie = false # Should the remote server require that it peers share the same secure-cookie (defined in the 'remote' section)? + untrusted-mode = false # Enable untrusted mode for full security of server managed actors, allows untrusted clients to connect. backlog = 4096 # Sets the size of the connection backlog } @@ -263,7 +265,7 @@ akka { max-time-to-wait-until-connected = 30 session-timeout = 60 connection-timeout = 60 - include-ref-node-in-replica-set = on # Can a replica be instantiated on the same node as the cluster reference to the actor + include-ref-node-in-replica-set = true # Can a replica be instantiated on the same node as the cluster reference to the actor # Default: on log-directory = "_akka_cluster" # Where ZooKeeper should store the logs and data files @@ -277,23 +279,11 @@ akka { } } - stm { - fair = on # Should global transactions be fair or non-fair (non fair yield better performance) - max-retries = 1000 - timeout = 5 # Default timeout for blocking transactions and transaction set (in unit defined by - # the time-unit property) - write-skew = true - blocking-allowed = false - interruptible = false - speculative = true - quick-release = true - propagation = "requires" - trace-level = "none" - } - + # TODO move to testkit-reference test { timefactor = "1.0" # factor by which to scale timeouts during tests, e.g. to account for shared build system load filter-leeway = 3 # time-units EventFilter.intercept waits after the block is finished until all required messages are received single-expect-default = 3 # time-units to wait in expectMsg and friends outside of within() block by default } + } diff --git a/akka-actor/src/main/scala/akka/actor/Actor.scala b/akka-actor/src/main/scala/akka/actor/Actor.scala index 30eebb2f87..4224eff8e6 100644 --- a/akka-actor/src/main/scala/akka/actor/Actor.scala +++ b/akka-actor/src/main/scala/akka/actor/Actor.scala @@ -6,7 +6,6 @@ package akka.actor import DeploymentConfig._ import akka.dispatch._ -import akka.config._ import akka.routing._ import akka.util.Duration import akka.remote.RemoteSupport diff --git a/akka-actor/src/main/scala/akka/actor/ActorSystem.scala b/akka-actor/src/main/scala/akka/actor/ActorSystem.scala index aaa307c228..404dd3b76e 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorSystem.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorSystem.scala @@ -3,7 +3,7 @@ */ package akka.actor -import akka.config._ +import akka.config.ConfigurationException import akka.actor._ import akka.event._ import akka.dispatch._ @@ -16,6 +16,11 @@ import akka.serialization.Serialization import akka.remote.RemoteAddress import org.jboss.netty.akka.util.HashedWheelTimer import java.util.concurrent.{ Executors, TimeUnit } +import java.io.File +import com.typesafe.config.Config +import com.typesafe.config.ConfigParseOptions +import com.typesafe.config.ConfigRoot +import com.typesafe.config.ConfigFactory object ActorSystem { @@ -33,39 +38,11 @@ object ActorSystem { val GlobalHome = systemHome orElse envHome - val envConf = System.getenv("AKKA_MODE") match { - case null | "" ⇒ None - case value ⇒ Some(value) - } - - val systemConf = System.getProperty("akka.mode") match { - case null | "" ⇒ None - case value ⇒ Some(value) - } - - val defaultLocation = (systemConf orElse envConf).map("akka." + _ + ".conf").getOrElse("akka.conf") - - val fromProperties = try { - Some(Configuration.fromFile(System.getProperty("akka.config", ""))) - } catch { case _ ⇒ None } - - val fromClasspath = try { - Some(Configuration.fromResource(defaultLocation, getClass.getClassLoader)) - } catch { case _ ⇒ None } - - val fromHome = try { - Some(Configuration.fromFile(GlobalHome.get + "/config/" + defaultLocation)) - } catch { case _ ⇒ None } - - val emptyConfig = Configuration.fromString("akka { version = \"" + Version + "\" }") - - val defaultConfig = fromProperties orElse fromClasspath orElse fromHome getOrElse emptyConfig - - def create(name: String, config: Configuration): ActorSystem = apply(name, config) - def apply(name: String, config: Configuration): ActorSystem = new ActorSystemImpl(name, config).start() + def create(name: String, config: Config): ActorSystem = apply(name, config) + def apply(name: String, config: Config): ActorSystem = new ActorSystemImpl(name, config).start() def create(name: String): ActorSystem = apply(name) - def apply(name: String): ActorSystem = apply(name, defaultConfig) + def apply(name: String): ActorSystem = apply(name, DefaultConfigurationLoader.defaultConfig) def create(): ActorSystem = apply() def apply(): ActorSystem = apply("default") @@ -74,52 +51,54 @@ object ActorSystem { case object Stopped extends ExitStatus case class Failed(cause: Throwable) extends ExitStatus - class Settings(val config: Configuration) { + class Settings(cfg: Config) { + val config: ConfigRoot = ConfigFactory.emptyRoot("akka").withFallback(cfg).withFallback(DefaultConfigurationLoader.referenceConfig).resolve() + + import scala.collection.JavaConverters._ + import akka.config.ConfigImplicits._ import config._ - val ConfigVersion = getString("akka.version", Version) + val ConfigVersion = getString("akka.version") - val ProviderClass = getString("akka.actor.provider", "akka.actor.LocalActorRefProvider") + val ProviderClass = getString("akka.actor.provider") - val DefaultTimeUnit = Duration.timeUnit(getString("akka.time-unit", "seconds")) - val ActorTimeout = Timeout(Duration(getInt("akka.actor.timeout", 5), DefaultTimeUnit)) + val DefaultTimeUnit = Duration.timeUnit(getString("akka.time-unit")) + val ActorTimeout = Timeout(Duration(getInt("akka.actor.timeout"), DefaultTimeUnit)) val ActorTimeoutMillis = ActorTimeout.duration.toMillis - val SerializeAllMessages = getBool("akka.actor.serialize-messages", false) + val SerializeAllMessages = getBoolean("akka.actor.serialize-messages") - val TestTimeFactor = - try java.lang.Double.parseDouble(System.getProperty("akka.test.timefactor")) catch { - case _: Exception ⇒ getDouble("akka.test.timefactor", 1.0) - } - val SingleExpectDefaultTimeout = Duration(getDouble("akka.test.single-expect-default", 1), DefaultTimeUnit) - val TestEventFilterLeeway = Duration(getDouble("akka.test.filter-leeway", 0.5), DefaultTimeUnit) + val TestTimeFactor = getDouble("akka.test.timefactor") + val SingleExpectDefaultTimeout = Duration(getDouble("akka.test.single-expect-default"), DefaultTimeUnit) + val TestEventFilterLeeway = Duration(getDouble("akka.test.filter-leeway"), DefaultTimeUnit) - val LogLevel = getString("akka.loglevel", "INFO") - val StdoutLogLevel = getString("akka.stdout-loglevel", LogLevel) - val EventHandlers = getList("akka.event-handlers") - val AddLoggingReceive = getBool("akka.actor.debug.receive", false) - val DebugAutoReceive = getBool("akka.actor.debug.autoreceive", false) - val DebugLifecycle = getBool("akka.actor.debug.lifecycle", false) - val FsmDebugEvent = getBool("akka.actor.debug.fsm", false) - val DebugEventStream = getBool("akka.actor.debug.event-stream", false) + val LogLevel = getString("akka.loglevel") + val StdoutLogLevel = getString("akka.stdout-loglevel") + val EventHandlers: Seq[String] = getStringList("akka.event-handlers").asScala + val AddLoggingReceive = getBoolean("akka.actor.debug.receive") + val DebugAutoReceive = getBoolean("akka.actor.debug.autoreceive") + val DebugLifecycle = getBoolean("akka.actor.debug.lifecycle") + val FsmDebugEvent = getBoolean("akka.actor.debug.fsm") + val DebugEventStream = getBoolean("akka.actor.debug.event-stream") - val DispatcherThroughput = getInt("akka.actor.throughput", 5) - val DispatcherDefaultShutdown = getLong("akka.actor.dispatcher-shutdown-timeout"). - map(time ⇒ Duration(time, DefaultTimeUnit)).getOrElse(1 second) - val MailboxCapacity = getInt("akka.actor.default-dispatcher.mailbox-capacity", -1) - val MailboxPushTimeout = Duration(getInt("akka.actor.default-dispatcher.mailbox-push-timeout-time", 10), DefaultTimeUnit) - val DispatcherThroughputDeadlineTime = Duration(getInt("akka.actor.throughput-deadline-time", -1), DefaultTimeUnit) + val DispatcherThroughput = getInt("akka.actor.default-dispatcher.throughput") + val DispatcherDefaultShutdown = Duration(getLong("akka.actor.dispatcher-shutdown-timeout"), DefaultTimeUnit) + val MailboxCapacity = getInt("akka.actor.default-dispatcher.mailbox-capacity") + val MailboxPushTimeout = Duration(getInt("akka.actor.default-dispatcher.mailbox-push-timeout-time"), DefaultTimeUnit) + val DispatcherThroughputDeadlineTime = Duration(getInt("akka.actor.default-dispatcher.throughput-deadline-time"), DefaultTimeUnit) - val Home = getString("akka.home") - val BootClasses = getList("akka.boot") + val Home = config.getStringOption("akka.home") + val BootClasses: Seq[String] = getStringList("akka.boot").asScala - val EnabledModules = getList("akka.enabled-modules") + val EnabledModules: Seq[String] = getStringList("akka.enabled-modules").asScala + + // TODO move to cluster extension val ClusterEnabled = EnabledModules exists (_ == "cluster") - val ClusterName = getString("akka.cluster.name", "default") + val ClusterName = getString("akka.cluster.name") - val RemoteTransport = getString("akka.remote.layer", "akka.remote.netty.NettyRemoteSupport") - val RemoteServerPort = getInt("akka.remote.server.port", 2552) - - val FailureDetectorThreshold: Int = getInt("akka.remote.failure-detector.threshold", 8) - val FailureDetectorMaxSampleSize: Int = getInt("akka.remote.failure-detector.max-sample-size", 1000) + // TODO move to remote extension + val RemoteTransport = getString("akka.remote.layer") + val RemoteServerPort = getInt("akka.remote.server.port") + val FailureDetectorThreshold = getInt("akka.remote.failure-detector.threshold") + val FailureDetectorMaxSampleSize = getInt("akka.remote.failure-detector.max-sample-size") if (ConfigVersion != Version) throw new ConfigurationException("Akka JAR version [" + Version + @@ -127,6 +106,48 @@ object ActorSystem { } + object DefaultConfigurationLoader { + + lazy val defaultConfig: Config = fromProperties orElse fromClasspath orElse fromHome getOrElse emptyConfig + + val envConf = System.getenv("AKKA_MODE") match { + case null | "" ⇒ None + case value ⇒ Some(value) + } + + val systemConf = System.getProperty("akka.mode") match { + case null | "" ⇒ None + case value ⇒ Some(value) + } + + // file extensions (.conf, .json, .properties), are handled by parseFileAnySyntax + val defaultLocation = (systemConf orElse envConf).map("akka." + _).getOrElse("akka") + private def configParseOptions = ConfigParseOptions.defaults.setAllowMissing(false) + + lazy val fromProperties = try { + val property = Option(System.getProperty("akka.config")) + property.map(p ⇒ + ConfigFactory.systemProperties().withFallback( + ConfigFactory.parseFileAnySyntax(new File(p), configParseOptions))) + } catch { case _ ⇒ None } + + lazy val fromClasspath = try { + Option(ConfigFactory.systemProperties().withFallback( + ConfigFactory.parseResourceAnySyntax(ActorSystem.getClass, "/" + defaultLocation, configParseOptions))) + } catch { case _ ⇒ None } + + lazy val fromHome = try { + Option(ConfigFactory.systemProperties().withFallback( + ConfigFactory.parseFileAnySyntax(new File(GlobalHome.get + "/config/" + defaultLocation), configParseOptions))) + } catch { case _ ⇒ None } + + val referenceConfig: Config = + ConfigFactory.parseResource(classOf[ActorSystem], "/akka-actor-reference.conf", configParseOptions) + + lazy val emptyConfig = ConfigFactory.systemProperties() + + } + } abstract class ActorSystem extends ActorRefFactory with TypedActorFactory { @@ -165,7 +186,7 @@ abstract class ActorSystem extends ActorRefFactory with TypedActorFactory { def stop() } -class ActorSystemImpl(val name: String, config: Configuration) extends ActorSystem { +class ActorSystemImpl(val name: String, config: Config) extends ActorSystem { import ActorSystem._ diff --git a/akka-actor/src/main/scala/akka/actor/Deployer.scala b/akka-actor/src/main/scala/akka/actor/Deployer.scala index 4b5d64bde6..9e4579bc48 100644 --- a/akka-actor/src/main/scala/akka/actor/Deployer.scala +++ b/akka-actor/src/main/scala/akka/actor/Deployer.scala @@ -9,11 +9,12 @@ import java.util.concurrent.ConcurrentHashMap import akka.event.Logging import akka.actor.DeploymentConfig._ import akka.AkkaException -import akka.config.{ Configuration, ConfigurationException } +import akka.config.ConfigurationException import akka.util.Duration import java.net.InetSocketAddress import akka.remote.RemoteAddress import akka.event.EventStream +import com.typesafe.config.Config trait ActorDeployer { private[akka] def init(deployments: Seq[Deploy]): Unit @@ -84,34 +85,40 @@ class Deployer(val settings: ActorSystem.Settings, val eventStream: EventStream, } private[akka] def pathsInConfig: List[String] = { - val deploymentPath = "akka.actor.deployment" - settings.config.getSection(deploymentPath) match { - case None ⇒ Nil - case Some(pathConfig) ⇒ - pathConfig.map.keySet - .map(path ⇒ path.substring(0, path.indexOf("."))) - .toSet.toList // toSet to force uniqueness + def pathSubstring(path: String) = { + val i = path.indexOf(".") + if (i == -1) path else path.substring(0, i) } + + import scala.collection.JavaConverters._ + settings.config.getConfig("akka.actor.deployment").toObject.keySet.asScala + .filterNot(path ⇒ path == "default") + .map(path ⇒ pathSubstring(path)) + .toSet.toList // toSet to force uniqueness } /** * Lookup deployment in 'akka.conf' configuration file. */ - private[akka] def lookupInConfig(path: String, configuration: Configuration = settings.config): Option[Deploy] = { + private[akka] def lookupInConfig(path: String, configuration: Config = settings.config): Option[Deploy] = { + import scala.collection.JavaConverters._ + import akka.config.ConfigImplicits._ import akka.util.ReflectiveAccess.{ createInstance, emptyArguments, emptyParams, getClassFor } + val defaultDeploymentConfig = configuration.getConfig("akka.actor.deployment.default") + // -------------------------------- // akka.actor.deployment. // -------------------------------- val deploymentKey = "akka.actor.deployment." + path - configuration.getSection(deploymentKey) match { + configuration.getConfigOption(deploymentKey) match { case None ⇒ None - case Some(pathConfig) ⇒ - + case Some(sectionConfig) ⇒ + val pathConfig = configuration.getConfig(deploymentKey).withFallback(defaultDeploymentConfig) // -------------------------------- // akka.actor.deployment..router // -------------------------------- - val router: Routing = pathConfig.getString("router", "direct") match { + val router: Routing = pathConfig.getString("router") match { case "direct" ⇒ Direct case "round-robin" ⇒ RoundRobin case "random" ⇒ Random @@ -128,13 +135,13 @@ class Deployer(val settings: ActorSystem.Settings, val eventStream: EventStream, val nrOfInstances = { if (router == Direct) OneNrOfInstances else { - pathConfig.getAny("nr-of-instances", "1") match { + pathConfig.getAnyRef("nr-of-instances").asInstanceOf[Any] match { case "auto" ⇒ AutoNrOfInstances - case "1" ⇒ OneNrOfInstances - case "0" ⇒ ZeroNrOfInstances - case nrOfReplicas: String ⇒ + case 1 ⇒ OneNrOfInstances + case 0 ⇒ ZeroNrOfInstances + case nrOfReplicas: Number ⇒ try { - new NrOfInstances(nrOfReplicas.toInt) + new NrOfInstances(nrOfReplicas.intValue) } catch { case e: Exception ⇒ throw new ConfigurationException( @@ -149,31 +156,25 @@ class Deployer(val settings: ActorSystem.Settings, val eventStream: EventStream, // -------------------------------- // akka.actor.deployment..create-as // -------------------------------- - val recipe: Option[ActorRecipe] = pathConfig.getSection("create-as") map { section ⇒ - val implementationClass = section.getString("class") match { - case Some(impl) ⇒ - getClassFor[Actor](impl).fold(e ⇒ throw new ConfigurationException( + val recipe: Option[ActorRecipe] = + pathConfig.getString("create-as.class") match { + case "" ⇒ None + case impl ⇒ + val implementationClass = getClassFor[Actor](impl).fold(e ⇒ throw new ConfigurationException( "Config option [" + deploymentKey + ".create-as.class] load failed", e), identity) - case None ⇒ - throw new ConfigurationException( - "Config option [" + deploymentKey + ".create-as.class] is missing, need the fully qualified name of the class") + Some(ActorRecipe(implementationClass)) } - ActorRecipe(implementationClass) - } // -------------------------------- // akka.actor.deployment..remote // -------------------------------- - pathConfig.getSection("remote") match { + pathConfig.getConfigOption("remote") match { case Some(remoteConfig) ⇒ // we have a 'remote' config section - if (pathConfig.getSection("cluster").isDefined) throw new ConfigurationException( - "Configuration for deployment ID [" + path + "] can not have both 'remote' and 'cluster' sections.") - // -------------------------------- // akka.actor.deployment..remote.nodes // -------------------------------- - val remoteAddresses = remoteConfig.getList("nodes") match { + val remoteAddresses = remoteConfig.getStringList("nodes").asScala.toSeq match { case Nil ⇒ Nil case nodes ⇒ def raiseRemoteNodeParsingError() = throw new ConfigurationException( @@ -200,7 +201,7 @@ class Deployer(val settings: ActorSystem.Settings, val eventStream: EventStream, // -------------------------------- // akka.actor.deployment..cluster // -------------------------------- - pathConfig.getSection("cluster") match { + pathConfig.getConfigOption("cluster") match { case None ⇒ None case Some(clusterConfig) ⇒ @@ -208,7 +209,7 @@ class Deployer(val settings: ActorSystem.Settings, val eventStream: EventStream, // akka.actor.deployment..cluster.preferred-nodes // -------------------------------- - val preferredNodes = clusterConfig.getList("preferred-nodes") match { + val preferredNodes = clusterConfig.getStringList("preferred-nodes").asScala.toSeq match { case Nil ⇒ Nil case homes ⇒ def raiseHomeConfigError() = throw new ConfigurationException( @@ -233,12 +234,13 @@ class Deployer(val settings: ActorSystem.Settings, val eventStream: EventStream, // -------------------------------- // akka.actor.deployment..cluster.replication // -------------------------------- - clusterConfig.getSection("replication") match { + clusterConfig.getConfigOption("replication") match { case None ⇒ Some(Deploy(path, recipe, router, nrOfInstances, deploymentConfig.ClusterScope(preferredNodes, Transient))) case Some(replicationConfig) ⇒ - val storage = replicationConfig.getString("storage", "transaction-log") match { + val replicationConfigWithFallback = replicationConfig.withFallback(configuration.getConfig("akka.actor.deployment.default.replication")) + val storage = replicationConfigWithFallback.getString("storage") match { case "transaction-log" ⇒ TransactionLog case "data-grid" ⇒ DataGrid case unknown ⇒ @@ -246,7 +248,7 @@ class Deployer(val settings: ActorSystem.Settings, val eventStream: EventStream, ".cluster.replication.storage] needs to be either [\"transaction-log\"] or [\"data-grid\"] - was [" + unknown + "]") } - val strategy = replicationConfig.getString("strategy", "write-through") match { + val strategy = replicationConfigWithFallback.getString("strategy") match { case "write-through" ⇒ WriteThrough case "write-behind" ⇒ WriteBehind case unknown ⇒ diff --git a/akka-actor/src/main/scala/akka/config/ConfigImplicits.scala b/akka-actor/src/main/scala/akka/config/ConfigImplicits.scala new file mode 100644 index 0000000000..b6dc622a95 --- /dev/null +++ b/akka-actor/src/main/scala/akka/config/ConfigImplicits.scala @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2009-2011 Typesafe Inc. + */ + +package akka.config +import com.typesafe.config.Config + +object ConfigImplicits { + implicit def decorateConfig(config: Config) = new ConfigWrapper(config) +} + +class ConfigWrapper(config: Config) { + + def getStringOption(path: String): Option[String] = { + config.hasPath(path) match { + case false ⇒ None + case true ⇒ Some(config.getString(path)) + } + } + + def getConfigOption(path: String): Option[Config] = { + config.hasPath(path) match { + case false ⇒ None + case true ⇒ Some(config.getConfig(path)) + } + } + +} \ No newline at end of file diff --git a/akka-actor/src/main/scala/akka/config/ConfigParser.scala b/akka-actor/src/main/scala/akka/config/ConfigParser.scala deleted file mode 100644 index 4b3d4abdaa..0000000000 --- a/akka-actor/src/main/scala/akka/config/ConfigParser.scala +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (C) 2009-2011 Typesafe Inc. - * - * Based on Configgy by Robey Pointer. - * Copyright 2009 Robey Pointer - * http://www.apache.org/licenses/LICENSE-2.0 - */ - -package akka.config - -import scala.collection.mutable -import scala.util.parsing.combinator._ - -class ConfigParser(var prefix: String = "", map: mutable.Map[String, Any] = mutable.Map.empty[String, Any], importer: Importer) extends RegexParsers { - val sections = mutable.Stack[String]() - - def createPrefix = { - prefix = if (sections.isEmpty) "" else sections.toList.reverse.mkString("", ".", ".") - } - - override val whiteSpace = """(\s+|#[^\n]*\n)+""".r - - // tokens - - val numberToken: Parser[String] = """-?\d+(\.\d+)?""".r - val stringToken: Parser[String] = ("\"" + """([^\\\"]|\\[^ux]|\\\n|\\u[0-9a-fA-F]{4}|\\x[0-9a-fA-F]{2})*""" + "\"").r - val booleanToken: Parser[String] = "(true|on|false|off)".r - val identToken: Parser[String] = """([\da-zA-Z_/][-\w]*)(\.[a-zA-Z_/][-/\w]*)*""".r - val assignToken: Parser[String] = "=".r - val sectionToken: Parser[String] = """[a-zA-Z_/][-/\w]*""".r - - // values - - def value: Parser[Any] = number | string | list | boolean - def number = numberToken - def string = stringToken ^^ { s ⇒ s.substring(1, s.length - 1) } - def list = "[" ~> repsep(string | numberToken, opt(",")) <~ (opt(",") ~ "]") - def boolean = booleanToken - - // parser - - def root = rep(includeFile | assignment | sectionOpen | sectionClose) - - def includeFile = "include" ~> string ^^ { - case filename: String ⇒ - new ConfigParser(prefix, map, importer) parse importer.importFile(filename) - } - - def assignment = identToken ~ assignToken ~ value ^^ { - case k ~ a ~ v ⇒ map(prefix + k) = v - } - - def sectionOpen = sectionToken <~ "{" ^^ { name ⇒ - sections push name - createPrefix - } - - def sectionClose = "}" ^^ { _ ⇒ - if (sections.isEmpty) { - failure("dangling close tag") - } else { - sections.pop - createPrefix - } - } - - def parse(in: String): Map[String, Any] = { - parseAll(root, in) match { - case Success(result, _) ⇒ map.toMap - case x @ Failure(msg, _) ⇒ throw new ConfigurationException(x.toString) - case x @ Error(msg, _) ⇒ throw new ConfigurationException(x.toString) - } - } -} diff --git a/akka-actor/src/main/scala/akka/config/Configuration.scala b/akka-actor/src/main/scala/akka/config/Configuration.scala deleted file mode 100644 index bbd7b9a6c4..0000000000 --- a/akka-actor/src/main/scala/akka/config/Configuration.scala +++ /dev/null @@ -1,194 +0,0 @@ -/** - * Copyright (C) 2009-2011 Typesafe Inc. - * - * Based on Configgy by Robey Pointer. - * Copyright 2009 Robey Pointer - * http://www.apache.org/licenses/LICENSE-2.0 - */ - -package akka.config - -import java.io.File - -object Configuration { - val DefaultPath = new File(".").getCanonicalPath - val DefaultImporter = new FilesystemImporter(DefaultPath) - - val outputConfigSources = System.getProperty("akka.output.config.source") ne null - - def load(data: String, importer: Importer = DefaultImporter): Configuration = { - val parser = new ConfigParser(importer = importer) - new Configuration(parser parse data) - } - - def fromFile(filename: String, importer: Importer): Configuration = { - load(importer.importFile(filename), importer) - } - - def fromFile(path: String, filename: String): Configuration = { - val importer = new FilesystemImporter(path) - fromFile(filename, importer) - } - - def fromFile(filename: String): Configuration = { - val n = filename.lastIndexOf('/') - if (n < 0) { - fromFile(DefaultPath, filename) - } else { - fromFile(filename.substring(0, n), filename.substring(n + 1)) - } - } - - def fromResource(filename: String): Configuration = { - fromResource(filename, ClassLoader.getSystemClassLoader) - } - - def fromResource(filename: String, classLoader: ClassLoader): Configuration = { - val importer = new ResourceImporter(classLoader) - fromFile(filename, importer) - } - - def fromMap(map: Map[String, Any]) = { - new Configuration(map) - } - - def fromString(data: String): Configuration = { - load(data) - } - - def apply(pairs: (String, Any)*) = { - new Configuration(Map(pairs: _*)) - } -} - -class Configuration(val map: Map[String, Any]) { - private val trueValues = Set("true", "on") - private val falseValues = Set("false", "off") - - def ++(other: Configuration) = new Configuration(map ++ other.map) - - private def outputIfDesiredAndReturnInput[T](key: String, t: T): T = { - if (Configuration.outputConfigSources) - println("Akka config is using default value for: " + key) - t - } - - def contains(key: String): Boolean = map contains key - - def keys: Iterable[String] = map.keys - - def getAny(key: String): Option[Any] = { - try { - Some(map(key)) - } catch { - case _ ⇒ None - } - } - - def getAny(key: String, defaultValue: Any): Any = - getAny(key).getOrElse(outputIfDesiredAndReturnInput(key, defaultValue)) - - def getListAny(key: String): Seq[Any] = { - try { - map(key).asInstanceOf[Seq[Any]] - } catch { - case _ ⇒ Seq.empty[Any] - } - } - - def getString(key: String): Option[String] = map.get(key).map(_.toString) - - def getString(key: String, defaultValue: String): String = - getString(key).getOrElse(outputIfDesiredAndReturnInput(key, defaultValue)) - - def getList(key: String): Seq[String] = { - try { - map(key).asInstanceOf[Seq[String]] - } catch { - case _ ⇒ Seq.empty[String] - } - } - - def getInt(key: String): Option[Int] = { - try { - Some(map(key).toString.toInt) - } catch { - case _ ⇒ None - } - } - - def getInt(key: String, defaultValue: Int): Int = - getInt(key).getOrElse(outputIfDesiredAndReturnInput(key, defaultValue)) - - def getLong(key: String): Option[Long] = { - try { - Some(map(key).toString.toLong) - } catch { - case _ ⇒ None - } - } - - def getLong(key: String, defaultValue: Long): Long = - getLong(key).getOrElse(outputIfDesiredAndReturnInput(key, defaultValue)) - - def getFloat(key: String): Option[Float] = { - try { - Some(map(key).toString.toFloat) - } catch { - case _ ⇒ None - } - } - - def getFloat(key: String, defaultValue: Float): Float = - getFloat(key).getOrElse(outputIfDesiredAndReturnInput(key, defaultValue)) - - def getDouble(key: String): Option[Double] = { - try { - Some(map(key).toString.toDouble) - } catch { - case _ ⇒ None - } - } - - def getDouble(key: String, defaultValue: Double): Double = - getDouble(key).getOrElse(outputIfDesiredAndReturnInput(key, defaultValue)) - - def getBoolean(key: String): Option[Boolean] = { - getString(key) flatMap { s ⇒ - val isTrue = trueValues.contains(s) - if (!isTrue && !falseValues.contains(s)) None - else Some(isTrue) - } - } - - def getBoolean(key: String, defaultValue: Boolean): Boolean = - getBool(key).getOrElse(outputIfDesiredAndReturnInput(key, defaultValue)) - - def getBool(key: String): Option[Boolean] = getBoolean(key) - - def getBool(key: String, defaultValue: Boolean): Boolean = - getBoolean(key, defaultValue) - - def apply(key: String): String = getString(key) match { - case None ⇒ throw new ConfigurationException("undefined config: " + key) - case Some(v) ⇒ v - } - - def apply(key: String, defaultValue: String) = getString(key, defaultValue) - - def apply(key: String, defaultValue: Int) = getInt(key, defaultValue) - - def apply(key: String, defaultValue: Long) = getLong(key, defaultValue) - - def apply(key: String, defaultValue: Boolean) = getBool(key, defaultValue) - - def getSection(name: String): Option[Configuration] = { - val l = name.length + 1 - val pattern = name + "." - val m = map.collect { - case (k, v) if k.startsWith(pattern) ⇒ (k.substring(l), v) - } - if (m.isEmpty) None - else Some(new Configuration(m)) - } -} diff --git a/akka-actor/src/main/scala/akka/config/Config.scala b/akka-actor/src/main/scala/akka/config/ConfigurationException.scala similarity index 100% rename from akka-actor/src/main/scala/akka/config/Config.scala rename to akka-actor/src/main/scala/akka/config/ConfigurationException.scala diff --git a/akka-actor/src/main/scala/akka/config/Importer.scala b/akka-actor/src/main/scala/akka/config/Importer.scala deleted file mode 100644 index 2198ae58c5..0000000000 --- a/akka-actor/src/main/scala/akka/config/Importer.scala +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (C) 2009-2011 Typesafe Inc. - * - * Based on Configgy by Robey Pointer. - * Copyright 2009 Robey Pointer - * http://www.apache.org/licenses/LICENSE-2.0 - */ - -package akka.config - -import java.io.{ BufferedReader, File, FileInputStream, InputStream, InputStreamReader } - -/** - * An interface for finding config files and reading them into strings for - * parsing. This is used to handle `include` directives in config files. - */ -trait Importer { - - def importFile(filename: String): String - - private val BUFFER_SIZE = 8192 - - protected def streamToString(in: InputStream): String = { - try { - val reader = new BufferedReader(new InputStreamReader(in, "UTF-8")) - val buffer = new Array[Char](BUFFER_SIZE) - val sb = new StringBuilder - var n = 0 - while (n >= 0) { - n = reader.read(buffer, 0, buffer.length) - if (n >= 0) { - sb.appendAll(buffer, 0, n) - } - } - in.close() - sb.toString - } catch { - case x ⇒ throw new ConfigurationException(x.toString) - } - } -} - -/** - * An Importer that looks for imported config files in the filesystem. - * This is the default importer. - */ -class FilesystemImporter(val baseDir: String) extends Importer { - def importFile(filename: String): String = { - val f = new File(filename) - val file = if (f.isAbsolute) f else new File(baseDir, filename) - streamToString(new FileInputStream(file)) - } -} - -/** - * An Importer that looks for imported config files in the java resources - * of the system class loader (usually the jar used to launch this system). - */ -class ResourceImporter(classLoader: ClassLoader) extends Importer { - def importFile(filename: String): String = { - val stream = classLoader.getResourceAsStream(filename) - streamToString(stream) - } -} diff --git a/akka-actor/src/main/scala/akka/dispatch/AbstractDispatcher.scala b/akka-actor/src/main/scala/akka/dispatch/AbstractDispatcher.scala index 3d66532d31..107a86daaa 100644 --- a/akka-actor/src/main/scala/akka/dispatch/AbstractDispatcher.scala +++ b/akka-actor/src/main/scala/akka/dispatch/AbstractDispatcher.scala @@ -6,7 +6,6 @@ package akka.dispatch import java.util.concurrent._ import akka.event.Logging.Error -import akka.config.Configuration import akka.util.{ Duration, Switch, ReentrantGuard } import atomic.{ AtomicInteger, AtomicLong } import java.util.concurrent.ThreadPoolExecutor.{ AbortPolicy, CallerRunsPolicy, DiscardOldestPolicy, DiscardPolicy } @@ -16,6 +15,7 @@ import locks.ReentrantLock import scala.annotation.tailrec import akka.event.EventStream import akka.actor.ActorSystem.Settings +import com.typesafe.config.Config /** * @author Jonas Bonér @@ -292,33 +292,32 @@ abstract class MessageDispatcherConfigurator() { /** * Returns an instance of MessageDispatcher given a Configuration */ - def configure(config: Configuration, settings: Settings, prerequisites: DispatcherPrerequisites): MessageDispatcher + def configure(config: Config, settings: Settings, prerequisites: DispatcherPrerequisites): MessageDispatcher - def mailboxType(config: Configuration, settings: Settings): MailboxType = { - val capacity = config.getInt("mailbox-capacity", settings.MailboxCapacity) + def mailboxType(config: Config, settings: Settings): MailboxType = { + val capacity = config.getInt("mailbox-capacity") if (capacity < 1) UnboundedMailbox() else { - val duration = Duration( - config.getInt("mailbox-push-timeout-time", settings.MailboxPushTimeout.toMillis.toInt), - settings.DefaultTimeUnit) + val duration = Duration(config.getInt("mailbox-push-timeout-time"), settings.DefaultTimeUnit) BoundedMailbox(capacity, duration) } } - def configureThreadPool(config: Configuration, + def configureThreadPool(config: Config, settings: Settings, createDispatcher: ⇒ (ThreadPoolConfig) ⇒ MessageDispatcher): ThreadPoolConfigDispatcherBuilder = { import ThreadPoolConfigDispatcherBuilder.conf_? //Apply the following options to the config if they are present in the config + ThreadPoolConfigDispatcherBuilder(createDispatcher, ThreadPoolConfig()).configure( - conf_?(config getInt "keep-alive-time")(time ⇒ _.setKeepAliveTime(Duration(time, settings.DefaultTimeUnit))), - conf_?(config getDouble "core-pool-size-factor")(factor ⇒ _.setCorePoolSizeFromFactor(factor)), - conf_?(config getDouble "max-pool-size-factor")(factor ⇒ _.setMaxPoolSizeFromFactor(factor)), - conf_?(config getBool "allow-core-timeout")(allow ⇒ _.setAllowCoreThreadTimeout(allow)), - conf_?(config getInt "task-queue-size" flatMap { + conf_?(Some(config getInt "keep-alive-time"))(time ⇒ _.setKeepAliveTime(Duration(time, settings.DefaultTimeUnit))), + conf_?(Some(config getDouble "core-pool-size-factor"))(factor ⇒ _.setCorePoolSizeFromFactor(factor)), + conf_?(Some(config getDouble "max-pool-size-factor"))(factor ⇒ _.setMaxPoolSizeFromFactor(factor)), + conf_?(Some(config getBoolean "allow-core-timeout"))(allow ⇒ _.setAllowCoreThreadTimeout(allow)), + conf_?(Some(config getInt "task-queue-size") flatMap { case size if size > 0 ⇒ - config getString "task-queue-type" map { + Some(config getString "task-queue-type") map { case "array" ⇒ ThreadPoolConfig.arrayBlockingQueue(size, false) //TODO config fairness? case "" | "linked" ⇒ ThreadPoolConfig.linkedBlockingQueue(size) case x ⇒ throw new IllegalArgumentException("[%s] is not a valid task-queue-type [array|linked]!" format x) diff --git a/akka-actor/src/main/scala/akka/dispatch/Dispatchers.scala b/akka-actor/src/main/scala/akka/dispatch/Dispatchers.scala index 614fc3e939..52f04c540d 100644 --- a/akka-actor/src/main/scala/akka/dispatch/Dispatchers.scala +++ b/akka-actor/src/main/scala/akka/dispatch/Dispatchers.scala @@ -7,12 +7,13 @@ package akka.dispatch import akka.actor.LocalActorRef import akka.actor.newUuid import akka.util.{ Duration, ReflectiveAccess } -import akka.config.Configuration import java.util.concurrent.TimeUnit import akka.actor.ActorSystem import akka.event.EventStream import akka.actor.Scheduler import akka.actor.ActorSystem.Settings +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory trait DispatcherPrerequisites { def eventStream: EventStream @@ -63,8 +64,11 @@ class Dispatchers(val settings: ActorSystem.Settings, val prerequisites: Dispatc else BoundedMailbox(settings.MailboxCapacity, settings.MailboxPushTimeout) val DispatcherShutdownMillis = settings.DispatcherDefaultShutdown.toMillis + val defaultDispatcherConfig = settings.config.getConfig("akka.actor.default-dispatcher") + + // TODO PN Shouldn't we fail hard if default-dispatcher is wrong? lazy val defaultGlobalDispatcher = - settings.config.getSection("akka.actor.default-dispatcher").flatMap(from) getOrElse newDispatcher("AkkaDefaultGlobalDispatcher", 1, MailboxType).build + from(defaultDispatcherConfig) getOrElse newDispatcher("AkkaDefaultGlobalDispatcher", 1, MailboxType).build /** * Creates an thread based dispatcher serving a single actor through the same single thread. @@ -170,11 +174,21 @@ class Dispatchers(val settings: ActorSystem.Settings, val prerequisites: Dispatc * Utility function that tries to load the specified dispatcher config from the akka.conf * or else use the supplied default dispatcher */ - def fromConfig(key: String, default: ⇒ MessageDispatcher = defaultGlobalDispatcher): MessageDispatcher = - settings.config getSection key flatMap from getOrElse default + def fromConfig(key: String, default: ⇒ MessageDispatcher = defaultGlobalDispatcher, cfg: Config = settings.config): MessageDispatcher = { + import akka.config.ConfigImplicits._ + import scala.collection.JavaConverters._ + def simpleName = key.substring(key.lastIndexOf('.') + 1) + cfg.getConfigOption(key) match { + case None ⇒ default + case Some(conf) ⇒ + val confWithName = conf.withFallback(ConfigFactory.parseMap(Map("name" -> simpleName).asJava)) + from(confWithName).getOrElse(default) + } + } /* - * Creates of obtains a dispatcher from a ConfigMap according to the format below + * Creates of obtains a dispatcher from a ConfigMap according to the format below. + * Uses default values from default-dispatcher. * * default-dispatcher { * type = "Dispatcher" # Must be one of the following @@ -187,15 +201,16 @@ class Dispatchers(val settings: ActorSystem.Settings, val prerequisites: Dispatc * allow-core-timeout = on # Allow core threads to time out * throughput = 5 # Throughput for Dispatcher * } - * ex: from(config.getConfigMap(identifier).get) + * ex: from(config.getConfig(identifier).get) * * Gotcha: Only configures the dispatcher if possible - * Returns: None if "type" isn't specified in the config * Throws: IllegalArgumentException if the value of "type" is not valid * IllegalArgumentException if it cannot create the MessageDispatcherConfigurator */ - def from(cfg: Configuration): Option[MessageDispatcher] = { - cfg.getString("type") flatMap { + def from(cfg: Config): Option[MessageDispatcher] = { + val cfgWithFallback = cfg.withFallback(defaultDispatcherConfig) + + val dispatcherConfigurator = cfgWithFallback.getString("type") match { case "Dispatcher" ⇒ Some(new DispatcherConfigurator()) case "BalancingDispatcher" ⇒ Some(new BalancingDispatcherConfigurator()) case fqn ⇒ @@ -210,20 +225,20 @@ class Dispatchers(val settings: ActorSystem.Settings, val prerequisites: Dispatc case Left(exception) ⇒ throw new IllegalArgumentException("Unknown MessageDispatcherConfigurator type [%s]" format fqn, exception) } - } map { - _.configure(cfg, settings, prerequisites) } + + dispatcherConfigurator map (_.configure(cfgWithFallback, settings, prerequisites)) } } class DispatcherConfigurator() extends MessageDispatcherConfigurator() { - def configure(config: Configuration, settings: Settings, prerequisites: DispatcherPrerequisites): MessageDispatcher = { + def configure(config: Config, settings: Settings, prerequisites: DispatcherPrerequisites): MessageDispatcher = { configureThreadPool(config, settings, threadPoolConfig ⇒ new Dispatcher(prerequisites, - config.getString("name", newUuid.toString), - config.getInt("throughput", settings.DispatcherThroughput), - config.getInt("throughput-deadline-time", settings.DispatcherThroughputDeadlineTime.toMillis.toInt), + config.getString("name"), + config.getInt("throughput"), + config.getInt("throughput-deadline-time"), mailboxType(config, settings), threadPoolConfig, settings.DispatcherDefaultShutdown.toMillis)).build @@ -231,13 +246,13 @@ class DispatcherConfigurator() extends MessageDispatcherConfigurator() { } class BalancingDispatcherConfigurator() extends MessageDispatcherConfigurator() { - def configure(config: Configuration, settings: Settings, prerequisites: DispatcherPrerequisites): MessageDispatcher = { + def configure(config: Config, settings: Settings, prerequisites: DispatcherPrerequisites): MessageDispatcher = { configureThreadPool(config, settings, threadPoolConfig ⇒ new BalancingDispatcher(prerequisites, - config.getString("name", newUuid.toString), - config.getInt("throughput", settings.DispatcherThroughput), - config.getInt("throughput-deadline-time", settings.DispatcherThroughputDeadlineTime.toMillis.toInt), + config.getString("name"), + config.getInt("throughput"), + config.getInt("throughput-deadline-time"), mailboxType(config, settings), threadPoolConfig, settings.DispatcherDefaultShutdown.toMillis)).build diff --git a/akka-actor/src/main/scala/akka/serialization/Serialization.scala b/akka-actor/src/main/scala/akka/serialization/Serialization.scala index 9973b11a17..7e1df8382e 100644 --- a/akka-actor/src/main/scala/akka/serialization/Serialization.scala +++ b/akka-actor/src/main/scala/akka/serialization/Serialization.scala @@ -69,24 +69,26 @@ class Serialization(val system: ActorSystemImpl) { * By default always contains the following mapping: "default" -> akka.serialization.JavaSerializer * But "default" can be overridden in config */ - val serializers: Map[String, Serializer] = - system.settings.config.getSection("akka.actor.serializers") - .map(_.map) - .getOrElse(Map()) - .foldLeft(Map[String, Serializer]("default" -> akka.serialization.JavaSerializer)) { - case (result, (k: String, v: String)) ⇒ result + (k -> serializerOf(v).fold(throw _, identity)) - case (result, _) ⇒ result - } + val serializers: Map[String, Serializer] = { + import scala.collection.JavaConverters._ + val serializersConf = system.settings.config.getConfig("akka.actor.serializers").toObject.unwrapped.asScala.toMap + for ((k: String, v: String) ← serializersConf) + yield k -> serializerOf(v).fold(throw _, identity) + } /** * bindings is a Map whose keys = FQN of class that is serializable and values = the alias of the serializer to be used */ - val bindings: Map[String, String] = system.settings.config.getSection("akka.actor.serialization-bindings") map { - _.map.foldLeft(Map[String, String]()) { - case (result, (k: String, vs: List[_])) ⇒ result ++ (vs collect { case v: String ⇒ (v, k) }) //All keys which are lists, take the Strings from them and Map them - case (result, _) ⇒ result //For any other values, just skip them, TODO: print out warnings? - } - } getOrElse Map() + val bindings: Map[String, String] = { + import akka.config.ConfigImplicits._ + import scala.collection.JavaConverters._ + system.settings.config.getConfigOption("akka.actor.serialization-bindings") map { + _.toObject.unwrapped.asScala.foldLeft(Map[String, String]()) { + case (result, (k: String, vs: List[_])) ⇒ result ++ (vs collect { case v: String ⇒ (v, k) }) //All keys which are lists, take the Strings from them and Map them + case (result, _) ⇒ result //For any other values, just skip them, TODO: print out warnings? + } + } getOrElse Map() + } /** * serializerMap is a Map whose keys = FQN of class that is serializable and values = the FQN of the serializer to be used for that class diff --git a/akka-docs/scala/code/ActorDocSpec.scala b/akka-docs/scala/code/ActorDocSpec.scala index 8ec3daa2f2..fc942e1419 100644 --- a/akka-docs/scala/code/ActorDocSpec.scala +++ b/akka-docs/scala/code/ActorDocSpec.scala @@ -8,7 +8,7 @@ import akka.util.duration._ //#imports import akka.actor.Actor import akka.event.Logging -import akka.config.Configuration +import com.typesafe.config.Config //#imports @@ -22,7 +22,7 @@ class MyActor extends Actor { } //#my-actor -class ActorDocSpec extends AkkaSpec(Configuration("akka.loglevel" -> "INFO")) { +class ActorDocSpec extends AkkaSpec(Map("akka.loglevel" -> "INFO")) { "creating actor with AkkaSpec.actorOf" in { //#creating-actorOf 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/akka-beanstalk-mailbox-reference.conf new file mode 100644 index 0000000000..cf7f2cb8a9 --- /dev/null +++ b/akka-durable-mailboxes/akka-beanstalk-mailbox/src/main/resources/akka-beanstalk-mailbox-reference.conf @@ -0,0 +1,20 @@ +################################################## +# Akka Beanstalk Mailboxes Reference Config File # +################################################## + +# This the reference config file has all the default settings. +# Make your edits/overrides in your akka.conf. + +akka { + mailbox { + beanstalk { + hostname = "0.0.0.0" + port = 11300 + reconnect-window = 5 + message-submit-delay = 0 + message-submit-timeout = 5 + message-time-to-live = 120 + } + } + +} diff --git a/akka-durable-mailboxes/akka-beanstalk-mailbox/src/main/scala/akka/actor/mailbox/BeanstalkBasedMailbox.scala b/akka-durable-mailboxes/akka-beanstalk-mailbox/src/main/scala/akka/actor/mailbox/BeanstalkBasedMailbox.scala index fae743b46c..7eefc0107f 100644 --- a/akka-durable-mailboxes/akka-beanstalk-mailbox/src/main/scala/akka/actor/mailbox/BeanstalkBasedMailbox.scala +++ b/akka-durable-mailboxes/akka-beanstalk-mailbox/src/main/scala/akka/actor/mailbox/BeanstalkBasedMailbox.scala @@ -20,13 +20,13 @@ class BeanstalkBasedMailboxException(message: String) extends AkkaException(mess */ class BeanstalkBasedMailbox(val owner: ActorCell) extends DurableMailbox(owner) with DurableMessageSerialization { - val hostname = system.settings.config.getString("akka.actor.mailbox.beanstalk.hostname", "0.0.0.0") - val port = system.settings.config.getInt("akka.actor.mailbox.beanstalk.port", 11300) + val hostname = system.settings.config.getString("akka.actor.mailbox.beanstalk.hostname") + val port = system.settings.config.getInt("akka.actor.mailbox.beanstalk.port") def defaultTimeUnit = system.settings.DefaultTimeUnit - val reconnectWindow = Duration(system.settings.config.getInt("akka.actor.mailbox.beanstalk.reconnect-window", 5), defaultTimeUnit).toSeconds.toInt - val messageSubmitDelay = Duration(system.settings.config.getInt("akka.actor.mailbox.beanstalk.message-submit-delay", 0), defaultTimeUnit).toSeconds.toInt - val messageSubmitTimeout = Duration(system.settings.config.getInt("akka.actor.mailbox.beanstalk.message-submit-timeout", 5), defaultTimeUnit).toSeconds.toInt - val messageTimeToLive = Duration(system.settings.config.getInt("akka.actor.mailbox.beanstalk.message-time-to-live", 120), defaultTimeUnit).toSeconds.toInt + val reconnectWindow = Duration(system.settings.config.getInt("akka.actor.mailbox.beanstalk.reconnect-window"), defaultTimeUnit).toSeconds.toInt + val messageSubmitDelay = Duration(system.settings.config.getInt("akka.actor.mailbox.beanstalk.message-submit-delay"), defaultTimeUnit).toSeconds.toInt + val messageSubmitTimeout = Duration(system.settings.config.getInt("akka.actor.mailbox.beanstalk.message-submit-timeout"), defaultTimeUnit).toSeconds.toInt + val messageTimeToLive = Duration(system.settings.config.getInt("akka.actor.mailbox.beanstalk.message-time-to-live"), defaultTimeUnit).toSeconds.toInt val log = Logging(system, this) 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/akka-file-mailbox-reference.conf new file mode 100644 index 0000000000..2b84205fa5 --- /dev/null +++ b/akka-durable-mailboxes/akka-file-mailbox/src/main/resources/akka-file-mailbox-reference.conf @@ -0,0 +1,26 @@ +############################################# +# Akka File Mailboxes Reference Config File # +############################################# + +# This the reference config file has all the default settings. +# Make your edits/overrides in your akka.conf. + +akka { + mailbox { + file-based { + directory-path = "./_mb" + max-items = 2147483647 + max-size = 2147483647 + max-items = 2147483647 + max-item-size = 2147483647 + max-age = 0 + max-journal-size = 16777216 # 16 * 1024 * 1024 + max-memory-size = 134217728 # 128 * 1024 * 1024 + max-journal-overflow = 10 + max-journal-size-absolute = 9223372036854775807 + discard-old-when-full = true + keep-journal = true + sync-journal = false + } + } +} 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 2ea90fb7d7..ff12fe4f94 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 @@ -6,14 +6,14 @@ package akka.actor.mailbox import org.apache.commons.io.FileUtils import akka.actor.ActorCell -import akka.config.Configuration import akka.dispatch.Envelope import akka.event.Logging import akka.actor.ActorRef +import com.typesafe.config.Config object FileBasedMailbox { - def queuePath(config: Configuration): String = { - config.getString("akka.actor.mailbox.file-based.directory-path", "./_mb") // /var/spool/akka + def queuePath(config: Config): String = { + config.getString("akka.actor.mailbox.file-based.directory-path") // /var/spool/akka } } diff --git a/akka-durable-mailboxes/akka-file-mailbox/src/main/scala/akka/actor/mailbox/filequeue/PersistentQueue.scala b/akka-durable-mailboxes/akka-file-mailbox/src/main/scala/akka/actor/mailbox/filequeue/PersistentQueue.scala index 63e52a939c..638e67f17e 100644 --- a/akka-durable-mailboxes/akka-file-mailbox/src/main/scala/akka/actor/mailbox/filequeue/PersistentQueue.scala +++ b/akka-durable-mailboxes/akka-file-mailbox/src/main/scala/akka/actor/mailbox/filequeue/PersistentQueue.scala @@ -19,8 +19,8 @@ package akka.actor.mailbox.filequeue import java.io._ import scala.collection.mutable -import akka.config.Configuration import akka.event.LoggingAdapter +import com.typesafe.config.Config // a config value that's backed by a global setting but may be locally overridden class OverlaySetting[T](base: ⇒ T) { @@ -32,7 +32,7 @@ class OverlaySetting[T](base: ⇒ T) { def apply() = local.getOrElse(base) } -class PersistentQueue(persistencePath: String, val name: String, val config: Configuration, log: LoggingAdapter) { +class PersistentQueue(persistencePath: String, val name: String, val config: Config, log: LoggingAdapter) { private case object ItemArrived @@ -127,18 +127,18 @@ class PersistentQueue(persistencePath: String, val name: String, val config: Con //config.subscribe { c => configure(c.getOrElse(new Config)) } configure(config) - def configure(config: Configuration) = synchronized { - maxItems set config.getInt("akka.actor.mailbox.file-based.max-items") - maxSize set config.getLong("akka.actor.mailbox.file-based.max-size") - maxItemSize set config.getLong("akka.actor.mailbox.file-based.max-item-size") - maxAge set config.getInt("akka.actor.mailbox.file-based.max-age") - maxJournalSize set config.getLong("akka.actor.mailbox.file-based.max-journal-size") - maxMemorySize set config.getLong("akka.actor.mailbox.file-based.max-memory-size") - maxJournalOverflow set config.getInt("akka.actor.mailbox.file-based.max-journal-overflow") - maxJournalSizeAbsolute set config.getLong("akka.actor.mailbox.file-based.max-journal-size-absolute") - discardOldWhenFull set config.getBool("akka.actor.mailbox.file-based.discard-old-when-full") - keepJournal set config.getBool("akka.actor.mailbox.file-based.journal") - syncJournal set config.getBool("akka.actor.mailbox.file-based.sync-journal") + def configure(config: Config) = synchronized { + maxItems set Some(config.getInt("akka.actor.mailbox.file-based.max-items")) + maxSize set Some(config.getLong("akka.actor.mailbox.file-based.max-size")) + maxItemSize set Some(config.getLong("akka.actor.mailbox.file-based.max-item-size")) + maxAge set Some(config.getInt("akka.actor.mailbox.file-based.max-age")) + maxJournalSize set Some(config.getLong("akka.actor.mailbox.file-based.max-journal-size")) + maxMemorySize set Some(config.getLong("akka.actor.mailbox.file-based.max-memory-size")) + maxJournalOverflow set Some(config.getInt("akka.actor.mailbox.file-based.max-journal-overflow")) + maxJournalSizeAbsolute set Some(config.getLong("akka.actor.mailbox.file-based.max-journal-size-absolute")) + discardOldWhenFull set Some(config.getBoolean("akka.actor.mailbox.file-based.discard-old-when-full")) + keepJournal set Some(config.getBoolean("akka.actor.mailbox.file-based.keep-journal")) + syncJournal set Some(config.getBoolean("akka.actor.mailbox.file-based.sync-journal")) log.info("Configuring queue %s: journal=%s, max-items=%s, max-size=%s, max-age=%s, max-journal-size=%s, max-memory-size=%s, max-journal-overflow=%s, max-journal-size-absolute=%s, discard-old-when-full=%s, sync-journal=%s" .format( name, keepJournal(), maxItems(), maxSize(), maxAge(), maxJournalSize(), maxMemorySize(), diff --git a/akka-durable-mailboxes/akka-file-mailbox/src/main/scala/akka/actor/mailbox/filequeue/QueueCollection.scala b/akka-durable-mailboxes/akka-file-mailbox/src/main/scala/akka/actor/mailbox/filequeue/QueueCollection.scala index 69b3ba7605..91f07b2ea2 100644 --- a/akka-durable-mailboxes/akka-file-mailbox/src/main/scala/akka/actor/mailbox/filequeue/QueueCollection.scala +++ b/akka-durable-mailboxes/akka-file-mailbox/src/main/scala/akka/actor/mailbox/filequeue/QueueCollection.scala @@ -20,12 +20,12 @@ package akka.actor.mailbox.filequeue import java.io.File import java.util.concurrent.CountDownLatch import scala.collection.mutable -import akka.config.Configuration import akka.event.LoggingAdapter +import com.typesafe.config.Config class InaccessibleQueuePath extends Exception("Inaccessible queue path: Must be a directory and writable") -class QueueCollection(queueFolder: String, private var queueConfigs: Configuration, log: LoggingAdapter) { +class QueueCollection(queueFolder: String, private var queueConfigs: Config, log: LoggingAdapter) { private val path = new File(queueFolder) if (!path.isDirectory) { diff --git a/akka-durable-mailboxes/akka-mailboxes-common/src/main/scala/akka/actor/mailbox/DurableMailbox.scala b/akka-durable-mailboxes/akka-mailboxes-common/src/main/scala/akka/actor/mailbox/DurableMailbox.scala index 92a4ce1d4e..b5d832d443 100644 --- a/akka-durable-mailboxes/akka-mailboxes-common/src/main/scala/akka/actor/mailbox/DurableMailbox.scala +++ b/akka-durable-mailboxes/akka-mailboxes-common/src/main/scala/akka/actor/mailbox/DurableMailbox.scala @@ -5,12 +5,10 @@ package akka.actor.mailbox import akka.util.ReflectiveAccess import java.lang.reflect.InvocationTargetException - import akka.AkkaException import akka.actor.ActorCell import akka.actor.ActorRef import akka.actor.SerializedActorRef -import akka.config.Configuration import akka.dispatch.Envelope import akka.dispatch.DefaultSystemMessageQueue import akka.dispatch.Dispatcher @@ -25,6 +23,7 @@ import akka.remote.RemoteProtocol.RemoteMessageProtocol import akka.remote.RemoteActorRefProvider import akka.remote.netty.NettyRemoteServer import akka.serialization.Serialization +import com.typesafe.config.Config private[akka] object DurableExecutableMailboxConfig { val Name = "[\\.\\/\\$\\s]".r @@ -130,8 +129,9 @@ case class FqnDurableMailboxType(mailboxFQN: String) extends DurableMailboxType( class DurableMailboxConfigurator { // TODO PN #896: when and how is this class supposed to be used? Can we remove it? - def mailboxType(config: Configuration): MailboxType = { - val storage = config.getString("storage") map { + def mailboxType(config: Config): MailboxType = { + if (!config.hasPath("storage")) throw new DurableMailboxException("No 'storage' defined for durable mailbox") + config.getString("storage") match { case "redis" ⇒ RedisDurableMailboxType case "mongodb" ⇒ MongoDurableMailboxType case "beanstalk" ⇒ BeanstalkDurableMailboxType @@ -139,7 +139,5 @@ class DurableMailboxConfigurator { case "file" ⇒ FileDurableMailboxType case fqn ⇒ FqnDurableMailboxType(fqn) } - - storage.getOrElse(throw new DurableMailboxException("No 'storage' defined for durable mailbox")) } } 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/akka-mongo-mailbox-reference.conf new file mode 100644 index 0000000000..8b5730212b --- /dev/null +++ b/akka-durable-mailboxes/akka-mongo-mailbox/src/main/resources/akka-mongo-mailbox-reference.conf @@ -0,0 +1,21 @@ +################################################ +# Akka MongoDB Mailboxes Reference Config File # +################################################ + +# This the reference config file has all the default settings. +# Make your edits/overrides in your akka.conf. + +akka { + mailbox { + mongodb { + # Any specified collection name will be used as a prefix for collections that use durable mongo mailboxes + uri = "mongodb://localhost/akka.mailbox" # Follow Mongo URI Spec - http://www.mongodb.org/display/DOCS/Connections + + # Configurable timeouts for certain ops + timeout { + read = 3000 # number of milliseconds to wait for a read to succeed before timing out the future + write = 3000 # number of milliseconds to wait for a write to succeed before timing out the future + } + } + } +} diff --git a/akka-durable-mailboxes/akka-mongo-mailbox/src/main/scala/akka/actor/mailbox/MongoBasedMailbox.scala b/akka-durable-mailboxes/akka-mongo-mailbox/src/main/scala/akka/actor/mailbox/MongoBasedMailbox.scala index 9a262fd3b3..e26f892567 100644 --- a/akka-durable-mailboxes/akka-mongo-mailbox/src/main/scala/akka/actor/mailbox/MongoBasedMailbox.scala +++ b/akka-durable-mailboxes/akka-mongo-mailbox/src/main/scala/akka/actor/mailbox/MongoBasedMailbox.scala @@ -31,12 +31,13 @@ class MongoBasedMailbox(val owner: ActorCell) extends DurableMailbox(owner) { implicit val mailboxBSONSer = new BSONSerializableMailbox(system) implicit val safeWrite = WriteConcern.Safe // TODO - Replica Safe when appropriate! + def config = system.settings.config val URI_CONFIG_KEY = "akka.actor.mailbox.mongodb.uri" val WRITE_TIMEOUT_KEY = "akka.actor.mailbox.mongodb.timeout.write" val READ_TIMEOUT_KEY = "akka.actor.mailbox.mongodb.timeout.read" - val mongoURI = system.settings.config.getString(URI_CONFIG_KEY) - val writeTimeout = system.settings.config.getInt(WRITE_TIMEOUT_KEY, 3000) - val readTimeout = system.settings.config.getInt(READ_TIMEOUT_KEY, 3000) + val mongoURI = if (config.hasPath(URI_CONFIG_KEY)) Some(config.getString(URI_CONFIG_KEY)) else None + val writeTimeout = config.getInt(WRITE_TIMEOUT_KEY) + val readTimeout = config.getInt(READ_TIMEOUT_KEY) val log = Logging(system, this) 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/akka-redis-mailbox-reference.conf new file mode 100644 index 0000000000..8028176981 --- /dev/null +++ b/akka-durable-mailboxes/akka-redis-mailbox/src/main/resources/akka-redis-mailbox-reference.conf @@ -0,0 +1,15 @@ +############################################## +# Akka Redis Mailboxes Reference Config File # +############################################## + +# This the reference config file has all the default settings. +# Make your edits/overrides in your akka.conf. + +akka { + mailbox { + redis { + hostname = "127.0.0.1" + port = 6379 + } + } +} diff --git a/akka-durable-mailboxes/akka-redis-mailbox/src/main/scala/akka/actor/mailbox/RedisBasedMailbox.scala b/akka-durable-mailboxes/akka-redis-mailbox/src/main/scala/akka/actor/mailbox/RedisBasedMailbox.scala index ca765bfd62..49ca117e86 100644 --- a/akka-durable-mailboxes/akka-redis-mailbox/src/main/scala/akka/actor/mailbox/RedisBasedMailbox.scala +++ b/akka-durable-mailboxes/akka-redis-mailbox/src/main/scala/akka/actor/mailbox/RedisBasedMailbox.scala @@ -58,8 +58,8 @@ class RedisBasedMailbox(val owner: ActorCell) extends DurableMailbox(owner) with private[akka] def connect() = { new RedisClientPool( - system.settings.config.getString("akka.actor.mailbox.redis.hostname", "127.0.0.1"), - system.settings.config.getInt("akka.actor.mailbox.redis.port", 6379)) + system.settings.config.getString("akka.actor.mailbox.redis.hostname"), + system.settings.config.getInt("akka.actor.mailbox.redis.port")) } private def withErrorHandling[T](body: ⇒ T): T = { 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/akka-zookeeper-mailbox-reference.conf new file mode 100644 index 0000000000..00dec1e3ca --- /dev/null +++ b/akka-durable-mailboxes/akka-zookeeper-mailbox/src/main/resources/akka-zookeeper-mailbox-reference.conf @@ -0,0 +1,17 @@ +################################################## +# Akka ZooKepper Mailboxes Reference Config File # +################################################## + +# This the reference config file has all the default settings. +# Make your edits/overrides in your akka.conf. + +akka { + mailbox { + zookeeper { + server-addresses = "localhost:2181" + session-timeout = 60 + connection-timeout = 60 + blocking-queue = true + } + } +} 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 6fff77d7cc..622a0de41e 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 @@ -21,11 +21,11 @@ class ZooKeeperBasedMailboxException(message: String) extends AkkaException(mess */ class ZooKeeperBasedMailbox(val owner: ActorCell) extends DurableMailbox(owner) with DurableMessageSerialization { - val zkServerAddresses = system.settings.config.getString("akka.actor.mailbox.zookeeper.server-addresses", "localhost:2181") + val zkServerAddresses = system.settings.config.getString("akka.actor.mailbox.zookeeper.server-addresses") def defaultTimeUnit = system.settings.DefaultTimeUnit - val sessionTimeout = Duration(system.settings.config.getInt("akka.actor.mailbox.zookeeper.session-timeout", 60), defaultTimeUnit).toMillis.toInt - val connectionTimeout = Duration(system.settings.config.getInt("akka.actor.mailbox.zookeeper.connection-timeout", 60), defaultTimeUnit).toMillis.toInt - val blockingQueue = system.settings.config.getBool("akka.actor.mailbox.zookeeper.blocking-queue", true) + val sessionTimeout = Duration(system.settings.config.getInt("akka.actor.mailbox.zookeeper.session-timeout"), defaultTimeUnit).toMillis.toInt + val connectionTimeout = Duration(system.settings.config.getInt("akka.actor.mailbox.zookeeper.connection-timeout"), defaultTimeUnit).toMillis.toInt + val blockingQueue = system.settings.config.getBoolean("akka.actor.mailbox.zookeeper.blocking-queue") val queueNode = "/queues" val queuePathTemplate = queueNode + "/%s" diff --git a/akka-durable-mailboxes/akka-mailboxes-common/src/test/resources/zoo.cfg b/akka-durable-mailboxes/akka-zookeeper-mailbox/src/test/resources/zoo.cfg similarity index 100% rename from akka-durable-mailboxes/akka-mailboxes-common/src/test/resources/zoo.cfg rename to akka-durable-mailboxes/akka-zookeeper-mailbox/src/test/resources/zoo.cfg diff --git a/akka-remote/src/main/resources/akka-remote-reference.conf b/akka-remote/src/main/resources/akka-remote-reference.conf new file mode 100644 index 0000000000..d845f786a0 --- /dev/null +++ b/akka-remote/src/main/resources/akka-remote-reference.conf @@ -0,0 +1,55 @@ +##################################### +# Akka Remote Reference Config File # +##################################### + +# This the reference config file has all the default settings. +# Make your edits/overrides in your akka.conf. + +akka { + + remote { + # FIXME rename to transport + layer = "akka.cluster.netty.NettyRemoteSupport" + + use-compression = false + + secure-cookie = "" # Generate your own with '$AKKA_HOME/scripts/generate_config_with_secure_cookie.sh' + # or using 'akka.util.Crypt.generateSecureCookie' + + remote-daemon-ack-timeout = 30 # Timeout for ACK of cluster operations, lik checking actor out etc. + + use-passive-connections = true # Reuse inbound connections for outbound messages + + failure-detector { # accrual failure detection config + threshold = 8 # defines the failure detector threshold + # A low threshold is prone to generate many wrong suspicions but ensures a + # quick detection in the event of a real crash. Conversely, a high threshold + # generates fewer mistakes but needs more time to detect actual crashes + max-sample-size = 1000 + } + + server { + 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 + connection-timeout = 120 # Length in time-unit + require-cookie = false # Should the remote server require that it peers share the same secure-cookie (defined in the 'remote' section)? + untrusted-mode = false # Enable untrusted mode for full security of server managed actors, allows untrusted clients to connect. + backlog = 4096 # Sets the size of the connection backlog + } + + client { + buffering { + retry-message-send-on-failure = false # Should message buffering on remote client error be used (buffer flushed on successful reconnect) + capacity = -1 # If negative (or zero) then an unbounded mailbox is used (default) + # If positive then a bounded mailbox is used and the capacity is set using the property + } + reconnect-delay = 5 + read-timeout = 3600 + message-frame-size = 1048576 + reap-futures-delay = 5 + reconnection-time-window = 600 # Maximum time window that a client should try to reconnect for + } + } + + +} diff --git a/akka-remote/src/main/scala/akka/remote/AccrualFailureDetector.scala b/akka-remote/src/main/scala/akka/remote/AccrualFailureDetector.scala index 987146a39b..4ca3c433da 100644 --- a/akka-remote/src/main/scala/akka/remote/AccrualFailureDetector.scala +++ b/akka-remote/src/main/scala/akka/remote/AccrualFailureDetector.scala @@ -27,8 +27,8 @@ class AccrualFailureDetector(val threshold: Int = 8, val maxSampleSize: Int = 10 def this(system: ActorSystem) { this( - system.settings.config.getInt("akka.remote.failure-detector.theshold", 8), - system.settings.config.getInt("akka.remote.failure-detector.max-sample-size", 1000)) + system.settings.config.getInt("akka.remote.failure-detector.threshold"), + system.settings.config.getInt("akka.remote.failure-detector.max-sample-size")) } private final val PhiFactor = 1.0 / math.log(10.0) diff --git a/akka-remote/src/main/scala/akka/remote/Remote.scala b/akka-remote/src/main/scala/akka/remote/Remote.scala index a140f1f252..7a102cb10e 100644 --- a/akka-remote/src/main/scala/akka/remote/Remote.scala +++ b/akka-remote/src/main/scala/akka/remote/Remote.scala @@ -35,8 +35,8 @@ class Remote(val system: ActorSystemImpl, val nodename: String) { import AC._ // TODO move to settings? - val shouldCompressData = config.getBool("akka.remote.use-compression", false) - val remoteSystemDaemonAckTimeout = Duration(config.getInt("akka.remote.remote-daemon-ack-timeout", 30), DefaultTimeUnit).toMillis.toInt + val shouldCompressData = config.getBoolean("akka.remote.use-compression") + val remoteSystemDaemonAckTimeout = Duration(config.getInt("akka.remote.remote-daemon-ack-timeout"), DefaultTimeUnit).toMillis.toInt val failureDetector = new AccrualFailureDetector(system) diff --git a/akka-remote/src/main/scala/akka/remote/RemoteConfig.scala b/akka-remote/src/main/scala/akka/remote/RemoteConfig.scala index 4f1d161dc1..53c32e5179 100644 --- a/akka-remote/src/main/scala/akka/remote/RemoteConfig.scala +++ b/akka-remote/src/main/scala/akka/remote/RemoteConfig.scala @@ -5,37 +5,42 @@ package akka.remote import akka.util.Duration -import akka.config.{ Configuration, ConfigurationException } +import akka.config.ConfigurationException import java.util.concurrent.TimeUnit +import com.typesafe.config.Config -class RemoteClientSettings(config: Configuration, defaultTimeUnit: TimeUnit) { - val SECURE_COOKIE: Option[String] = config.getString("akka.remote.secure-cookie", "") match { +class RemoteClientSettings(config: Config, defaultTimeUnit: TimeUnit) { + val SECURE_COOKIE: Option[String] = config.getString("akka.remote.secure-cookie") match { case "" ⇒ None case cookie ⇒ Some(cookie) } - val RECONNECTION_TIME_WINDOW = Duration(config.getInt("akka.remote.client.reconnection-time-window", 600), defaultTimeUnit).toMillis - val READ_TIMEOUT = Duration(config.getInt("akka.remote.client.read-timeout", 3600), defaultTimeUnit) - val RECONNECT_DELAY = Duration(config.getInt("akka.remote.client.reconnect-delay", 5), defaultTimeUnit) - val MESSAGE_FRAME_SIZE = config.getInt("akka.remote.client.message-frame-size", 1048576) + val RECONNECTION_TIME_WINDOW = Duration(config.getInt("akka.remote.client.reconnection-time-window"), defaultTimeUnit).toMillis + val READ_TIMEOUT = Duration(config.getInt("akka.remote.client.read-timeout"), defaultTimeUnit) + val RECONNECT_DELAY = Duration(config.getInt("akka.remote.client.reconnect-delay"), defaultTimeUnit) + val MESSAGE_FRAME_SIZE = config.getInt("akka.remote.client.message-frame-size") } -class RemoteServerSettings(config: Configuration, defaultTimeUnit: TimeUnit) { - val isRemotingEnabled = config.getList("akka.enabled-modules").exists(_ == "cluster") //TODO FIXME Shouldn't this be "remote"? - val MESSAGE_FRAME_SIZE = config.getInt("akka.remote.server.message-frame-size", 1048576) - val SECURE_COOKIE = config.getString("akka.remote.secure-cookie") +class RemoteServerSettings(config: Config, defaultTimeUnit: TimeUnit) { + import scala.collection.JavaConverters._ + val isRemotingEnabled = config.getStringList("akka.enabled-modules").asScala.exists(_ == "cluster") //TODO FIXME Shouldn't this be "remote"? + val MESSAGE_FRAME_SIZE = config.getInt("akka.remote.server.message-frame-size") + val SECURE_COOKIE: Option[String] = config.getString("akka.remote.secure-cookie") match { + case "" ⇒ None + case cookie ⇒ Some(cookie) + } val REQUIRE_COOKIE = { - val requireCookie = config.getBool("akka.remote.server.require-cookie", false) + val requireCookie = config.getBoolean("akka.remote.server.require-cookie") if (isRemotingEnabled && requireCookie && SECURE_COOKIE.isEmpty) throw new ConfigurationException( "Configuration option 'akka.remote.server.require-cookie' is turned on but no secure cookie is defined in 'akka.remote.secure-cookie'.") requireCookie } - val USE_PASSIVE_CONNECTIONS = config.getBool("akka.remote.use-passive-connections", false) + val USE_PASSIVE_CONNECTIONS = config.getBoolean("akka.remote.use-passive-connections") - val UNTRUSTED_MODE = config.getBool("akka.remote.server.untrusted-mode", false) - val PORT = config.getInt("akka.remote.server.port", 2552) - val CONNECTION_TIMEOUT = Duration(config.getInt("akka.remote.server.connection-timeout", 100), defaultTimeUnit) + val UNTRUSTED_MODE = config.getBoolean("akka.remote.server.untrusted-mode") + val PORT = config.getInt("akka.remote.server.port") + val CONNECTION_TIMEOUT = Duration(config.getInt("akka.remote.server.connection-timeout"), defaultTimeUnit) - val BACKLOG = config.getInt("akka.remote.server.backlog", 4096) + val BACKLOG = config.getInt("akka.remote.server.backlog") } diff --git a/akka-remote/src/multi-jvm/scala/akka/remote/direct_routed/DirectRoutedRemoteActorMultiJvmNode1.conf b/akka-remote/src/multi-jvm/scala/akka/remote/direct_routed/DirectRoutedRemoteActorMultiJvmNode1.conf index 732cd00d48..1b1c7b398c 100644 --- a/akka-remote/src/multi-jvm/scala/akka/remote/direct_routed/DirectRoutedRemoteActorMultiJvmNode1.conf +++ b/akka-remote/src/multi-jvm/scala/akka/remote/direct_routed/DirectRoutedRemoteActorMultiJvmNode1.conf @@ -1,5 +1,11 @@ -akka.actor.provider = "akka.remote.RemoteActorRefProvider" -akka.loglevel = "WARNING" -akka.actor.deployment./app/service-hello.router = "direct" -akka.actor.deployment./app/service-hello.nr-of-instances = 1 -akka.actor.deployment./app/service-hello.remote.nodes = ["localhost:9991"] +akka { + loglevel = "WARNING" + actor { + provider = "akka.remote.RemoteActorRefProvider" + deployment { + /app/service-hello.router = "direct" + /app/service-hello.nr-of-instances = 1 + /app/service-hello.remote.nodes = ["localhost:9991"] + } + } +} diff --git a/akka-remote/src/multi-jvm/scala/akka/remote/direct_routed/DirectRoutedRemoteActorMultiJvmNode2.conf b/akka-remote/src/multi-jvm/scala/akka/remote/direct_routed/DirectRoutedRemoteActorMultiJvmNode2.conf index 732cd00d48..1b1c7b398c 100644 --- a/akka-remote/src/multi-jvm/scala/akka/remote/direct_routed/DirectRoutedRemoteActorMultiJvmNode2.conf +++ b/akka-remote/src/multi-jvm/scala/akka/remote/direct_routed/DirectRoutedRemoteActorMultiJvmNode2.conf @@ -1,5 +1,11 @@ -akka.actor.provider = "akka.remote.RemoteActorRefProvider" -akka.loglevel = "WARNING" -akka.actor.deployment./app/service-hello.router = "direct" -akka.actor.deployment./app/service-hello.nr-of-instances = 1 -akka.actor.deployment./app/service-hello.remote.nodes = ["localhost:9991"] +akka { + loglevel = "WARNING" + actor { + provider = "akka.remote.RemoteActorRefProvider" + deployment { + /app/service-hello.router = "direct" + /app/service-hello.nr-of-instances = 1 + /app/service-hello.remote.nodes = ["localhost:9991"] + } + } +} diff --git a/akka-remote/src/multi-jvm/scala/akka/remote/new_remote_actor/NewRemoteActorMultiJvmNode1.conf b/akka-remote/src/multi-jvm/scala/akka/remote/new_remote_actor/NewRemoteActorMultiJvmNode1.conf index 727d892e66..9073ed4ed3 100644 --- a/akka-remote/src/multi-jvm/scala/akka/remote/new_remote_actor/NewRemoteActorMultiJvmNode1.conf +++ b/akka-remote/src/multi-jvm/scala/akka/remote/new_remote_actor/NewRemoteActorMultiJvmNode1.conf @@ -1,3 +1,9 @@ -akka.actor.provider = "akka.remote.RemoteActorRefProvider" -akka.loglevel = "WARNING" -akka.actor.deployment./app/service-hello.remote.nodes = ["localhost:9991"] +akka { + loglevel = "WARNING" + actor { + provider = "akka.remote.RemoteActorRefProvider" + deployment { + /app/service-hello.remote.nodes = ["localhost:9991"] + } + } +} diff --git a/akka-remote/src/multi-jvm/scala/akka/remote/new_remote_actor/NewRemoteActorMultiJvmNode2.conf b/akka-remote/src/multi-jvm/scala/akka/remote/new_remote_actor/NewRemoteActorMultiJvmNode2.conf index 727d892e66..9073ed4ed3 100644 --- a/akka-remote/src/multi-jvm/scala/akka/remote/new_remote_actor/NewRemoteActorMultiJvmNode2.conf +++ b/akka-remote/src/multi-jvm/scala/akka/remote/new_remote_actor/NewRemoteActorMultiJvmNode2.conf @@ -1,3 +1,9 @@ -akka.actor.provider = "akka.remote.RemoteActorRefProvider" -akka.loglevel = "WARNING" -akka.actor.deployment./app/service-hello.remote.nodes = ["localhost:9991"] +akka { + loglevel = "WARNING" + actor { + provider = "akka.remote.RemoteActorRefProvider" + deployment { + /app/service-hello.remote.nodes = ["localhost:9991"] + } + } +} diff --git a/akka-remote/src/multi-jvm/scala/akka/remote/random_routed/RandomRoutedRemoteActorMultiJvmNode1.conf b/akka-remote/src/multi-jvm/scala/akka/remote/random_routed/RandomRoutedRemoteActorMultiJvmNode1.conf index 7b9fdcd84b..e373bc9c0e 100644 --- a/akka-remote/src/multi-jvm/scala/akka/remote/random_routed/RandomRoutedRemoteActorMultiJvmNode1.conf +++ b/akka-remote/src/multi-jvm/scala/akka/remote/random_routed/RandomRoutedRemoteActorMultiJvmNode1.conf @@ -1,5 +1,11 @@ -akka.actor.provider = "akka.remote.RemoteActorRefProvider" -akka.loglevel = "WARNING" -akka.actor.deployment./app/service-hello.router = "random" -akka.actor.deployment./app/service-hello.nr-of-instances = 3 -akka.actor.deployment./app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] +akka { + loglevel = "WARNING" + actor { + provider = "akka.remote.RemoteActorRefProvider" + deployment { + /app/service-hello.router = "random" + /app/service-hello.nr-of-instances = 3 + /app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] + } + } +} diff --git a/akka-remote/src/multi-jvm/scala/akka/remote/random_routed/RandomRoutedRemoteActorMultiJvmNode2.conf b/akka-remote/src/multi-jvm/scala/akka/remote/random_routed/RandomRoutedRemoteActorMultiJvmNode2.conf index 7b9fdcd84b..b6d6e7b3f9 100644 --- a/akka-remote/src/multi-jvm/scala/akka/remote/random_routed/RandomRoutedRemoteActorMultiJvmNode2.conf +++ b/akka-remote/src/multi-jvm/scala/akka/remote/random_routed/RandomRoutedRemoteActorMultiJvmNode2.conf @@ -1,5 +1,9 @@ -akka.actor.provider = "akka.remote.RemoteActorRefProvider" -akka.loglevel = "WARNING" -akka.actor.deployment./app/service-hello.router = "random" -akka.actor.deployment./app/service-hello.nr-of-instances = 3 -akka.actor.deployment./app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] +akka { + loglevel = "WARNING" + actor { + provider = "akka.remote.RemoteActorRefProvider" + deployment./app/service-hello.router = "random" + deployment./app/service-hello.nr-of-instances = 3 + deployment./app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] + } +} diff --git a/akka-remote/src/multi-jvm/scala/akka/remote/random_routed/RandomRoutedRemoteActorMultiJvmNode3.conf b/akka-remote/src/multi-jvm/scala/akka/remote/random_routed/RandomRoutedRemoteActorMultiJvmNode3.conf index 7b9fdcd84b..e373bc9c0e 100644 --- a/akka-remote/src/multi-jvm/scala/akka/remote/random_routed/RandomRoutedRemoteActorMultiJvmNode3.conf +++ b/akka-remote/src/multi-jvm/scala/akka/remote/random_routed/RandomRoutedRemoteActorMultiJvmNode3.conf @@ -1,5 +1,11 @@ -akka.actor.provider = "akka.remote.RemoteActorRefProvider" -akka.loglevel = "WARNING" -akka.actor.deployment./app/service-hello.router = "random" -akka.actor.deployment./app/service-hello.nr-of-instances = 3 -akka.actor.deployment./app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] +akka { + loglevel = "WARNING" + actor { + provider = "akka.remote.RemoteActorRefProvider" + deployment { + /app/service-hello.router = "random" + /app/service-hello.nr-of-instances = 3 + /app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] + } + } +} diff --git a/akka-remote/src/multi-jvm/scala/akka/remote/random_routed/RandomRoutedRemoteActorMultiJvmNode4.conf b/akka-remote/src/multi-jvm/scala/akka/remote/random_routed/RandomRoutedRemoteActorMultiJvmNode4.conf index 7b9fdcd84b..e373bc9c0e 100644 --- a/akka-remote/src/multi-jvm/scala/akka/remote/random_routed/RandomRoutedRemoteActorMultiJvmNode4.conf +++ b/akka-remote/src/multi-jvm/scala/akka/remote/random_routed/RandomRoutedRemoteActorMultiJvmNode4.conf @@ -1,5 +1,11 @@ -akka.actor.provider = "akka.remote.RemoteActorRefProvider" -akka.loglevel = "WARNING" -akka.actor.deployment./app/service-hello.router = "random" -akka.actor.deployment./app/service-hello.nr-of-instances = 3 -akka.actor.deployment./app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] +akka { + loglevel = "WARNING" + actor { + provider = "akka.remote.RemoteActorRefProvider" + deployment { + /app/service-hello.router = "random" + /app/service-hello.nr-of-instances = 3 + /app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] + } + } +} diff --git a/akka-remote/src/multi-jvm/scala/akka/remote/round_robin_routed/RoundRobinRoutedRemoteActorMultiJvmNode1.conf b/akka-remote/src/multi-jvm/scala/akka/remote/round_robin_routed/RoundRobinRoutedRemoteActorMultiJvmNode1.conf index d660a824de..a0ec833383 100644 --- a/akka-remote/src/multi-jvm/scala/akka/remote/round_robin_routed/RoundRobinRoutedRemoteActorMultiJvmNode1.conf +++ b/akka-remote/src/multi-jvm/scala/akka/remote/round_robin_routed/RoundRobinRoutedRemoteActorMultiJvmNode1.conf @@ -1,5 +1,11 @@ -akka.actor.provider = "akka.remote.RemoteActorRefProvider" -akka.loglevel = "WARNING" -akka.actor.deployment./app/service-hello.router = "round-robin" -akka.actor.deployment./app/service-hello.nr-of-instances = 3 -akka.actor.deployment./app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] +akka { + loglevel = "WARNING" + actor { + provider = "akka.remote.RemoteActorRefProvider" + deployment { + /app/service-hello.router = "round-robin" + /app/service-hello.nr-of-instances = 3 + /app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] + } + } +} diff --git a/akka-remote/src/multi-jvm/scala/akka/remote/round_robin_routed/RoundRobinRoutedRemoteActorMultiJvmNode2.conf b/akka-remote/src/multi-jvm/scala/akka/remote/round_robin_routed/RoundRobinRoutedRemoteActorMultiJvmNode2.conf index d660a824de..a0ec833383 100644 --- a/akka-remote/src/multi-jvm/scala/akka/remote/round_robin_routed/RoundRobinRoutedRemoteActorMultiJvmNode2.conf +++ b/akka-remote/src/multi-jvm/scala/akka/remote/round_robin_routed/RoundRobinRoutedRemoteActorMultiJvmNode2.conf @@ -1,5 +1,11 @@ -akka.actor.provider = "akka.remote.RemoteActorRefProvider" -akka.loglevel = "WARNING" -akka.actor.deployment./app/service-hello.router = "round-robin" -akka.actor.deployment./app/service-hello.nr-of-instances = 3 -akka.actor.deployment./app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] +akka { + loglevel = "WARNING" + actor { + provider = "akka.remote.RemoteActorRefProvider" + deployment { + /app/service-hello.router = "round-robin" + /app/service-hello.nr-of-instances = 3 + /app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] + } + } +} diff --git a/akka-remote/src/multi-jvm/scala/akka/remote/round_robin_routed/RoundRobinRoutedRemoteActorMultiJvmNode3.conf b/akka-remote/src/multi-jvm/scala/akka/remote/round_robin_routed/RoundRobinRoutedRemoteActorMultiJvmNode3.conf index d660a824de..a0ec833383 100644 --- a/akka-remote/src/multi-jvm/scala/akka/remote/round_robin_routed/RoundRobinRoutedRemoteActorMultiJvmNode3.conf +++ b/akka-remote/src/multi-jvm/scala/akka/remote/round_robin_routed/RoundRobinRoutedRemoteActorMultiJvmNode3.conf @@ -1,5 +1,11 @@ -akka.actor.provider = "akka.remote.RemoteActorRefProvider" -akka.loglevel = "WARNING" -akka.actor.deployment./app/service-hello.router = "round-robin" -akka.actor.deployment./app/service-hello.nr-of-instances = 3 -akka.actor.deployment./app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] +akka { + loglevel = "WARNING" + actor { + provider = "akka.remote.RemoteActorRefProvider" + deployment { + /app/service-hello.router = "round-robin" + /app/service-hello.nr-of-instances = 3 + /app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] + } + } +} diff --git a/akka-remote/src/multi-jvm/scala/akka/remote/round_robin_routed/RoundRobinRoutedRemoteActorMultiJvmNode4.conf b/akka-remote/src/multi-jvm/scala/akka/remote/round_robin_routed/RoundRobinRoutedRemoteActorMultiJvmNode4.conf index d660a824de..a0ec833383 100644 --- a/akka-remote/src/multi-jvm/scala/akka/remote/round_robin_routed/RoundRobinRoutedRemoteActorMultiJvmNode4.conf +++ b/akka-remote/src/multi-jvm/scala/akka/remote/round_robin_routed/RoundRobinRoutedRemoteActorMultiJvmNode4.conf @@ -1,5 +1,11 @@ -akka.actor.provider = "akka.remote.RemoteActorRefProvider" -akka.loglevel = "WARNING" -akka.actor.deployment./app/service-hello.router = "round-robin" -akka.actor.deployment./app/service-hello.nr-of-instances = 3 -akka.actor.deployment./app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] +akka { + loglevel = "WARNING" + actor { + provider = "akka.remote.RemoteActorRefProvider" + deployment { + /app/service-hello.router = "round-robin" + /app/service-hello.nr-of-instances = 3 + /app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] + } + } +} diff --git a/akka-remote/src/multi-jvm/scala/akka/remote/scatter_gather_routed/ScatterGatherRoutedRemoteActorMultiJvmNode1.conf b/akka-remote/src/multi-jvm/scala/akka/remote/scatter_gather_routed/ScatterGatherRoutedRemoteActorMultiJvmNode1.conf index ae28dee91e..80ad72e3de 100644 --- a/akka-remote/src/multi-jvm/scala/akka/remote/scatter_gather_routed/ScatterGatherRoutedRemoteActorMultiJvmNode1.conf +++ b/akka-remote/src/multi-jvm/scala/akka/remote/scatter_gather_routed/ScatterGatherRoutedRemoteActorMultiJvmNode1.conf @@ -1,5 +1,11 @@ -akka.actor.provider = "akka.remote.RemoteActorRefProvider" -akka.loglevel = "WARNING" -akka.actor.deployment./app/service-hello.router = "scatter-gather" -akka.actor.deployment./app/service-hello.nr-of-instances = 3 -akka.actor.deployment./app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] +akka { + loglevel = "WARNING" + actor { + provider = "akka.remote.RemoteActorRefProvider" + deployment { + /app/service-hello.router = "scatter-gather" + /app/service-hello.nr-of-instances = 3 + /app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] + } + } +} diff --git a/akka-remote/src/multi-jvm/scala/akka/remote/scatter_gather_routed/ScatterGatherRoutedRemoteActorMultiJvmNode2.conf b/akka-remote/src/multi-jvm/scala/akka/remote/scatter_gather_routed/ScatterGatherRoutedRemoteActorMultiJvmNode2.conf index ae28dee91e..80ad72e3de 100644 --- a/akka-remote/src/multi-jvm/scala/akka/remote/scatter_gather_routed/ScatterGatherRoutedRemoteActorMultiJvmNode2.conf +++ b/akka-remote/src/multi-jvm/scala/akka/remote/scatter_gather_routed/ScatterGatherRoutedRemoteActorMultiJvmNode2.conf @@ -1,5 +1,11 @@ -akka.actor.provider = "akka.remote.RemoteActorRefProvider" -akka.loglevel = "WARNING" -akka.actor.deployment./app/service-hello.router = "scatter-gather" -akka.actor.deployment./app/service-hello.nr-of-instances = 3 -akka.actor.deployment./app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] +akka { + loglevel = "WARNING" + actor { + provider = "akka.remote.RemoteActorRefProvider" + deployment { + /app/service-hello.router = "scatter-gather" + /app/service-hello.nr-of-instances = 3 + /app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] + } + } +} diff --git a/akka-remote/src/multi-jvm/scala/akka/remote/scatter_gather_routed/ScatterGatherRoutedRemoteActorMultiJvmNode3.conf b/akka-remote/src/multi-jvm/scala/akka/remote/scatter_gather_routed/ScatterGatherRoutedRemoteActorMultiJvmNode3.conf index ae28dee91e..80ad72e3de 100644 --- a/akka-remote/src/multi-jvm/scala/akka/remote/scatter_gather_routed/ScatterGatherRoutedRemoteActorMultiJvmNode3.conf +++ b/akka-remote/src/multi-jvm/scala/akka/remote/scatter_gather_routed/ScatterGatherRoutedRemoteActorMultiJvmNode3.conf @@ -1,5 +1,11 @@ -akka.actor.provider = "akka.remote.RemoteActorRefProvider" -akka.loglevel = "WARNING" -akka.actor.deployment./app/service-hello.router = "scatter-gather" -akka.actor.deployment./app/service-hello.nr-of-instances = 3 -akka.actor.deployment./app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] +akka { + loglevel = "WARNING" + actor { + provider = "akka.remote.RemoteActorRefProvider" + deployment { + /app/service-hello.router = "scatter-gather" + /app/service-hello.nr-of-instances = 3 + /app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] + } + } +} diff --git a/akka-remote/src/multi-jvm/scala/akka/remote/scatter_gather_routed/ScatterGatherRoutedRemoteActorMultiJvmNode4.conf b/akka-remote/src/multi-jvm/scala/akka/remote/scatter_gather_routed/ScatterGatherRoutedRemoteActorMultiJvmNode4.conf index ae28dee91e..80ad72e3de 100644 --- a/akka-remote/src/multi-jvm/scala/akka/remote/scatter_gather_routed/ScatterGatherRoutedRemoteActorMultiJvmNode4.conf +++ b/akka-remote/src/multi-jvm/scala/akka/remote/scatter_gather_routed/ScatterGatherRoutedRemoteActorMultiJvmNode4.conf @@ -1,5 +1,11 @@ -akka.actor.provider = "akka.remote.RemoteActorRefProvider" -akka.loglevel = "WARNING" -akka.actor.deployment./app/service-hello.router = "scatter-gather" -akka.actor.deployment./app/service-hello.nr-of-instances = 3 -akka.actor.deployment./app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] +akka { + loglevel = "WARNING" + actor { + provider = "akka.remote.RemoteActorRefProvider" + deployment { + /app/service-hello.router = "scatter-gather" + /app/service-hello.nr-of-instances = 3 + /app/service-hello.remote.nodes = ["localhost:9991","localhost:9992","localhost:9993"] + } + } +} diff --git a/akka-remote/src/test/scala/akka/remote/AccrualFailureDetectorSpec.scala b/akka-remote/src/test/scala/akka/remote/AccrualFailureDetectorSpec.scala index 5b94895756..38d18ac6c5 100644 --- a/akka-remote/src/test/scala/akka/remote/AccrualFailureDetectorSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/AccrualFailureDetectorSpec.scala @@ -1,13 +1,11 @@ package akka.remote -import org.scalatest.WordSpec -import org.scalatest.matchers.MustMatchers - import java.net.InetSocketAddress +import akka.testkit.AkkaSpec -class AccrualFailureDetectorSpec extends WordSpec with MustMatchers { +class AccrualFailureDetectorSpec extends AkkaSpec { - "An AccrualFailureDetector" should { + "An AccrualFailureDetector" must { val conn = RemoteAddress(new InetSocketAddress("localhost", 2552)) "mark node as available after a series of successful heartbeats" in { diff --git a/akka-remote/src/test/scala/akka/remote/GossiperSpec.scala b/akka-remote/src/test/scala/akka/remote/GossiperSpec.scala index 06d0b73c5b..12e2925b26 100644 --- a/akka-remote/src/test/scala/akka/remote/GossiperSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/GossiperSpec.scala @@ -1,13 +1,11 @@ package akka.remote -import org.scalatest.WordSpec -import org.scalatest.matchers.MustMatchers - import java.net.InetSocketAddress +import akka.testkit.AkkaSpec -class GossiperSpec extends WordSpec with MustMatchers { +class GossiperSpec extends AkkaSpec { - "An Gossiper" should { + "An Gossiper" must { "..." in { } diff --git a/akka-remote/src/test/scala/akka/remote/VectorClockSpec.scala b/akka-remote/src/test/scala/akka/remote/VectorClockSpec.scala index 6cb4414b6c..5bfda16666 100644 --- a/akka-remote/src/test/scala/akka/remote/VectorClockSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/VectorClockSpec.scala @@ -1,11 +1,9 @@ package akka.remote -import org.scalatest.WordSpec -import org.scalatest.matchers.MustMatchers - import java.net.InetSocketAddress +import akka.testkit.AkkaSpec -class VectorClockSpec extends WordSpec with MustMatchers { +class VectorClockSpec extends AkkaSpec { import VectorClock._ "An VectorClock" must { diff --git a/akka-stm/src/main/resources/akka-stm-reference.conf b/akka-stm/src/main/resources/akka-stm-reference.conf new file mode 100644 index 0000000000..1142d44b8f --- /dev/null +++ b/akka-stm/src/main/resources/akka-stm-reference.conf @@ -0,0 +1,24 @@ +################################## +# Akka STM Reference Config File # +################################## + +# This the reference config file has all the default settings. +# Make your edits/overrides in your akka.conf. + +akka { + + stm { + fair = true # Should global transactions be fair or non-fair (non fair yield better performance) + max-retries = 1000 + timeout = 5 # Default timeout for blocking transactions and transaction set (in unit defined by + # the time-unit property) + write-skew = true + blocking-allowed = false + interruptible = false + speculative = true + quick-release = true + propagation = "requires" + trace-level = "none" + } + +} 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 f8b5700cd1..ad86126deb 100644 --- a/akka-stm/src/test/java/akka/stm/example/RetryExample.java +++ b/akka-stm/src/test/java/akka/stm/example/RetryExample.java @@ -3,48 +3,49 @@ package akka.stm.example; import akka.actor.ActorSystem; import akka.stm.*; import akka.actor.*; +import akka.testkit.AkkaSpec; public class RetryExample { - public static void main(String[] args) { - System.out.println(); - System.out.println("Retry example"); - System.out.println(); + public static void main(String[] args) { + System.out.println(); + System.out.println("Retry example"); + System.out.println(); - ActorSystem application = ActorSystem.create("RetryExample"); + ActorSystem application = ActorSystem.create("RetryExample", AkkaSpec.testConf()); - final Ref account1 = new Ref(100.0); - final Ref account2 = new Ref(100.0); + final Ref account1 = new Ref(100.0); + final Ref account2 = new Ref(100.0); - ActorRef transferer = application.actorOf(new Props().withCreator(Transferer.class)); + ActorRef transferer = application.actorOf(new Props().withCreator(Transferer.class)); - transferer.tell(new Transfer(account1, account2, 500.0)); - // Transferer: not enough money - retrying + transferer.tell(new Transfer(account1, account2, 500.0)); + // Transferer: not enough money - retrying - new Atomic() { - public Object atomically() { - return account1.set(account1.get() + 2000); - } - }.execute(); - // Transferer: transferring + new Atomic() { + public Object atomically() { + return account1.set(account1.get() + 2000); + } + }.execute(); + // Transferer: transferring - Double acc1 = new Atomic() { - public Double atomically() { - return account1.get(); - } - }.execute(); + Double acc1 = new Atomic() { + public Double atomically() { + return account1.get(); + } + }.execute(); - Double acc2 = new Atomic() { - public Double atomically() { - return account2.get(); - } - }.execute(); + Double acc2 = new Atomic() { + public Double atomically() { + return account2.get(); + } + }.execute(); - System.out.println("Account 1: " + acc1); - // Account 1: 1600.0 + System.out.println("Account 1: " + acc1); + // Account 1: 1600.0 - System.out.println("Account 2: " + acc2); - // Account 2: 600.0 + System.out.println("Account 2: " + acc2); + // Account 2: 600.0 - transferer.stop(); - } + transferer.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 dd8498ef42..344c98dfee 100644 --- a/akka-stm/src/test/java/akka/transactor/example/UntypedCoordinatedExample.java +++ b/akka-stm/src/test/java/akka/transactor/example/UntypedCoordinatedExample.java @@ -4,45 +4,46 @@ import akka.actor.ActorSystem; import akka.actor.ActorRef; import akka.actor.Props; import akka.dispatch.Future; +import akka.testkit.AkkaSpec; 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(); + 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"); + 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)); + ActorRef counter1 = application.actorOf(new Props().withCreator(UntypedCoordinatedCounter.class)); + ActorRef counter2 = application.actorOf(new Props().withCreator(UntypedCoordinatedCounter.class)); - counter1.tell(new Coordinated(new Increment(counter2))); + counter1.tell(new Coordinated(new Increment(counter2))); - Thread.sleep(3000); + Thread.sleep(3000); - long timeout = 5000; + long timeout = 5000; - Future future1 = counter1.ask("GetCount", timeout); - Future future2 = counter2.ask("GetCount", timeout); + Future future1 = counter1.ask("GetCount", timeout); + Future future2 = counter2.ask("GetCount", timeout); - future1.await(); - if (future1.isCompleted()) { - if (future1.result().isDefined()) { - int result = (Integer) future1.result().get(); - System.out.println("counter 1: " + result); - } - } - - future2.await(); - if (future2.isCompleted()) { - if (future2.result().isDefined()) { - int result = (Integer) future2.result().get(); - System.out.println("counter 2: " + result); - } - } - - counter1.stop(); - counter2.stop(); + future1.await(); + if (future1.isCompleted()) { + if (future1.result().isDefined()) { + int result = (Integer) future1.result().get(); + System.out.println("counter 1: " + result); + } } + + future2.await(); + if (future2.isCompleted()) { + if (future2.result().isDefined()) { + int result = (Integer) future2.result().get(); + System.out.println("counter 2: " + result); + } + } + + counter1.stop(); + counter2.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 0e425e4094..882d5b7b1f 100644 --- a/akka-stm/src/test/java/akka/transactor/example/UntypedTransactorExample.java +++ b/akka-stm/src/test/java/akka/transactor/example/UntypedTransactorExample.java @@ -4,44 +4,45 @@ import akka.actor.ActorSystem; import akka.actor.ActorRef; import akka.actor.Props; import akka.dispatch.Future; +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(); + 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"); + 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)); + ActorRef counter1 = application.actorOf(new Props().withCreator(UntypedCounter.class)); + ActorRef counter2 = application.actorOf(new Props().withCreator(UntypedCounter.class)); - counter1.tell(new Increment(counter2)); + counter1.tell(new Increment(counter2)); - Thread.sleep(3000); + Thread.sleep(3000); - long timeout = 5000; + long timeout = 5000; - Future future1 = counter1.ask("GetCount", timeout); - Future future2 = counter2.ask("GetCount", timeout); + Future future1 = counter1.ask("GetCount", timeout); + Future future2 = counter2.ask("GetCount", timeout); - future1.await(); - if (future1.isCompleted()) { - if (future1.result().isDefined()) { - int result = (Integer) future1.result().get(); - System.out.println("counter 1: " + result); - } - } - - future2.await(); - if (future2.isCompleted()) { - if (future2.result().isDefined()) { - int result = (Integer) future2.result().get(); - System.out.println("counter 2: " + result); - } - } - - counter1.stop(); - counter2.stop(); + future1.await(); + if (future1.isCompleted()) { + if (future1.result().isDefined()) { + int result = (Integer) future1.result().get(); + System.out.println("counter 1: " + result); + } } + + future2.await(); + if (future2.isCompleted()) { + if (future2.result().isDefined()) { + int result = (Integer) future2.result().get(); + System.out.println("counter 2: " + result); + } + } + + counter1.stop(); + counter2.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 e4d861f894..0d44d16496 100644 --- a/akka-stm/src/test/java/akka/transactor/test/UntypedCoordinatedIncrementTest.java +++ b/akka-stm/src/test/java/akka/transactor/test/UntypedCoordinatedIncrementTest.java @@ -14,6 +14,7 @@ import akka.actor.Props; import akka.actor.UntypedActor; import akka.actor.UntypedActorFactory; import akka.dispatch.Future; +import akka.testkit.AkkaSpec; import akka.testkit.EventFilter; import akka.testkit.ErrorFilter; import akka.testkit.TestEvent; @@ -30,70 +31,73 @@ import scala.collection.JavaConverters; import scala.collection.Seq; public class UntypedCoordinatedIncrementTest { - ActorSystem application = ActorSystem.create("UntypedCoordinatedIncrementTest"); + ActorSystem application = ActorSystem.create("UntypedCoordinatedIncrementTest", AkkaSpec.testConf()); - List counters; - ActorRef failer; + List counters; + ActorRef failer; - int numCounters = 3; - int timeout = 5; - int askTimeout = 5000; + int numCounters = 3; + int timeout = 5; + int askTimeout = 5000; - @Before public void initialise() { - Props p = new Props().withCreator(UntypedFailer.class); - counters = new ArrayList(); - for (int i = 1; i <= numCounters; i++) { - final String name = "counter" + i; - ActorRef counter = application.actorOf(new Props().withCreator(new UntypedActorFactory() { - public UntypedActor create() { - return new UntypedCoordinatedCounter(name); - } - })); - counters.add(counter); + @Before + public void initialise() { + Props p = new Props().withCreator(UntypedFailer.class); + counters = new ArrayList(); + for (int i = 1; i <= numCounters; i++) { + final String name = "counter" + i; + ActorRef counter = application.actorOf(new Props().withCreator(new UntypedActorFactory() { + public UntypedActor create() { + return new UntypedCoordinatedCounter(name); } - failer = application.actorOf(p); + })); + counters.add(counter); } + failer = application.actorOf(p); + } - @Test public void incrementAllCountersWithSuccessfulTransaction() { - CountDownLatch incrementLatch = new CountDownLatch(numCounters); - Increment message = new Increment(counters.subList(1, counters.size()), incrementLatch); - counters.get(0).tell(new Coordinated(message)); - try { - incrementLatch.await(timeout, TimeUnit.SECONDS); - } catch (InterruptedException exception) {} - for (ActorRef counter : counters) { - Future future = counter.ask("GetCount", askTimeout); - assertEquals(1, ((Integer)future.get()).intValue()); - } + @Test + public void incrementAllCountersWithSuccessfulTransaction() { + CountDownLatch incrementLatch = new CountDownLatch(numCounters); + Increment message = new Increment(counters.subList(1, counters.size()), incrementLatch); + counters.get(0).tell(new Coordinated(message)); + try { + incrementLatch.await(timeout, TimeUnit.SECONDS); + } catch (InterruptedException exception) { } + for (ActorRef counter : counters) { + Future future = counter.ask("GetCount", askTimeout); + assertEquals(1, ((Integer) future.get()).intValue()); + } + } - @Test public void incrementNoCountersWithFailingTransaction() { - 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)); - CountDownLatch incrementLatch = new CountDownLatch(numCounters); - List actors = new ArrayList(counters); - actors.add(failer); - Increment message = new Increment(actors.subList(1, actors.size()), incrementLatch); - actors.get(0).tell(new Coordinated(message)); - try { - incrementLatch.await(timeout, TimeUnit.SECONDS); - } catch (InterruptedException exception) {} - for (ActorRef counter : counters) { - Future future = counter.ask("GetCount", askTimeout); - assertEquals(0, ((Integer)future.get()).intValue()); - } + @Test + public void incrementNoCountersWithFailingTransaction() { + 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)); + CountDownLatch incrementLatch = new CountDownLatch(numCounters); + List actors = new ArrayList(counters); + actors.add(failer); + Increment message = new Increment(actors.subList(1, actors.size()), incrementLatch); + actors.get(0).tell(new Coordinated(message)); + try { + incrementLatch.await(timeout, TimeUnit.SECONDS); + } catch (InterruptedException exception) { } + for (ActorRef counter : counters) { + Future future = counter.ask("GetCount", askTimeout); + assertEquals(0, ((Integer) future.get()).intValue()); + } + } - public Seq seq(A... args) { - return JavaConverters.collectionAsScalaIterableConverter(Arrays.asList(args)).asScala().toSeq(); - } + public Seq seq(A... args) { + return JavaConverters.collectionAsScalaIterableConverter(Arrays.asList(args)).asScala().toSeq(); + } - @After - public void stop() { - application.stop(); - } + @After + public void stop() { + application.stop(); + } } - - 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 7de09c6697..8d2a3e4db8 100644 --- a/akka-stm/src/test/java/akka/transactor/test/UntypedTransactorTest.java +++ b/akka-stm/src/test/java/akka/transactor/test/UntypedTransactorTest.java @@ -25,82 +25,86 @@ import java.util.concurrent.TimeUnit; import scala.Option; import scala.collection.JavaConverters; import scala.collection.Seq; +import akka.testkit.AkkaSpec; public class UntypedTransactorTest { - ActorSystem application = ActorSystem.create("UntypedTransactorTest"); + ActorSystem application = ActorSystem.create("UntypedTransactorTest", AkkaSpec.testConf()); - List counters; - ActorRef failer; + List counters; + ActorRef failer; - int numCounters = 3; - int timeout = 5; - int askTimeout = 5000; + int numCounters = 3; + int timeout = 5; + int askTimeout = 5000; - @Before public void initialise() { - counters = new ArrayList(); - for (int i = 1; i <= numCounters; i++) { - final String name = "counter" + i; - ActorRef counter = application.actorOf(new Props().withCreator(new UntypedActorFactory() { - public UntypedActor create() { - return new UntypedCounter(name); - } - })); - counters.add(counter); + @Before + public void initialise() { + counters = new ArrayList(); + for (int i = 1; i <= numCounters; i++) { + final String name = "counter" + i; + ActorRef counter = application.actorOf(new Props().withCreator(new UntypedActorFactory() { + public UntypedActor create() { + return new UntypedCounter(name); } - failer = application.actorOf(new Props().withCreator(UntypedFailer.class)); + })); + counters.add(counter); } + failer = application.actorOf(new Props().withCreator(UntypedFailer.class)); + } - @Test public void incrementAllCountersWithSuccessfulTransaction() { - CountDownLatch incrementLatch = new CountDownLatch(numCounters); - Increment message = new Increment(counters.subList(1, counters.size()), incrementLatch); - counters.get(0).tell(message); - try { - incrementLatch.await(timeout, TimeUnit.SECONDS); - } catch (InterruptedException exception) {} - for (ActorRef counter : counters) { - Future future = counter.ask("GetCount", askTimeout); - future.await(); - if (future.isCompleted()) { - Option resultOption = future.result(); - if (resultOption.isDefined()) { - Object result = resultOption.get(); - int count = (Integer) result; - assertEquals(1, count); - } - } + @Test + public void incrementAllCountersWithSuccessfulTransaction() { + CountDownLatch incrementLatch = new CountDownLatch(numCounters); + Increment message = new Increment(counters.subList(1, counters.size()), incrementLatch); + counters.get(0).tell(message); + try { + incrementLatch.await(timeout, TimeUnit.SECONDS); + } catch (InterruptedException exception) { + } + for (ActorRef counter : counters) { + Future future = counter.ask("GetCount", askTimeout); + future.await(); + if (future.isCompleted()) { + Option resultOption = future.result(); + if (resultOption.isDefined()) { + Object result = resultOption.get(); + int count = (Integer) result; + assertEquals(1, count); } + } } + } - @Test public void incrementNoCountersWithFailingTransaction() { - 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)); - CountDownLatch incrementLatch = new CountDownLatch(numCounters); - List actors = new ArrayList(counters); - actors.add(failer); - Increment message = new Increment(actors.subList(1, actors.size()), incrementLatch); - actors.get(0).tell(message); - try { - incrementLatch.await(timeout, TimeUnit.SECONDS); - } catch (InterruptedException exception) {} - for (ActorRef counter : counters) { - Future future = counter.ask("GetCount", askTimeout); - future.await(); - if (future.isCompleted()) { - Option resultOption = future.result(); - if (resultOption.isDefined()) { - Object result = resultOption.get(); - int count = (Integer) result; - assertEquals(0, count); - } - } + @Test + public void incrementNoCountersWithFailingTransaction() { + 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)); + CountDownLatch incrementLatch = new CountDownLatch(numCounters); + List actors = new ArrayList(counters); + actors.add(failer); + Increment message = new Increment(actors.subList(1, actors.size()), incrementLatch); + actors.get(0).tell(message); + try { + incrementLatch.await(timeout, TimeUnit.SECONDS); + } catch (InterruptedException exception) { + } + for (ActorRef counter : counters) { + Future future = counter.ask("GetCount", askTimeout); + future.await(); + if (future.isCompleted()) { + Option resultOption = future.result(); + if (resultOption.isDefined()) { + Object result = resultOption.get(); + int count = (Integer) result; + assertEquals(0, count); } + } } + } - public Seq seq(A... args) { - return JavaConverters.collectionAsScalaIterableConverter(Arrays.asList(args)).asScala().toSeq(); - } + public Seq seq(A... args) { + return JavaConverters.collectionAsScalaIterableConverter(Arrays.asList(args)).asScala().toSeq(); + } } - - diff --git a/akka-stm/src/test/scala/agent/AgentSpec.scala b/akka-stm/src/test/scala/akka/agent/test/AgentSpec.scala similarity index 100% rename from akka-stm/src/test/scala/agent/AgentSpec.scala rename to akka-stm/src/test/scala/akka/agent/test/AgentSpec.scala diff --git a/akka-stm/src/test/scala/akka/stm/test/ConfigSpec.scala b/akka-stm/src/test/scala/akka/stm/test/ConfigSpec.scala new file mode 100644 index 0000000000..7204f01468 --- /dev/null +++ b/akka-stm/src/test/scala/akka/stm/test/ConfigSpec.scala @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2009-2011 Typesafe Inc. + */ + +package akka.stm.test + +import org.junit.runner.RunWith +import org.scalatest.WordSpec +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)) { + + "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 { + val config = system.settings.config + import config._ + + // TODO are these config values used anywhere? + + getBoolean("akka.stm.blocking-allowed") must equal(false) + getBoolean("akka.stm.fair") must equal(true) + getBoolean("akka.stm.interruptible") must equal(false) + getInt("akka.stm.max-retries") must equal(1000) + getString("akka.stm.propagation") must equal("requires") + getBoolean("akka.stm.quick-release") must equal(true) + getBoolean("akka.stm.speculative") must equal(true) + getLong("akka.stm.timeout") must equal(5) + getString("akka.stm.trace-level") must equal("none") + getBoolean("akka.stm.write-skew") must equal(true) + } + } +} diff --git a/akka-stm/src/test/scala/stm/JavaStmSpec.scala b/akka-stm/src/test/scala/akka/stm/test/JavaStmSpec.scala similarity index 100% rename from akka-stm/src/test/scala/stm/JavaStmSpec.scala rename to akka-stm/src/test/scala/akka/stm/test/JavaStmSpec.scala diff --git a/akka-stm/src/test/scala/stm/RefSpec.scala b/akka-stm/src/test/scala/akka/stm/test/RefSpec.scala similarity index 100% rename from akka-stm/src/test/scala/stm/RefSpec.scala rename to akka-stm/src/test/scala/akka/stm/test/RefSpec.scala diff --git a/akka-stm/src/test/scala/stm/StmSpec.scala b/akka-stm/src/test/scala/akka/stm/test/StmSpec.scala similarity index 99% rename from akka-stm/src/test/scala/stm/StmSpec.scala rename to akka-stm/src/test/scala/akka/stm/test/StmSpec.scala index e7890b138d..1f547fc1ae 100644 --- a/akka-stm/src/test/scala/stm/StmSpec.scala +++ b/akka-stm/src/test/scala/akka/stm/test/StmSpec.scala @@ -1,7 +1,7 @@ package akka.stm.test import akka.actor.Actor -import Actor._ +import akka.actor.Actor._ import org.multiverse.api.exceptions.ReadonlyException diff --git a/akka-stm/src/test/scala/transactor/CoordinatedIncrementSpec.scala b/akka-stm/src/test/scala/akka/transactor/test/CoordinatedIncrementSpec.scala similarity index 95% rename from akka-stm/src/test/scala/transactor/CoordinatedIncrementSpec.scala rename to akka-stm/src/test/scala/akka/transactor/test/CoordinatedIncrementSpec.scala index e6e015a546..eda336b78e 100644 --- a/akka-stm/src/test/scala/transactor/CoordinatedIncrementSpec.scala +++ b/akka-stm/src/test/scala/akka/transactor/test/CoordinatedIncrementSpec.scala @@ -1,13 +1,11 @@ -package akka.transactor.test +package akka.transactor import org.scalatest.BeforeAndAfterAll import akka.actor.ActorSystem -import akka.transactor.Coordinated import akka.actor._ import akka.stm.{ Ref, TransactionFactory } import akka.util.duration._ -import akka.transactor.CoordinatedTransactionException import akka.testkit._ object CoordinatedIncrement { diff --git a/akka-stm/src/test/scala/transactor/FickleFriendsSpec.scala b/akka-stm/src/test/scala/akka/transactor/test/FickleFriendsSpec.scala similarity index 96% rename from akka-stm/src/test/scala/transactor/FickleFriendsSpec.scala rename to akka-stm/src/test/scala/akka/transactor/test/FickleFriendsSpec.scala index 885ce0283c..a74490b410 100644 --- a/akka-stm/src/test/scala/transactor/FickleFriendsSpec.scala +++ b/akka-stm/src/test/scala/akka/transactor/test/FickleFriendsSpec.scala @@ -1,14 +1,12 @@ -package akka.transactor.test +package akka.transactor import org.scalatest.WordSpec import org.scalatest.matchers.MustMatchers import org.scalatest.BeforeAndAfterAll import akka.actor.ActorSystem -import akka.transactor.Coordinated import akka.actor._ import akka.stm._ import akka.util.duration._ -import akka.transactor.CoordinatedTransactionException import akka.testkit._ import scala.util.Random.{ nextInt ⇒ random } import java.util.concurrent.CountDownLatch diff --git a/akka-stm/src/test/scala/transactor/JavaUntypedCoordinatedSpec.scala b/akka-stm/src/test/scala/akka/transactor/test/JavaUntypedCoordinatedSpec.scala similarity index 87% rename from akka-stm/src/test/scala/transactor/JavaUntypedCoordinatedSpec.scala rename to akka-stm/src/test/scala/akka/transactor/test/JavaUntypedCoordinatedSpec.scala index c2a96f436c..f48705469c 100644 --- a/akka-stm/src/test/scala/transactor/JavaUntypedCoordinatedSpec.scala +++ b/akka-stm/src/test/scala/akka/transactor/test/JavaUntypedCoordinatedSpec.scala @@ -1,4 +1,4 @@ -package akka.transactor.test +package akka.transactor import org.scalatest.junit.JUnitWrapperSuite diff --git a/akka-stm/src/test/scala/transactor/JavaUntypedTransactorSpec.scala b/akka-stm/src/test/scala/akka/transactor/test/JavaUntypedTransactorSpec.scala similarity index 87% rename from akka-stm/src/test/scala/transactor/JavaUntypedTransactorSpec.scala rename to akka-stm/src/test/scala/akka/transactor/test/JavaUntypedTransactorSpec.scala index a643fb141d..d4da5f0545 100644 --- a/akka-stm/src/test/scala/transactor/JavaUntypedTransactorSpec.scala +++ b/akka-stm/src/test/scala/akka/transactor/test/JavaUntypedTransactorSpec.scala @@ -1,4 +1,4 @@ -package akka.transactor.test +package akka.transactor import org.scalatest.junit.JUnitWrapperSuite diff --git a/akka-stm/src/test/scala/transactor/TransactorSpec.scala b/akka-stm/src/test/scala/akka/transactor/test/TransactorSpec.scala similarity index 96% rename from akka-stm/src/test/scala/transactor/TransactorSpec.scala rename to akka-stm/src/test/scala/akka/transactor/test/TransactorSpec.scala index 9dc195e39a..43ee399196 100644 --- a/akka-stm/src/test/scala/transactor/TransactorSpec.scala +++ b/akka-stm/src/test/scala/akka/transactor/test/TransactorSpec.scala @@ -1,14 +1,12 @@ -package akka.transactor.test +package akka.transactor import org.scalatest.WordSpec import org.scalatest.matchers.MustMatchers import akka.actor.ActorSystem -import akka.transactor.Transactor import akka.actor._ import akka.stm._ import akka.util.duration._ -import akka.transactor.CoordinatedTransactionException import akka.testkit._ object TransactorIncrement { diff --git a/akka-stm/src/test/scala/config/ConfigSpec.scala b/akka-stm/src/test/scala/config/ConfigSpec.scala deleted file mode 100644 index ffb28e206e..0000000000 --- a/akka-stm/src/test/scala/config/ConfigSpec.scala +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (C) 2009-2011 Typesafe Inc. - */ - -package akka.config - -import org.junit.runner.RunWith -import org.scalatest.WordSpec -import org.scalatest.junit.JUnitRunner -import org.scalatest.matchers.MustMatchers - -import akka.actor.ActorSystem - -@RunWith(classOf[JUnitRunner]) -class ConfigSpec extends WordSpec with MustMatchers { - - "The default configuration file (i.e. akka-reference.conf)" should { - "contain all configuration properties for akka-stm that are used in code with their correct defaults" in { - val config = ActorSystem("ConfigSpec").settings.config - - import config._ - - getBool("akka.stm.blocking-allowed") must equal(Some(false)) - getBool("akka.stm.fair") must equal(Some(true)) - getBool("akka.stm.interruptible") must equal(Some(false)) - getInt("akka.stm.max-retries") must equal(Some(1000)) - getString("akka.stm.propagation") must equal(Some("requires")) - getBool("akka.stm.quick-release") must equal(Some(true)) - getBool("akka.stm.speculative") must equal(Some(true)) - getLong("akka.stm.timeout") must equal(Some(5)) - getString("akka.stm.trace-level") must equal(Some("none")) - getBool("akka.stm.write-skew") must equal(Some(true)) - } - } -} diff --git a/akka-testkit/src/main/resources/akka-testkit-reference.conf b/akka-testkit/src/main/resources/akka-testkit-reference.conf new file mode 100644 index 0000000000..017a00eb21 --- /dev/null +++ b/akka-testkit/src/main/resources/akka-testkit-reference.conf @@ -0,0 +1,14 @@ +###################################### +# Akka Testkit Reference Config File # +###################################### + +# This the reference config file has all the default settings. +# Make your edits/overrides in your akka.conf. + +akka { + test { + timefactor = "1.0" # factor by which to scale timeouts during tests, e.g. to account for shared build system load + filter-leeway = 3 # time-units EventFilter.intercept waits after the block is finished until all required messages are received + single-expect-default = 3 # time-units to wait in expectMsg and friends outside of within() block by default + } +} diff --git a/akka-testkit/src/test/scala/akka/testkit/AkkaSpec.scala b/akka-testkit/src/test/scala/akka/testkit/AkkaSpec.scala index c365cd43fa..93c6df8060 100644 --- a/akka-testkit/src/test/scala/akka/testkit/AkkaSpec.scala +++ b/akka-testkit/src/test/scala/akka/testkit/AkkaSpec.scala @@ -3,7 +3,6 @@ */ package akka.testkit -import akka.config.Configuration import org.scalatest.{ WordSpec, BeforeAndAfterAll, Tag } import org.scalatest.matchers.MustMatchers import akka.actor.{ ActorSystem, ActorSystemImpl } @@ -12,10 +11,37 @@ import akka.dispatch.MessageDispatcher import akka.event.{ Logging, LoggingAdapter } import akka.util.duration._ import akka.dispatch.FutureTimeoutException +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import com.typesafe.config.ConfigParseOptions +import java.io.StringReader object TimingTest extends Tag("timing") -abstract class AkkaSpec(_application: ActorSystem = ActorSystem()) +object AkkaSpec { + val testConf = + ActorSystem.DefaultConfigurationLoader.defaultConfig.withFallback( + ConfigFactory.parseReader(new StringReader(""" + akka { + event-handlers = ["akka.testkit.TestEventListener"] + loglevel = "WARNING" + actor { + default-dispatcher { + core-pool-size = 4 + max-pool-size = 32 + } + } + } + """), ConfigParseOptions.defaults)) + + def mapToConfig(map: Map[String, Any]): Config = { + import scala.collection.JavaConverters._ + ConfigFactory.parseMap(map.asJava) + } + +} + +abstract class AkkaSpec(_application: ActorSystem = ActorSystem(getClass.getSimpleName, AkkaSpec.testConf)) extends TestKit(_application) with WordSpec with MustMatchers with BeforeAndAfterAll { val log: LoggingAdapter = Logging(system, this) @@ -36,7 +62,11 @@ abstract class AkkaSpec(_application: ActorSystem = ActorSystem()) protected def atTermination() {} - def this(config: Configuration) = this(ActorSystem(getClass.getSimpleName, ActorSystem.defaultConfig ++ config)) + def this(config: Config) = this(ActorSystem(getClass.getSimpleName, config.withFallback(AkkaSpec.testConf))) + + def this(configMap: Map[String, _]) = { + this(AkkaSpec.mapToConfig(configMap).withFallback(AkkaSpec.testConf)) + } def actorOf(props: Props): ActorRef = system.actorOf(props) @@ -55,10 +85,12 @@ abstract class AkkaSpec(_application: ActorSystem = ActorSystem()) class AkkaSpecSpec extends WordSpec with MustMatchers { "An AkkaSpec" must { "terminate all actors" in { - import ActorSystem.defaultConfig - val system = ActorSystem("test", defaultConfig ++ Configuration( + 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")) + "akka.loglevel" -> "DEBUG", "akka.stdout-loglevel" -> "DEBUG") + val system = ActorSystem("test", ConfigFactory.parseMap(conf.asJava).withFallback(defaultConfig)) val spec = new AkkaSpec(system) { val ref = Seq(testActor, system.actorOf(Props.empty, "name")) } diff --git a/akka-testkit/src/test/scala/akka/testkit/TestTimeSpec.scala b/akka-testkit/src/test/scala/akka/testkit/TestTimeSpec.scala index b799df3751..6c33062882 100644 --- a/akka-testkit/src/test/scala/akka/testkit/TestTimeSpec.scala +++ b/akka-testkit/src/test/scala/akka/testkit/TestTimeSpec.scala @@ -3,10 +3,10 @@ package akka.testkit import org.scalatest.matchers.MustMatchers import org.scalatest.{ BeforeAndAfterEach, WordSpec } import akka.util.Duration -import akka.config.Configuration +import com.typesafe.config.Config @org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner]) -class TestTimeSpec extends AkkaSpec(Configuration("akka.test.timefactor" -> 2.0)) with BeforeAndAfterEach { +class TestTimeSpec extends AkkaSpec(Map("akka.test.timefactor" -> 2.0)) with BeforeAndAfterEach { "A TestKit" must { diff --git a/config/akka.conf b/config/akka.conf index 84b9bfbbcf..64883cf7c1 100644 --- a/config/akka.conf +++ b/config/akka.conf @@ -1,5 +1,2 @@ -# This config imports the Akka reference configuration. -include "akka-reference.conf" - # In this file you can override any option defined in the 'akka-reference.conf' file. # Copy in all or parts of the 'akka-reference.conf' file and modify as you please. diff --git a/config/akka.test.conf b/config/akka.test.conf deleted file mode 100644 index 8e21a7d184..0000000000 --- a/config/akka.test.conf +++ /dev/null @@ -1,16 +0,0 @@ -# This config imports the Akka reference configuration. -include "akka-reference.conf" - -# In this file you can override any option defined in the 'akka-reference.conf' file. -# Copy in all or parts of the 'akka-reference.conf' file and modify as you please. - -akka { - event-handlers = ["akka.testkit.TestEventListener"] - loglevel = "WARNING" - actor { - default-dispatcher { - core-pool-size = 4 - max-pool-size = 32 - } - } -}