diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c8da947
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+target
+*.iml
+.idea
+out
diff --git a/AGPL_FILE_HEADER b/AGPL_FILE_HEADER
new file mode 100755
index 0000000..76296e4
--- /dev/null
+++ b/AGPL_FILE_HEADER
@@ -0,0 +1,17 @@
+Copyright 2011-2016 Green Energy Corp.
+
+Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+contributor license agreements. See the NOTICE file distributed with this
+work for additional information regarding copyright ownership. Green Energy
+Corp licenses this file to you under the GNU Affero General Public License
+Version 3.0 (the "License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+http://www.gnu.org/licenses/agpl.html
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations under
+the License.
+
diff --git a/APACHE_FILE_HEADER b/APACHE_FILE_HEADER
new file mode 100755
index 0000000..eedcedf
--- /dev/null
+++ b/APACHE_FILE_HEADER
@@ -0,0 +1,16 @@
+Copyright 2011-2016 Green Energy Corp.
+
+Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+contributor license agreements. See the NOTICE file distributed with this
+work for additional information regarding copyright ownership. Green Energy
+Corp licenses this file to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations under
+the License.
\ No newline at end of file
diff --git a/CodeFormat.xml b/CodeFormat.xml
new file mode 100755
index 0000000..b750e93
--- /dev/null
+++ b/CodeFormat.xml
@@ -0,0 +1,279 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..daaf964
--- /dev/null
+++ b/README.md
@@ -0,0 +1,32 @@
+GreenBus Platform
+--------------------
+
+Prerequisites:
+
+- Maven 3
+- Postgresql
+- Qpid Broker (for integration tests)
+- Google Protocol Buffers compiler (version 2.5.0)
+
+### Setting up Postgresql
+
+The Postgres install must be initialized with the databases and users. The relevant configuration is contained in
+the init_postgres.sql.
+
+ sudo su postgres -c psql < init_postgres.sql
+
+### Setting up Qpid
+
+Qpid needs have auth settings that match the configuration in `io.greenbus.msg.amqp.cfg`. For development purposes,
+the Qpid auth can be disabled entirely by setting `auth=no` in the broker configuration and removing references to the
+ACL submodule.
+
+### Building with Maven
+
+Build:
+
+ mvn clean install
+
+### Dependency Projects
+
+The GreenBus platform project is based on [GreenBus Messaging](https://github.com/gec/greenbus-msg).
\ No newline at end of file
diff --git a/app-framework/pom.xml b/app-framework/pom.xml
new file mode 100755
index 0000000..01ea3c2
--- /dev/null
+++ b/app-framework/pom.xml
@@ -0,0 +1,91 @@
+
+ 4.0.0
+
+
+ io.greenbus
+ greenbus-scala-base
+ 3.0.0
+ ../scala-base
+
+
+ greenbus-app-framework
+ jar
+
+
+
+ AGPLv3
+ http://www.gnu.org/licenses/agpl-3.0.txt
+
+
+
+
+
+
+
+ com.mycila.maven-license-plugin
+ maven-license-plugin
+
+
+
+
+
+ maven-assembly-plugin
+
+
+ jar-with-dependencies
+
+
+
+
+ make-assembly
+ package
+
+ single
+
+
+
+
+
+
+
+
+
+ io.greenbus
+ greenbus-client
+ 3.0.0
+
+
+ io.greenbus
+ greenbus-util
+ 3.0.0
+
+
+ io.greenbus.msg
+ greenbus-msg-qpid
+ 1.0.0
+
+
+ com.typesafe.play
+ play-json_2.10
+ 2.3.9
+
+
+ com.typesafe.akka
+ akka-actor_2.10
+ 2.2.0
+
+
+ com.typesafe.akka
+ akka-slf4j_2.10
+ 2.2.0
+
+
+ commons-io
+ commons-io
+ 2.4
+
+
+
+
+
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/AmqpConnectionConfig.scala b/app-framework/src/main/scala/io/greenbus/app/actor/AmqpConnectionConfig.scala
new file mode 100644
index 0000000..75da1b4
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/AmqpConnectionConfig.scala
@@ -0,0 +1,39 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor
+
+case class AmqpConnectionConfig(amqpConfigFileList: IndexedSeq[String],
+ failureLimit: Int,
+ retryDelayMs: Long,
+ connectionTimeoutMs: Long)
+
+object AmqpConnectionConfig {
+
+ def default(path: String): AmqpConnectionConfig = {
+ AmqpConnectionConfig(Vector(path), failureLimit = 1, retryDelayMs = 5000, connectionTimeoutMs = 10000)
+ }
+
+ def default(paths: Seq[String]): AmqpConnectionConfig = {
+ AmqpConnectionConfig(paths.toVector, failureLimit = 1, retryDelayMs = 5000, connectionTimeoutMs = 10000)
+ }
+
+ def load(paths: Seq[String], failureLimit: Int, retryDelayMs: Long, connectionTimeoutMs: Long): AmqpConnectionConfig = {
+ AmqpConnectionConfig(paths.toVector, failureLimit, retryDelayMs, connectionTimeoutMs)
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/AmqpConnectionManager.scala b/app-framework/src/main/scala/io/greenbus/app/actor/AmqpConnectionManager.scala
new file mode 100644
index 0000000..b5cdcdd
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/AmqpConnectionManager.scala
@@ -0,0 +1,179 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor
+
+import akka.actor._
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.app.actor.util.NestedStateMachine
+import io.greenbus.client.ServiceConnection
+import io.greenbus.client.exception.UnauthorizedException
+import io.greenbus.msg.SessionUnusableException
+import io.greenbus.msg.amqp.AmqpSettings
+import io.greenbus.msg.qpid.QpidBroker
+import scala.concurrent.duration._
+
+object FailoverConnectionManager {
+
+ sealed trait State
+ case class Down(configOffset: Int, failures: Int, sequence: Int) extends State
+ case class Running(configOffset: Int, connection: ServiceConnection, child: ActorRef, sequence: Int) extends State
+
+ case object AttemptConnect
+ case class LostBrokerConnection(expected: Boolean, sequence: Int)
+
+ def props(processName: String,
+ amqpConfigFileList: IndexedSeq[String],
+ failureLimit: Int,
+ retryDelayMs: Long,
+ connectionTimeoutMs: Long,
+ factory: ServiceConnection => Props): Props = {
+ Props(classOf[FailoverConnectionManager], processName, amqpConfigFileList, failureLimit, retryDelayMs, connectionTimeoutMs, factory)
+ }
+}
+
+class FailoverConnectionManager(
+ processName: String,
+ amqpConfigFileList: IndexedSeq[String],
+ failureLimit: Int,
+ retryDelayMs: Long,
+ connectionTimeoutMs: Long,
+ factory: ServiceConnection => Props) extends NestedStateMachine with MessageScheduling with Logging {
+ import FailoverConnectionManager._
+
+ protected type StateType = State
+ protected def start: StateType = Down(0, 0, 0)
+
+ self ! AttemptConnect
+
+ protected def machine = {
+
+ case state @ Down(offset, failures, sequence) => {
+
+ case AttemptConnect => {
+
+ val configToAttempt = amqpConfigFileList(offset)
+ logger.info(s"Process $processName attempting connection using config: $configToAttempt")
+ logger.debug(s"Process $processName attempting connection, offset: $offset, failures: $failures, list: $amqpConfigFileList")
+
+ try {
+ val connection = connectQpid(configToAttempt)
+
+ connection.addConnectionListener { expected =>
+ self ! LostBrokerConnection(expected, sequence)
+ }
+
+ val child = context.actorOf(factory(connection))
+
+ Running(offset, connection, child, sequence)
+
+ } catch {
+ case ex: Throwable =>
+
+ logger.error(s"Couldn't initialize connection for $processName: " + ex.getMessage)
+
+ scheduleMsg(retryDelayMs, AttemptConnect)
+ if (failures >= failureLimit) {
+ val nextOffset = if (offset >= (amqpConfigFileList.size - 1)) 0 else offset + 1
+ Down(nextOffset, 0, sequence)
+ } else {
+ Down(offset, failures + 1, sequence)
+ }
+
+ }
+ }
+ case LostBrokerConnection(expected, connSeq) => {
+ if (!expected) {
+ logger.warn(s"Saw unexpected lost connection event while $processName was already down")
+ }
+ state
+ }
+ }
+
+ case state @ Running(configOffset, connection, child, sequence) => {
+
+ case LostBrokerConnection(expected, connSeq) => {
+ if (connSeq == sequence) {
+
+ try {
+ connection.disconnect()
+ } catch {
+ case ex: Throwable =>
+ }
+ child ! PoisonPill
+
+ logger.info(s"Process $processName lost connection")
+
+ scheduleMsg(retryDelayMs, AttemptConnect)
+
+ Down(configOffset, 0, sequence + 1)
+
+ } else {
+ state
+ }
+ }
+
+ case RestartConnection(reason) => {
+ logger.warn(s"Restarting $processName due to $reason")
+
+ try {
+ connection.disconnect()
+ } catch {
+ case ex: Throwable =>
+ }
+ child ! PoisonPill
+
+ scheduleMsg(retryDelayMs, AttemptConnect)
+
+ Down(configOffset, 0, sequence + 1)
+ }
+ }
+ }
+
+ private def connectQpid(path: String): ServiceConnection = {
+ val config = AmqpSettings.load(path)
+ val connection = ServiceConnection.connect(config, QpidBroker, connectionTimeoutMs)
+ logger.info(s"Connected to broker: $config")
+ connection
+ }
+
+ override protected def onShutdown(state: State): Unit = {
+ state match {
+ case Running(configOffset, connection, child, sequence) =>
+ logger.info(s"Disconnecting on shutdown for $processName")
+ connection.disconnect()
+ case _ =>
+ }
+ }
+
+ override def supervisorStrategy: SupervisorStrategy = {
+ import SupervisorStrategy._
+ OneForOneStrategy(maxNrOfRetries = 1, withinTimeRange = 3.seconds) {
+ case _: SessionUnusableException =>
+ self ! RestartConnection("session error")
+ Stop
+ case _: UnauthorizedException =>
+ self ! RestartConnection("auth failure")
+ Stop
+ case ex: Throwable =>
+ self ! RestartConnection("unknown error: " + ex)
+ Stop
+ }
+ }
+
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/ConnectedApplicationManager.scala b/app-framework/src/main/scala/io/greenbus/app/actor/ConnectedApplicationManager.scala
new file mode 100644
index 0000000..bc95c35
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/ConnectedApplicationManager.scala
@@ -0,0 +1,210 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor
+
+import io.greenbus.client.ServiceConnection
+import io.greenbus.msg.{ SessionUnusableException, Session }
+import akka.actor._
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.msg.amqp.{ AmqpServiceOperations, AmqpSettings }
+import io.greenbus.msg.qpid.QpidBroker
+import io.greenbus.client.exception.{ ServiceException, UnauthorizedException }
+import scala.concurrent.duration._
+import akka.actor.OneForOneStrategy
+import io.greenbus.util.UserSettings
+
+case class RestartConnection(reason: String)
+
+object ConnectedApplicationManager {
+
+ sealed trait State
+ case object Down extends State
+ case object Connected extends State
+ case object Running extends State
+
+ sealed trait Data
+ case object NoData extends Data
+ case class ConnEstablished(conn: ServiceConnection) extends Data
+ case class SessionEstablished(child: ActorRef, session: Session, conn: ServiceConnection) extends Data
+
+ case object AttemptConnect
+ case object AttemptLogin
+ case class LoginSuccess(session: Session)
+ case class LoginFailure(ex: Throwable)
+ case class LostBrokerConnection(expected: Boolean)
+ //case class ConnectionRestart(reason: String)
+
+ def props(processName: String, amqpConfigPath: String, userConfigPath: String, factory: (Session, AmqpServiceOperations) => Props): Props =
+ Props(classOf[ConnectedApplicationManager], processName, amqpConfigPath, userConfigPath, factory)
+}
+
+import ConnectedApplicationManager._
+
+class ConnectedApplicationManager(processName: String, amqpConfigPath: String, userConfigPath: String, factory: (Session, AmqpServiceOperations) => Props) extends Actor with FSM[State, Data] with Logging {
+
+ import context.dispatcher
+
+ private def connectQpid(): ServiceConnection = {
+ val config = AmqpSettings.load(amqpConfigPath)
+ val connection = ServiceConnection.connect(config, QpidBroker, 5000)
+ logger.info(s"Connected to broker: $config")
+ connection
+ }
+
+ private def userSettings(): UserSettings = {
+ UserSettings.load(userConfigPath)
+ }
+
+ override def supervisorStrategy: SupervisorStrategy = {
+ import SupervisorStrategy._
+ OneForOneStrategy(maxNrOfRetries = 1, withinTimeRange = 3.seconds) {
+ case _: SessionUnusableException =>
+ self ! RestartConnection("session error")
+ Stop
+ case _: UnauthorizedException =>
+ self ! RestartConnection("auth failure")
+ Stop
+ case ex: Throwable =>
+ self ! RestartConnection("unknown error: " + ex)
+ Stop
+ }
+ }
+
+ private def attemptLogin(conn: ServiceConnection) {
+ val userConfig = userSettings()
+ val future = conn.login(userConfig.user, userConfig.password)
+ future.onSuccess {
+ case session: Session => self ! LoginSuccess(session)
+ }
+ future.onFailure {
+ case ex: Throwable => self ! LoginFailure(ex)
+ }
+ }
+
+ startWith(Down, NoData)
+
+ when(Down) {
+
+ case Event(AttemptConnect, NoData) => {
+ try {
+ val conn = connectQpid()
+
+ conn.addConnectionListener { expected =>
+ self ! LostBrokerConnection(expected)
+ }
+
+ attemptLogin(conn)
+
+ goto(Connected) using ConnEstablished(conn)
+
+ } catch {
+ case ex: Throwable => {
+ logger.error(s"Couldn't initialize $processName: " + ex.getMessage)
+ scheduleMsg(3000, AttemptConnect)
+ stay using NoData
+ }
+ }
+ }
+
+ case Event(LostBrokerConnection(expected), NoData) => {
+ if (!expected) {
+ logger.warn(s"Saw unexpected lost connection event while $processName was already down")
+ }
+ stay using NoData
+ }
+ }
+
+ when(Connected) {
+
+ case Event(AttemptLogin, data @ ConnEstablished(conn)) => {
+ attemptLogin(conn)
+ stay using data
+ }
+
+ case Event(LoginSuccess(session), ConnEstablished(conn)) => {
+ logger.info(s"$processName logged into services")
+
+ val child = context.actorOf(factory(session, conn.serviceOperations))
+
+ goto(Running) using SessionEstablished(child, session, conn)
+ }
+
+ case Event(LoginFailure(ex), data @ ConnEstablished(conn)) => {
+ ex match {
+ case ex: ServiceException =>
+ logger.warn(s"$processName login failed: " + ex.getMessage)
+ scheduleMsg(3000, AttemptLogin)
+ stay using data
+ case ex: Throwable =>
+ logger.warn(s"$processName login error: " + ex.getMessage)
+ conn.disconnect()
+ scheduleMsg(3000, AttemptConnect)
+ goto(Down) using NoData
+ }
+ }
+
+ case Event(LostBrokerConnection(expected), ConnEstablished(conn)) => {
+ conn.disconnect()
+ onLostConnection
+ }
+ }
+
+ when(Running) {
+
+ case Event(RestartConnection(reason), SessionEstablished(child, _, conn)) =>
+ logger.warn(s"Restarting $processName due to $reason")
+ child ! PoisonPill
+ onUnrecoverable(conn)
+
+ case Event(LostBrokerConnection(expected), SessionEstablished(child, _, _)) =>
+ child ! PoisonPill
+ onLostConnection
+ }
+
+ def onUnrecoverable(conn: ServiceConnection) = {
+ conn.disconnect()
+ scheduleMsg(3000, AttemptConnect)
+ goto(Down) using NoData
+ }
+
+ private def onLostConnection = {
+ logger.warn(s"$processName lost connection")
+ scheduleMsg(3000, AttemptConnect)
+ goto(Down) using NoData
+ }
+
+ override def preStart() {
+ self ! AttemptConnect
+ }
+
+ onTermination {
+ case StopEvent(_, _, ConnEstablished(conn)) => conn.disconnect()
+ case StopEvent(_, _, SessionEstablished(_, _, conn)) => conn.disconnect()
+ }
+
+ private def scheduleMsg(timeMs: Long, msg: AnyRef) {
+ import context.dispatcher
+ context.system.scheduler.scheduleOnce(
+ Duration(timeMs, MILLISECONDS),
+ self,
+ msg)
+ }
+
+ initialize()
+}
\ No newline at end of file
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/EndpointCollectionManager.scala b/app-framework/src/main/scala/io/greenbus/app/actor/EndpointCollectionManager.scala
new file mode 100644
index 0000000..4f3cebc
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/EndpointCollectionManager.scala
@@ -0,0 +1,164 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor
+
+import akka.actor._
+import akka.actor.Actor
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.msg.{ SessionUnusableException, SubscriptionBinding, Subscription, Session }
+import io.greenbus.msg.amqp.AmqpServiceOperations
+import io.greenbus.client.service.proto.Model.{ EndpointNotification, Endpoint }
+import io.greenbus.client.exception.UnauthorizedException
+import io.greenbus.client.service.proto.Model.ModelUUID
+import io.greenbus.client.proto.Envelope.SubscriptionEventType
+import java.io.IOException
+
+class OutOfEndpointCollectionException(end: Endpoint, msg: String) extends Exception(msg) {
+ def endpoint: Endpoint = end
+}
+
+object EndpointCollectionManager {
+
+ case class EndpointsResolved(endpoints: Seq[Endpoint]) // Unlike added it just means it was there when we came up
+ case class EndpointAdded(endpoint: Endpoint)
+ case class EndpointModified(endpoint: Endpoint)
+ case class EndpointRemoved(endpoint: Endpoint)
+ case class EndpointOutOfCollection(endpoint: Endpoint) // Unlike removed, just means we're not responsible for it anymore
+
+ private case object LookupEndpoints
+ private case class EndpointsResult(poll: Seq[Endpoint], subscription: Subscription[EndpointNotification])
+ private case class RequestFailure(ex: Throwable)
+
+ def props(strategy: EndpointCollectionStrategy, session: Session, serviceOps: AmqpServiceOperations, endpointObserver: Option[ActorRef], factory: (Endpoint, CollectionMembership, Session, AmqpServiceOperations) => Props): Props =
+ Props(classOf[EndpointCollectionManager], strategy, session, serviceOps, endpointObserver, factory)
+
+}
+
+class EndpointCollectionManager(strategy: EndpointCollectionStrategy, session: Session, serviceOps: AmqpServiceOperations, endpointObserver: Option[ActorRef], factory: (Endpoint, CollectionMembership, Session, AmqpServiceOperations) => Props) extends Actor with Logging {
+ import EndpointCollectionManager._
+
+ private var binding = Option.empty[SubscriptionBinding]
+ private var streams = Map.empty[ModelUUID, ActorRef]
+
+ self ! LookupEndpoints
+
+ def receive = {
+
+ case LookupEndpoints => {
+
+ import context.dispatcher
+ val configFut = strategy.configuration(session)
+
+ configFut.onSuccess { case (poll, subscription) => self ! EndpointsResult(poll, subscription) }
+ configFut.onFailure { case ex => self ! RequestFailure(ex) }
+ }
+
+ case EndpointsResult(results, subscription) => {
+
+ binding = Some(subscription)
+ val whitelistOpt = strategy.nameWhitelist
+
+ whitelistOpt match {
+ case None => subscription.start { event => self ! event }
+ case Some(whitelist) =>
+ subscription.start { event =>
+ if (whitelist.contains(event.getValue.getName)) {
+ self ! event
+ }
+ }
+ }
+
+ endpointObserver.foreach(ref => ref ! EndpointsResolved(results))
+
+ val initial = results.filter(_.getDisabled == false).map { endpoint =>
+ (endpoint.getUuid, launchStream(endpoint))
+ }
+
+ streams = initial.toMap
+
+ logger.info("Endpoint collection management initialized")
+ }
+
+ case RequestFailure(ex) => throw ex
+
+ case event: EndpointNotification => {
+ val endpoint = event.getValue
+ val uuid = endpoint.getUuid
+ val name = endpoint.getName
+
+ endpointObserver.foreach { obsRef =>
+ val msg = event.getEventType match {
+ case SubscriptionEventType.ADDED => EndpointAdded(endpoint)
+ case SubscriptionEventType.MODIFIED => EndpointModified(endpoint)
+ case SubscriptionEventType.REMOVED => EndpointRemoved(endpoint)
+ }
+
+ obsRef ! msg
+ }
+
+ (event.getEventType, endpoint.getDisabled) match {
+ case (SubscriptionEventType.ADDED, false) =>
+ if (!streams.contains(uuid)) {
+ addStream(endpoint)
+ } else {
+ logger.warn(s"Saw add event on existing endpoint stream: $name (${uuid.getValue})")
+ }
+ case (SubscriptionEventType.ADDED, true) =>
+ case (SubscriptionEventType.MODIFIED, false) =>
+ if (!streams.contains(uuid)) {
+ addStream(endpoint)
+ }
+ case (SubscriptionEventType.MODIFIED, true) => removeStream(uuid)
+ case (SubscriptionEventType.REMOVED, _) => removeStream(uuid)
+ }
+ }
+
+ }
+
+ private def addStream(endpoint: Endpoint) {
+ streams = streams + ((endpoint.getUuid, launchStream(endpoint)))
+ }
+ private def removeStream(uuid: ModelUUID) {
+ logger.debug("Removing endpoint " + uuid.getValue)
+ streams.get(uuid).foreach(ref => ref ! PoisonPill)
+ streams -= uuid
+ }
+
+ private def launchStream(endpoint: Endpoint): ActorRef = {
+ logger.debug("Launching endpoint " + endpoint.getName)
+ context.actorOf(factory(endpoint, strategy.membership, session.spawn(), serviceOps))
+ }
+
+ override def supervisorStrategy: SupervisorStrategy = {
+ import SupervisorStrategy._
+ OneForOneStrategy() {
+ case out: OutOfEndpointCollectionException => {
+ logger.info("Endpoint marked itself as out of collection: " + out.endpoint.getName)
+ endpointObserver.foreach(_ ! EndpointOutOfCollection(out.endpoint))
+ removeStream(out.endpoint.getUuid)
+ Resume
+ }
+ case _: UnauthorizedException => Escalate
+ case _: SessionUnusableException => Escalate
+ case _: IOException => Escalate
+ case _: Throwable => Escalate
+ }
+ }
+}
+
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/EndpointCollectionStrategy.scala b/app-framework/src/main/scala/io/greenbus/app/actor/EndpointCollectionStrategy.scala
new file mode 100644
index 0000000..08a3cc7
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/EndpointCollectionStrategy.scala
@@ -0,0 +1,118 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor
+
+import io.greenbus.client.service.ModelService
+import io.greenbus.client.service.proto.Model.{ Endpoint, EndpointNotification }
+import io.greenbus.client.service.proto.ModelRequests.{ EntityKeySet, EndpointQuery, EndpointSubscriptionQuery, EntityPagingParams }
+import io.greenbus.msg.{ Session, Subscription }
+
+import scala.collection.JavaConversions._
+import scala.concurrent.{ ExecutionContext, Future }
+
+sealed trait CollectionMembership
+case object AnyAndAllMembership extends CollectionMembership
+case object NamedMembership extends CollectionMembership
+case class ProtocolMembership(validProtocols: Set[String]) extends CollectionMembership
+
+trait EndpointCollectionStrategy {
+ def membership: CollectionMembership
+
+ def nameWhitelist: Option[Set[String]]
+
+ def configuration(session: Session)(implicit context: ExecutionContext): Future[(Seq[Endpoint], Subscription[EndpointNotification])]
+}
+
+class AllEndpointsStrategy(whitelistOpt: Option[Set[String]] = None) extends EndpointCollectionStrategy {
+
+ def membership = AnyAndAllMembership
+
+ def nameWhitelist: Option[Set[String]] = whitelistOpt
+
+ def configuration(session: Session)(implicit context: ExecutionContext): Future[(Seq[Endpoint], Subscription[EndpointNotification])] = {
+ val modelClient = ModelService.client(session)
+
+ val subFut = modelClient.subscribeToEndpoints(EndpointSubscriptionQuery.newBuilder().build())
+
+ subFut.flatMap {
+ case (emptyCurrent, sub) =>
+ val currFut = whitelistOpt match {
+ case None =>
+ val query = EndpointQuery.newBuilder()
+ .setPagingParams(EntityPagingParams.newBuilder()
+ .setPageSize(Int.MaxValue)) // TODO: page it if we can't trust the strategy choice to be sane?
+ .build()
+
+ modelClient.endpointQuery(query)
+
+ case Some(whitelist) =>
+ modelClient.getEndpoints(EntityKeySet.newBuilder().addAllNames(whitelist).build())
+ }
+
+ currFut.onFailure { case ex => sub.cancel() }
+
+ currFut.map { ends =>
+ (ends, sub)
+ }
+ }
+ }
+}
+
+class ProtocolsEndpointStrategy(validProtocols: Set[String], whitelistOpt: Option[Set[String]] = None) extends EndpointCollectionStrategy {
+
+ def membership = ProtocolMembership(validProtocols)
+
+ def nameWhitelist: Option[Set[String]] = None
+
+ def configuration(session: Session)(implicit context: ExecutionContext): Future[(Seq[Endpoint], Subscription[EndpointNotification])] = {
+ val modelClient = ModelService.client(session)
+
+ val subQuery = EndpointSubscriptionQuery.newBuilder()
+ .addAllProtocols(validProtocols)
+ .build()
+
+ val subFut = modelClient.subscribeToEndpoints(subQuery)
+
+ subFut.flatMap {
+ case (emptyCurrent, sub) =>
+
+ val currFut = whitelistOpt match {
+ case None =>
+ val query = EndpointQuery.newBuilder()
+ .addAllProtocols(validProtocols)
+ .setPagingParams(EntityPagingParams.newBuilder()
+ .setPageSize(Int.MaxValue)) // TODO: page it if we can't trust the strategy choice to be sane?
+ .build()
+
+ modelClient.endpointQuery(query)
+
+ case Some(whitelist) =>
+ modelClient.getEndpoints(EntityKeySet.newBuilder().addAllNames(whitelist).build()).map { results =>
+ results.filter(r => validProtocols.contains(r.getProtocol))
+ }
+ }
+
+ currFut.onFailure { case ex => sub.cancel() }
+
+ currFut.map { ends =>
+ (ends, sub)
+ }
+ }
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/EndpointMonitor.scala b/app-framework/src/main/scala/io/greenbus/app/actor/EndpointMonitor.scala
new file mode 100644
index 0000000..d6209ed
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/EndpointMonitor.scala
@@ -0,0 +1,127 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor
+
+import akka.actor._
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.msg.amqp.AmqpServiceOperations
+import io.greenbus.msg.{ Session, SessionUnusableException, Subscription, SubscriptionBinding }
+import io.greenbus.client.exception.UnauthorizedException
+import io.greenbus.client.service.ModelService
+import io.greenbus.client.service.proto.Model._
+import io.greenbus.client.service.proto.ModelRequests.EndpointSubscriptionQuery
+
+import scala.concurrent.{ ExecutionContext, Future }
+
+object EndpointMonitor {
+
+ class EndpointConfigException(msg: String) extends Exception(msg)
+
+ def queryEndpoint(session: Session, endpoint: Endpoint)(implicit context: ExecutionContext): Future[(Seq[Endpoint], Subscription[EndpointNotification])] = {
+ val modelClient = ModelService.client(session)
+
+ val endSubQuery = EndpointSubscriptionQuery.newBuilder().addUuids(endpoint.getUuid).build()
+
+ modelClient.subscribeToEndpoints(endSubQuery)
+ }
+
+ case object MakeRequests
+ case class EndpointConfig(endpoints: Seq[Endpoint], endpointSub: Subscription[EndpointNotification])
+ case class RequestFailure(ex: Throwable)
+
+ def props(endpoint: Endpoint, membership: CollectionMembership, session: Session, serviceOps: AmqpServiceOperations, factory: (Endpoint, Session, AmqpServiceOperations) => Props): Props =
+ Props(classOf[EndpointMonitor], endpoint, membership, session, serviceOps, factory)
+
+}
+
+class EndpointMonitor(endpoint: Endpoint, membership: CollectionMembership, session: Session, serviceOps: AmqpServiceOperations, factory: (Endpoint, Session, AmqpServiceOperations) => Props) extends Actor with Logging {
+ import io.greenbus.app.actor.EndpointMonitor._
+
+ private var endpointBinding = Option.empty[SubscriptionBinding]
+
+ private var child = Option.empty[ActorRef]
+
+ self ! MakeRequests
+
+ def receive = {
+
+ case MakeRequests => {
+ import context.dispatcher
+
+ val dataFut = queryEndpoint(session, endpoint)
+ dataFut.onSuccess { case config => self ! EndpointConfig(config._1, config._2) }
+ dataFut.onFailure { case ex => self ! RequestFailure(ex) }
+ }
+
+ case config: EndpointConfig => {
+ import config._
+
+ endpointBinding = Some(endpointSub)
+
+ endpointSub.start { self ! _ }
+
+ logger.info(s"Endpoint management initialized for ${endpoint.getName}")
+
+ child = Some(context.actorOf(factory(endpoint, session, serviceOps)))
+ }
+
+ case endEvent: EndpointNotification => {
+ import io.greenbus.client.proto.Envelope.SubscriptionEventType._
+
+ // We need to help the collection manager know if our endpoint has fallen outside our set
+ // If the collection manager is subscribing by name/protocol, a change to those parameters may not be
+ // reflected in its subscription
+ endEvent.getEventType match {
+ case ADDED => logger.warn(s"Saw added in endpoint manager for ${endpoint.getName}; endpoint already existed")
+ case REMOVED => // not our responsibility (collection manager will handle)
+ case MODIFIED =>
+ val changed = endEvent.getValue
+ membership match {
+ case AnyAndAllMembership =>
+ case NamedMembership =>
+ if (endpoint.getName != changed.getName) {
+ throw new OutOfEndpointCollectionException(endpoint, "name changed")
+ }
+ case ProtocolMembership(valid) =>
+ if (!valid.contains(changed.getProtocol)) {
+ throw new OutOfEndpointCollectionException(endpoint, "protocol changed")
+ }
+ }
+ }
+ }
+
+ case RequestFailure(ex) =>
+ logger.warn(s"Error making requests for endpoint manager for ${endpoint.getName}: ${ex.getMessage}")
+ throw ex
+ }
+
+ override def postStop() {
+ endpointBinding.foreach(_.cancel())
+ }
+
+ override def supervisorStrategy: SupervisorStrategy = {
+ import akka.actor.SupervisorStrategy._
+ OneForOneStrategy() {
+ case _: UnauthorizedException => Escalate
+ case _: SessionUnusableException => Escalate
+ case _: Throwable => Escalate
+ }
+ }
+
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/MessageScheduling.scala b/app-framework/src/main/scala/io/greenbus/app/actor/MessageScheduling.scala
new file mode 100644
index 0000000..eb466ab
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/MessageScheduling.scala
@@ -0,0 +1,33 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor
+
+import akka.actor.Actor
+import scala.concurrent.duration._
+
+trait MessageScheduling extends Actor {
+
+ protected def scheduleMsg(timeMs: Long, msg: AnyRef) {
+ import context.dispatcher
+ context.system.scheduler.scheduleOnce(
+ Duration(timeMs, MILLISECONDS),
+ self,
+ msg)
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/SessionLoginManager.scala b/app-framework/src/main/scala/io/greenbus/app/actor/SessionLoginManager.scala
new file mode 100644
index 0000000..5dfb908
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/SessionLoginManager.scala
@@ -0,0 +1,105 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor
+
+import akka.actor._
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.client.ServiceConnection
+import io.greenbus.client.exception.ServiceException
+import io.greenbus.msg.Session
+import io.greenbus.msg.amqp.AmqpServiceOperations
+import io.greenbus.util.UserSettings
+
+import scala.concurrent.duration._
+
+object SessionLoginManager {
+
+ sealed trait State
+ case object Unestablished extends State
+ case class Running(child: ActorRef)
+
+ case object AttemptLogin
+ case class LoginSuccess(session: Session)
+ case class LoginFailure(ex: Throwable)
+
+ def props(processName: String,
+ userConfigPath: String,
+ connection: ServiceConnection,
+ loginRetryMs: Long,
+ factory: (Session, AmqpServiceOperations) => Props): Props = {
+ Props(classOf[SessionLoginManager], processName, userConfigPath, connection, loginRetryMs, factory)
+ }
+}
+
+class SessionLoginManager(
+ processName: String,
+ userConfigPath: String,
+ connection: ServiceConnection,
+ loginRetryMs: Long,
+ factory: (Session, AmqpServiceOperations) => Props)
+ extends Actor with MessageScheduling with Logging {
+ import SessionLoginManager._
+ import context.dispatcher
+
+ private var childOpt = Option.empty[ActorRef]
+
+ self ! AttemptLogin
+
+ def receive = {
+ case AttemptLogin => {
+ attemptLogin(connection)
+ }
+ case LoginSuccess(session) => {
+ logger.info(s"$processName logged into services")
+
+ val child = context.actorOf(factory(session, connection.serviceOperations))
+ childOpt = Some(child)
+ }
+ case LoginFailure(ex) => {
+ ex match {
+ case ex: ServiceException =>
+ logger.warn(s"$processName login failed: " + ex.getMessage)
+ scheduleMsg(loginRetryMs, AttemptLogin)
+ }
+ }
+ }
+
+ private def attemptLogin(conn: ServiceConnection) {
+ val userConfig = userSettings()
+ val future = conn.login(userConfig.user, userConfig.password)
+ future.onSuccess {
+ case session: Session => self ! LoginSuccess(session)
+ }
+ future.onFailure {
+ case ex: Throwable => self ! LoginFailure(ex)
+ }
+ }
+
+ private def userSettings(): UserSettings = {
+ UserSettings.load(userConfigPath)
+ }
+
+ override def supervisorStrategy: SupervisorStrategy = {
+ import SupervisorStrategy._
+ OneForOneStrategy(maxNrOfRetries = 1, withinTimeRange = 3.seconds) {
+ case ex: Throwable =>
+ Escalate
+ }
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/frontend/ActorRefCommandAcceptor.scala b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/ActorRefCommandAcceptor.scala
new file mode 100644
index 0000000..8775b9f
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/ActorRefCommandAcceptor.scala
@@ -0,0 +1,52 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.frontend
+
+import akka.actor._
+import akka.pattern.{ AskTimeoutException, ask }
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.app.actor.frontend.ActorProtocolManager.CommandIssued
+import io.greenbus.client.service.proto.Commands.{ CommandStatus, CommandResult, CommandRequest }
+import io.greenbus.client.service.proto.Model.Endpoint
+
+import scala.concurrent.Future
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.duration._
+
+class ActorRefCommandAcceptor(ref: ActorRef, endpoint: Endpoint) extends ProtocolCommandAcceptor with Logging {
+
+ def issue(commandName: String, request: CommandRequest): Future[CommandResult] = {
+ val askFut = ref.ask(CommandIssued(commandName, request))(4000.milliseconds)
+
+ askFut.flatMap {
+ case cmdFut: Future[_] => cmdFut.asInstanceOf[Future[CommandResult]]
+ case all =>
+ logger.error("Endpoint " + endpoint.getName + " received unknown response from protocol master actor: " + all)
+ Future.successful(CommandResult.newBuilder().setStatus(CommandStatus.UNDEFINED).build())
+ }.recover {
+ case at: AskTimeoutException =>
+ logger.warn("Endpoint " + endpoint.getName + " could not issue command to protocol master actor")
+ CommandResult.newBuilder().setStatus(CommandStatus.TIMEOUT).build()
+
+ case ex =>
+ logger.warn("Endpoint " + endpoint.getName + " had error communicating with protocol master actor: " + ex)
+ CommandResult.newBuilder().setStatus(CommandStatus.UNDEFINED).build()
+ }
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/frontend/BusDrivenProtocolEndpoint.scala b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/BusDrivenProtocolEndpoint.scala
new file mode 100644
index 0000000..f29500e
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/BusDrivenProtocolEndpoint.scala
@@ -0,0 +1,193 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.frontend
+
+import akka.actor.{ PoisonPill, ActorRef, Props }
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.msg.{ Session, SessionUnusableException }
+import io.greenbus.app.actor.MessageScheduling
+import io.greenbus.app.actor.frontend.BusDrivenProtocolEndpoint.{ ProtocolFactory, ProxyFactory, RegisterFunc }
+import io.greenbus.app.actor.frontend.ProcessingProxy.LinkLapsed
+import io.greenbus.app.actor.util.NestedStateMachine
+import io.greenbus.client.exception.{ LockedException, UnauthorizedException }
+import io.greenbus.client.service.FrontEndService
+import io.greenbus.client.service.proto.FrontEnd.FrontEndRegistration
+import io.greenbus.client.service.proto.FrontEndRequests.FrontEndRegistrationTemplate
+import io.greenbus.client.service.proto.Model.Endpoint
+
+import scala.concurrent.{ ExecutionContext, Future }
+
+object BusDrivenProtocolEndpoint {
+
+ type RegisterFunc = (Session, Endpoint, Boolean) => Future[FrontEndRegistration]
+
+ type ProxyFactory = (ActorRef, Endpoint) => Props
+ type ProtocolFactory = (Endpoint, Session, MeasurementsPublished => Unit, StackStatusUpdated => Unit) => Props
+
+ protected sealed trait State
+ protected case object BusUp extends State
+ protected case object RegistrationPending extends State
+ protected case class Running(proxy: ActorRef, protocol: ActorRef, inputAddress: String) extends State
+ protected case class RunningUnregistered(proxy: ActorRef, protocol: ActorRef) extends State
+ protected case class RunningRegistrationPending(proxy: ActorRef, protocol: ActorRef) extends State
+
+ private case class RegistrationSuccess(conns: FrontEndRegistration)
+ private case class RegistrationFailure(ex: Throwable)
+
+ private case object DoRegistrationRetry
+
+ def register(session: Session, endpoint: Endpoint, holdingLock: Boolean, lockingEnabled: Boolean, nodeId: String)(implicit context: ExecutionContext): Future[FrontEndRegistration] = {
+
+ val client = FrontEndService.client(session)
+
+ val registration = FrontEndRegistrationTemplate.newBuilder()
+ .setEndpointUuid(endpoint.getUuid)
+ .setHoldingLock(holdingLock && lockingEnabled)
+ .setFepNodeId(nodeId)
+ .build()
+
+ client.putFrontEndRegistration(registration, endpoint.getUuid.getValue)
+ }
+
+ def props(
+ endpoint: Endpoint,
+ session: Session,
+ regFunc: RegisterFunc,
+ proxyFactory: ProxyFactory,
+ protocolFactory: ProtocolFactory,
+ registrationRetryMs: Long): Props = {
+ Props(classOf[BusDrivenProtocolEndpoint], endpoint, session, regFunc, proxyFactory, protocolFactory, registrationRetryMs)
+ }
+}
+
+class BusDrivenProtocolEndpoint(
+ endpoint: Endpoint,
+ session: Session,
+ regFunc: RegisterFunc,
+ proxyFactory: ProxyFactory,
+ protocolFactory: ProtocolFactory,
+ registrationRetryMs: Long)
+ extends NestedStateMachine with MessageScheduling with Logging {
+
+ import io.greenbus.app.actor.frontend.BusDrivenProtocolEndpoint._
+
+ protected type StateType = State
+ protected def start: StateType = RegistrationPending
+ override protected def instanceId: Option[String] = Some(endpoint.getName)
+
+ doRegistration()
+
+ protected def machine = {
+ case BusUp => {
+ case DoRegistrationRetry => {
+ doRegistration()
+ RegistrationPending
+ }
+ }
+
+ case RegistrationPending => {
+
+ case RegistrationSuccess(result) => {
+
+ val proxy = context.actorOf(proxyFactory(self, endpoint))
+ proxy ! ProcessingProxy.LinkUp(session, result.getInputAddress)
+
+ val protocol = context.actorOf(protocolFactory(endpoint, session, proxy ! _, proxy ! _))
+
+ Running(proxy, protocol, result.getInputAddress)
+ }
+
+ case RegistrationFailure(ex) => {
+ ex match {
+ case ex: SessionUnusableException => throw ex
+ case ex: UnauthorizedException => throw ex
+ /*case ex: LockedException =>
+ logger.warn(s"Endpoint ${endpoint.getName} registration failed")
+ scheduleMsg(registrationRetryMs, DoRegistrationRetry)
+ BusUp*/
+ case ex: Throwable =>
+ logger.warn(s"Endpoint ${endpoint.getName} registration failed")
+ scheduleMsg(registrationRetryMs, DoRegistrationRetry)
+ BusUp
+ }
+ }
+ }
+
+ case state: Running => {
+
+ case ServiceSessionDead => {
+ throw new SessionUnusableException("Proxy indicated session was dead")
+ }
+
+ case LinkLapsed => {
+ doRegistration()
+ RunningRegistrationPending(state.proxy, state.protocol)
+ }
+ }
+
+ case state: RunningUnregistered => {
+
+ case ServiceSessionDead => {
+ throw new SessionUnusableException("Proxy indicated session was dead")
+ }
+
+ case DoRegistrationRetry => {
+ doRegistration()
+ RunningRegistrationPending(state.proxy, state.protocol)
+ }
+ }
+
+ case state: RunningRegistrationPending => {
+
+ case ServiceSessionDead => {
+ throw new SessionUnusableException("Proxy indicated session was dead")
+ }
+
+ case RegistrationSuccess(result) => {
+ state.proxy ! ProcessingProxy.LinkUp(session, result.getInputAddress)
+ Running(state.proxy, state.protocol, result.getInputAddress)
+ }
+
+ case RegistrationFailure(ex) => {
+ ex match {
+ case ex: SessionUnusableException => throw ex
+ case ex: UnauthorizedException => throw ex
+ case ex: LockedException =>
+ logger.warn(s"Endpoint ${endpoint.getName} registration returned locked, shutting down protocol")
+ state.protocol ! PoisonPill
+ state.proxy ! PoisonPill
+ scheduleMsg(registrationRetryMs, DoRegistrationRetry)
+ BusUp
+ case ex: Throwable =>
+ logger.warn(s"Endpoint ${endpoint.getName} registration failed")
+ scheduleMsg(registrationRetryMs, DoRegistrationRetry)
+ RunningUnregistered(state.proxy, state.protocol)
+ }
+ }
+ }
+ }
+
+ private def doRegistration(): Unit = {
+ import context.dispatcher
+ val fut = regFunc(session, endpoint, true)
+
+ fut.onSuccess { case result => self ! RegistrationSuccess(result) }
+ fut.onFailure { case ex => self ! RegistrationFailure(ex) }
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/frontend/BusDrivenWithCommandsProtocolEndpoint.scala b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/BusDrivenWithCommandsProtocolEndpoint.scala
new file mode 100644
index 0000000..a9326d0
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/BusDrivenWithCommandsProtocolEndpoint.scala
@@ -0,0 +1,216 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.frontend
+
+import akka.actor.{ ActorRef, PoisonPill, Props }
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.msg.amqp.AmqpServiceOperations
+import io.greenbus.msg.service.ServiceHandlerSubscription
+import io.greenbus.msg.{ Session, SessionUnusableException }
+import io.greenbus.app.actor.MessageScheduling
+import io.greenbus.app.actor.frontend.CommandProxy.CommandSubscriptionRetrieved
+import io.greenbus.app.actor.frontend.ProcessingProxy.LinkLapsed
+import io.greenbus.app.actor.frontend.SimpleCommandProxy.ProtocolInitialized
+import io.greenbus.app.actor.util.NestedStateMachine
+import io.greenbus.client.exception.{ LockedException, UnauthorizedException }
+import io.greenbus.client.service.FrontEndService
+import io.greenbus.client.service.proto.FrontEnd.FrontEndRegistration
+import io.greenbus.client.service.proto.FrontEndRequests.FrontEndRegistrationTemplate
+import io.greenbus.client.service.proto.Model.Endpoint
+
+import scala.concurrent.{ ExecutionContext, Future }
+
+object BusDrivenWithCommandsProtocolEndpoint {
+
+ type RegisterFunc = (Session, AmqpServiceOperations, Endpoint, Boolean) => (Future[FrontEndRegistration], ServiceHandlerSubscription)
+
+ type ProxyFactory = (ActorRef, Endpoint) => Props
+ type ProtocolFactory = (Endpoint, Session, MeasurementsPublished => Unit, StackStatusUpdated => Unit) => Props
+
+ protected sealed trait State
+ protected case object BusUp extends State
+ protected case class RegistrationPending(cmdSub: ServiceHandlerSubscription) extends State
+ protected case class Running(proxy: ActorRef, commandActor: ActorRef, protocol: ActorRef, inputAddress: String) extends State
+ protected case class RunningUnregistered(proxy: ActorRef, commandActor: ActorRef, protocol: ActorRef) extends State
+ protected case class RunningRegistrationPending(proxy: ActorRef, commandActor: ActorRef, cmdSub: ServiceHandlerSubscription, protocol: ActorRef) extends State
+
+ private case class RegistrationSuccess(conns: FrontEndRegistration)
+ private case class RegistrationFailure(ex: Throwable)
+
+ private case object DoRegistrationRetry
+
+ def register(session: Session, serviceOps: AmqpServiceOperations, endpoint: Endpoint, holdingLock: Boolean, lockingEnabled: Boolean, nodeId: String)(implicit context: ExecutionContext): (Future[FrontEndRegistration], ServiceHandlerSubscription) = {
+ val serviceHandlerSub = serviceOps.routedServiceBinding()
+ val cmdAddress = serviceHandlerSub.getId()
+
+ try {
+ val client = FrontEndService.client(session)
+
+ val registration = FrontEndRegistrationTemplate.newBuilder()
+ .setEndpointUuid(endpoint.getUuid)
+ .setCommandAddress(cmdAddress)
+ .setHoldingLock(holdingLock && lockingEnabled)
+ .setFepNodeId(nodeId)
+ .build()
+
+ val regFut = client.putFrontEndRegistration(registration, endpoint.getUuid.getValue)
+
+ (regFut, serviceHandlerSub)
+
+ } catch {
+ case ex: Throwable =>
+ serviceHandlerSub.cancel()
+ throw ex
+ }
+ }
+
+ def props(
+ endpoint: Endpoint,
+ session: Session,
+ serviceOps: AmqpServiceOperations,
+ regFunc: RegisterFunc,
+ proxyFactory: ProxyFactory,
+ commandActorFactory: Endpoint => Props,
+ protocolFactory: ProtocolFactory,
+ registrationRetryMs: Long): Props = {
+ Props(classOf[BusDrivenWithCommandsProtocolEndpoint], endpoint, session, serviceOps, regFunc, proxyFactory, commandActorFactory, protocolFactory, registrationRetryMs)
+ }
+}
+
+import io.greenbus.app.actor.frontend.BusDrivenWithCommandsProtocolEndpoint._
+class BusDrivenWithCommandsProtocolEndpoint(
+ endpoint: Endpoint,
+ session: Session,
+ serviceOps: AmqpServiceOperations,
+ regFunc: RegisterFunc,
+ proxyFactory: ProxyFactory,
+ commandActorFactory: Endpoint => Props,
+ protocolFactory: ProtocolFactory,
+ registrationRetryMs: Long)
+ extends NestedStateMachine with MessageScheduling with Logging {
+
+ protected type StateType = State
+ protected def start: StateType = RegistrationPending(doRegistration())
+ override protected def instanceId: Option[String] = Some(endpoint.getName)
+
+ protected def machine = {
+ case BusUp => {
+ case DoRegistrationRetry => {
+ RegistrationPending(doRegistration())
+ }
+ }
+
+ case RegistrationPending(cmdSub) => {
+
+ case RegistrationSuccess(result) => {
+
+ val proxy = context.actorOf(proxyFactory(self, endpoint))
+ proxy ! ProcessingProxy.LinkUp(session, result.getInputAddress)
+
+ val protocol = context.actorOf(protocolFactory(endpoint, session, proxy ! _, proxy ! _))
+
+ val commandProxy = context.actorOf(commandActorFactory(endpoint))
+
+ commandProxy ! ProtocolInitialized(protocol)
+ commandProxy ! CommandSubscriptionRetrieved(cmdSub)
+
+ Running(proxy, commandProxy, protocol, result.getInputAddress)
+ }
+
+ case RegistrationFailure(ex) => {
+ ex match {
+ case ex: SessionUnusableException => throw ex
+ case ex: UnauthorizedException => throw ex
+ case ex: Throwable =>
+ logger.warn(s"Endpoint ${endpoint.getName} registration failed")
+ logger.debug(s"Endpoint ${endpoint.getName} registration failed: " + ex.getMessage)
+ scheduleMsg(registrationRetryMs, DoRegistrationRetry)
+ BusUp
+ }
+ }
+ }
+
+ case state: Running => {
+
+ case ServiceSessionDead => {
+ throw new SessionUnusableException("Proxy indicated session was dead")
+ }
+
+ case LinkLapsed => {
+ val cmdSub = doRegistration()
+ RunningRegistrationPending(state.proxy, state.commandActor, cmdSub, state.protocol)
+ }
+ }
+
+ case state: RunningUnregistered => {
+
+ case ServiceSessionDead => {
+ throw new SessionUnusableException("Proxy indicated session was dead")
+ }
+
+ case DoRegistrationRetry => {
+ val cmdSub = doRegistration()
+ RunningRegistrationPending(state.proxy, state.commandActor, cmdSub, state.protocol)
+ }
+ }
+
+ case state: RunningRegistrationPending => {
+
+ case ServiceSessionDead => {
+ state.cmdSub.cancel()
+ throw new SessionUnusableException("Proxy indicated session was dead")
+ }
+
+ case RegistrationSuccess(result) => {
+ state.proxy ! ProcessingProxy.LinkUp(session, result.getInputAddress)
+ state.commandActor ! CommandSubscriptionRetrieved(state.cmdSub)
+ Running(state.proxy, state.commandActor, state.protocol, result.getInputAddress)
+ }
+
+ case RegistrationFailure(ex) => {
+ state.cmdSub.cancel()
+ ex match {
+ case ex: SessionUnusableException => throw ex
+ case ex: UnauthorizedException => throw ex
+ case ex: LockedException =>
+ logger.warn(s"Endpoint ${endpoint.getName} registration returned locked, shutting down protocol")
+ state.protocol ! PoisonPill
+ state.proxy ! PoisonPill
+ state.commandActor ! PoisonPill
+ scheduleMsg(registrationRetryMs, DoRegistrationRetry)
+ BusUp
+ case ex: Throwable =>
+ logger.warn(s"Endpoint ${endpoint.getName} registration failed")
+ scheduleMsg(registrationRetryMs, DoRegistrationRetry)
+ RunningUnregistered(state.proxy, state.commandActor, state.protocol)
+ }
+ }
+ }
+ }
+
+ private def doRegistration(): ServiceHandlerSubscription = {
+ import context.dispatcher
+ val (fut, cmdSub) = regFunc(session, serviceOps, endpoint, true)
+
+ fut.onSuccess { case result => self ! RegistrationSuccess(result) }
+ fut.onFailure { case ex => self ! RegistrationFailure(ex) }
+
+ cmdSub
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/frontend/CommandProxy.scala b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/CommandProxy.scala
new file mode 100644
index 0000000..eb36844
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/CommandProxy.scala
@@ -0,0 +1,95 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.frontend
+
+import akka.actor.{ Props, Actor }
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.msg.SubscriptionBinding
+import io.greenbus.msg.service.ServiceHandlerSubscription
+import io.greenbus.client.service.proto.Commands.{ CommandStatus, CommandResult, CommandRequest }
+import io.greenbus.client.service.proto.Model.{ Endpoint, ModelUUID, Command }
+
+import scala.concurrent.Future
+
+object CommandProxy {
+
+ case class CommandsUpdated(commands: Seq[Command])
+
+ case class CommandSubscriptionRetrieved(commandSub: ServiceHandlerSubscription)
+
+ case class ProtocolInitialized(acceptor: ProtocolCommandAcceptor)
+ case object ProtocolUninitialized
+
+ private case class CommandIssued(req: CommandRequest, response: CommandResult => Unit)
+
+ def props(endpoint: Endpoint): Props = Props(classOf[CommandProxy], endpoint)
+}
+
+class CommandProxy(endpoint: Endpoint) extends Actor with Logging {
+ import CommandProxy._
+ import context.dispatcher
+
+ private var commandSub = Option.empty[SubscriptionBinding]
+ private var commandAcceptor = Option.empty[ProtocolCommandAcceptor]
+ private var commandMap = Map.empty[ModelUUID, String]
+
+ def receive = {
+ case CommandIssued(req, responseHandler) => {
+ commandAcceptor match {
+ case None =>
+ logger.debug(s"Endpoint ${endpoint.getName} saw command request while protocol uninitialized: " + req.getCommandUuid.getValue)
+ responseHandler(CommandResult.newBuilder().setStatus(CommandStatus.TIMEOUT).build())
+ case Some(acceptor) =>
+ commandMap.get(req.getCommandUuid) match {
+ case None => logger.warn(s"Saw command request with missing command for ${endpoint.getName}: " + req.getCommandUuid.getValue)
+ case Some(name) =>
+ logger.debug(s"Endpoint ${endpoint.getName} handling command request: " + name)
+ val fut = try {
+ acceptor.issue(name, req)
+ } catch {
+ case ex: Throwable =>
+ logger.warn(s"Immediate command failure for endpoint ${endpoint.getName}: " + ex)
+ Future.failed(ex)
+ }
+ fut.onSuccess { case result => responseHandler(result) }
+ fut.onFailure {
+ case ex: Throwable =>
+ logger.error(s"Command acceptor for endpoint ${endpoint.getName} failed: " + ex)
+ responseHandler(CommandResult.newBuilder().setStatus(CommandStatus.UNDEFINED).build())
+ }
+ }
+ }
+ }
+
+ case CommandsUpdated(updated) => {
+ commandMap = updated.map(c => (c.getUuid, c.getName)).toMap
+ }
+
+ case CommandSubscriptionRetrieved(sub) => {
+ commandSub.foreach(_.cancel())
+ commandSub = Some(sub)
+ val cmdDelegate = new DelegatingCommandHandler((req, resultAccept) => self ! CommandIssued(req, resultAccept))
+ sub.start(cmdDelegate)
+ }
+
+ case ProtocolInitialized(acceptor) => commandAcceptor = Some(acceptor)
+
+ case ProtocolUninitialized => commandAcceptor = None
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/frontend/DelegatingCommandHandler.scala b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/DelegatingCommandHandler.scala
new file mode 100644
index 0000000..6314129
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/DelegatingCommandHandler.scala
@@ -0,0 +1,83 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.frontend
+
+import io.greenbus.client.service.proto.Commands.{ CommandResult, CommandRequest }
+import io.greenbus.msg.service.ServiceHandler
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.client.proto.Envelope.{ ServiceResponse, ServiceRequest }
+import io.greenbus.client.exception.{ ServiceException, BadRequestException }
+import io.greenbus.client.service.proto.CommandRequests.{ PostCommandRequestResponse, PostCommandRequestRequest }
+import io.greenbus.client.proto.Envelope
+import com.google.protobuf.InvalidProtocolBufferException
+
+class DelegatingCommandHandler(handler: (CommandRequest, CommandResult => Unit) => Unit) extends ServiceHandler with Logging {
+
+ def handleMessage(msg: Array[Byte], responseHandler: (Array[Byte]) => Unit) {
+
+ try {
+ val requestEnvelope = ServiceRequest.parseFrom(msg)
+
+ if (!requestEnvelope.hasPayload) {
+ throw new BadRequestException("Must include request payload")
+ }
+ val payload = requestEnvelope.getPayload.toByteArray
+
+ val request = PostCommandRequestRequest.parseFrom(payload)
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include command request")
+ }
+
+ val cmdReq = request.getRequest
+
+ if (!cmdReq.hasCommandUuid) {
+ throw new BadRequestException("Must include command id")
+ }
+
+ def handleResult(result: CommandResult) {
+ val response = PostCommandRequestResponse.newBuilder().setResult(result).build()
+
+ val respEnvelope = ServiceResponse.newBuilder()
+ .setStatus(Envelope.Status.OK)
+ .setPayload(response.toByteString)
+ .build()
+
+ responseHandler(respEnvelope.toByteArray)
+ }
+
+ handler(cmdReq, handleResult)
+
+ } catch {
+ case ex: InvalidProtocolBufferException =>
+ responseHandler(buildErrorEnvelope(Envelope.Status.BAD_REQUEST, "Could not parse request").toByteArray)
+ case rse: ServiceException =>
+ responseHandler(buildErrorEnvelope(rse.getStatus, rse.getMessage).toByteArray)
+ case ex: Throwable =>
+ logger.error("Error handling command request: " + ex)
+ responseHandler(buildErrorEnvelope(Envelope.Status.INTERNAL_ERROR, "Error handling command request").toByteArray)
+ }
+ }
+
+ private def buildErrorEnvelope(status: Envelope.Status, message: String): ServiceResponse = {
+ ServiceResponse.newBuilder()
+ .setStatus(status)
+ .setErrorMessage(message)
+ .build()
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FepConfigLoader.scala b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FepConfigLoader.scala
new file mode 100644
index 0000000..01ebd3e
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FepConfigLoader.scala
@@ -0,0 +1,96 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.frontend
+
+import java.io.FileInputStream
+import java.util.UUID
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.app.actor.frontend.json.{ JsonFrontendConfiguration, JsonFrontendRegistrationConfig }
+import io.greenbus.app.actor.json.JsonAmqpConfig
+import io.greenbus.app.actor.{ AmqpConnectionConfig, EndpointCollectionStrategy, ProtocolsEndpointStrategy }
+import org.apache.commons.io.IOUtils
+
+object FepConfigLoader extends Logging {
+
+ def loadConfig(jsonPath: String, defaultAmqpConfigPath: String, defaultUserConfigPath: String, protocols: Set[String]): FrontendProcessConfig = {
+
+ loadJsonConfig(jsonPath) match {
+ case Some(jsonConfig) => {
+
+ val amqpConfig = jsonConfig.amqpConfig
+ .map(JsonAmqpConfig.read(_, Seq(defaultAmqpConfigPath)))
+ .getOrElse(AmqpConnectionConfig.default(defaultAmqpConfigPath))
+
+ val userConfigPath = jsonConfig.userConfigPath.getOrElse(defaultUserConfigPath)
+
+ val nodeId = jsonConfig.nodeId.getOrElse(UUID.randomUUID().toString)
+
+ val collStrat = jsonConfig.endpointWhitelist match {
+ case None => new ProtocolsEndpointStrategy(protocols)
+ case Some(list) => new ProtocolsEndpointStrategy(protocols, Some(list.toSet))
+ }
+
+ val regConfig = jsonConfig.registrationConfig.map(JsonFrontendRegistrationConfig.read).getOrElse(FrontendRegistrationConfig.defaults)
+
+ FrontendProcessConfig(amqpConfig, userConfigPath, collStrat, nodeId, regConfig)
+
+ }
+ case None => {
+
+ FrontendProcessConfig(
+ AmqpConnectionConfig.default(defaultAmqpConfigPath),
+ defaultUserConfigPath,
+ new ProtocolsEndpointStrategy(protocols),
+ UUID.randomUUID().toString,
+ FrontendRegistrationConfig.defaults)
+ }
+ }
+
+ }
+
+ def loadJsonConfig(path: String): Option[JsonFrontendConfiguration] = {
+
+ try {
+ val bytes = IOUtils.toByteArray(new FileInputStream(path))
+ JsonFrontendConfiguration.load(bytes)
+ } catch {
+ case ex: Throwable =>
+ logger.error("Could not load json configuration: " + path + ", reason: " + ex.getMessage)
+ None
+ }
+
+ }
+
+ def loadEndpointCollectionStrategy(protocols: Set[String], baseDir: String, property: String): EndpointCollectionStrategy = {
+
+ val endpointCollectionPathOpt = Option(System.getProperty(property))
+
+ endpointCollectionPathOpt match {
+ case None => new ProtocolsEndpointStrategy(protocols)
+ case Some(endpointCollectionPath) =>
+ val bytes = IOUtils.toByteArray(new FileInputStream(endpointCollectionPath))
+ val jsonConfig = JsonFrontendConfiguration.load(bytes).getOrElse {
+ throw new IllegalArgumentException(s"Could not parse endpoint collection configuration file $endpointCollectionPath")
+ }
+
+ new ProtocolsEndpointStrategy(protocols, jsonConfig.endpointWhitelist.map(_.toSet))
+ }
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FepEndpointCollectionManager.scala b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FepEndpointCollectionManager.scala
new file mode 100644
index 0000000..c83738a
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FepEndpointCollectionManager.scala
@@ -0,0 +1,149 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.frontend
+
+import java.io.IOException
+
+import akka.actor._
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.app.actor.{ CollectionMembership, EndpointCollectionStrategy, OutOfEndpointCollectionException }
+import io.greenbus.client.exception.UnauthorizedException
+import io.greenbus.client.proto.Envelope.SubscriptionEventType
+import io.greenbus.client.service.proto.Model.{ Endpoint, EndpointNotification, ModelUUID }
+import io.greenbus.msg.amqp.AmqpServiceOperations
+import io.greenbus.msg.{ Session, SessionUnusableException, Subscription, SubscriptionBinding }
+
+object FepEndpointCollectionManager {
+
+ case class EndpointsResolved(session: Session, serviceOps: AmqpServiceOperations, endpoints: Seq[Endpoint]) // Unlike added it just means it was there when we came up
+ case class EndpointAdded(endpoint: Endpoint)
+ case class EndpointModified(endpoint: Endpoint)
+ case class EndpointRemoved(endpoint: Endpoint)
+ case class EndpointOutOfCollection(endpoint: Endpoint) // Unlike removed, just means we're not responsible for it anymore
+
+ private case object LookupEndpoints
+ private case class EndpointsResult(poll: Seq[Endpoint], subscription: Subscription[EndpointNotification])
+ private case class RequestFailure(ex: Throwable)
+
+ def props(strategy: EndpointCollectionStrategy, session: Session, serviceOps: AmqpServiceOperations, endpointObserver: Option[ActorRef], factory: (Endpoint, CollectionMembership, Session, AmqpServiceOperations) => Props): Props =
+ Props(classOf[FepEndpointCollectionManager], strategy, session, serviceOps, endpointObserver, factory)
+
+}
+
+class FepEndpointCollectionManager(strategy: EndpointCollectionStrategy, session: Session, serviceOps: AmqpServiceOperations, endpointObserver: Option[ActorRef], factory: (Endpoint, CollectionMembership, Session, AmqpServiceOperations) => Props) extends Actor with Logging {
+ import FepEndpointCollectionManager._
+
+ private var binding = Option.empty[SubscriptionBinding]
+ private var streams = Map.empty[ModelUUID, ActorRef]
+
+ self ! LookupEndpoints
+
+ def receive = {
+
+ case LookupEndpoints => {
+
+ import context.dispatcher
+ val configFut = strategy.configuration(session)
+
+ configFut.onSuccess { case (poll, subscription) => self ! EndpointsResult(poll, subscription) }
+ configFut.onFailure { case ex => self ! RequestFailure(ex) }
+ }
+
+ case EndpointsResult(results, subscription) => {
+
+ binding = Some(subscription)
+ subscription.start { event => self ! event }
+
+ endpointObserver.foreach(ref => ref ! EndpointsResolved(session, serviceOps, results))
+
+ val initial = results.filter(_.getDisabled == false).map { endpoint =>
+ (endpoint.getUuid, launchStream(endpoint))
+ }
+
+ streams = initial.toMap
+
+ logger.info("Endpoint collection management initialized")
+ }
+
+ case RequestFailure(ex) => throw ex
+
+ case event: EndpointNotification => {
+ val endpoint = event.getValue
+ val uuid = endpoint.getUuid
+ val name = endpoint.getName
+
+ endpointObserver.foreach { obsRef =>
+ val msg = event.getEventType match {
+ case SubscriptionEventType.ADDED => EndpointAdded(endpoint)
+ case SubscriptionEventType.MODIFIED => EndpointModified(endpoint)
+ case SubscriptionEventType.REMOVED => EndpointRemoved(endpoint)
+ }
+
+ obsRef ! msg
+ }
+
+ (event.getEventType, endpoint.getDisabled) match {
+ case (SubscriptionEventType.ADDED, false) =>
+ if (!streams.contains(uuid)) {
+ addStream(endpoint)
+ } else {
+ logger.warn(s"Saw add event on existing endpoint stream: $name (${uuid.getValue})")
+ }
+ case (SubscriptionEventType.ADDED, true) =>
+ case (SubscriptionEventType.MODIFIED, false) =>
+ if (!streams.contains(uuid)) {
+ addStream(endpoint)
+ }
+ case (SubscriptionEventType.MODIFIED, true) => removeStream(uuid)
+ case (SubscriptionEventType.REMOVED, _) => removeStream(uuid)
+ }
+ }
+
+ }
+
+ private def addStream(endpoint: Endpoint) {
+ streams = streams + ((endpoint.getUuid, launchStream(endpoint)))
+ }
+ private def removeStream(uuid: ModelUUID) {
+ logger.debug("Removing endpoint " + uuid.getValue)
+ streams.get(uuid).foreach(ref => ref ! PoisonPill)
+ streams -= uuid
+ }
+
+ private def launchStream(endpoint: Endpoint): ActorRef = {
+ logger.debug("Launching endpoint " + endpoint.getName)
+ context.actorOf(factory(endpoint, strategy.membership, session.spawn(), serviceOps))
+ }
+
+ override def supervisorStrategy: SupervisorStrategy = {
+ import SupervisorStrategy._
+ OneForOneStrategy() {
+ case out: OutOfEndpointCollectionException => {
+ logger.info("Endpoint marked itself as out of collection: " + out.endpoint.getName)
+ endpointObserver.foreach(_ ! EndpointOutOfCollection(out.endpoint))
+ removeStream(out.endpoint.getUuid)
+ Resume
+ }
+ case _: UnauthorizedException => Escalate
+ case _: SessionUnusableException => Escalate
+ case _: IOException => Escalate
+ case _: Throwable => Escalate
+ }
+ }
+}
\ No newline at end of file
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FepEndpointMonitor.scala b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FepEndpointMonitor.scala
new file mode 100644
index 0000000..11a8220
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FepEndpointMonitor.scala
@@ -0,0 +1,123 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.frontend
+
+import akka.actor._
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.app.actor._
+import io.greenbus.client.exception.UnauthorizedException
+import io.greenbus.client.service.ModelService
+import io.greenbus.client.service.proto.Model.{ Endpoint, EndpointNotification }
+import io.greenbus.client.service.proto.ModelRequests.EndpointSubscriptionQuery
+import io.greenbus.msg.{ Session, SessionUnusableException, Subscription, SubscriptionBinding }
+
+import scala.concurrent.{ ExecutionContext, Future }
+
+object FepEndpointMonitor {
+
+ class EndpointConfigException(msg: String) extends Exception(msg)
+
+ def queryEndpoint(session: Session, endpoint: Endpoint)(implicit context: ExecutionContext): Future[(Seq[Endpoint], Subscription[EndpointNotification])] = {
+ val modelClient = ModelService.client(session)
+
+ val endSubQuery = EndpointSubscriptionQuery.newBuilder().addUuids(endpoint.getUuid).build()
+
+ modelClient.subscribeToEndpoints(endSubQuery)
+ }
+
+ case object MakeRequests
+ case class EndpointConfig(endpoints: Seq[Endpoint], endpointSub: Subscription[EndpointNotification])
+ case class RequestFailure(ex: Throwable)
+
+ def props(endpoint: Endpoint, membership: CollectionMembership, session: Session): Props =
+ Props(classOf[FepEndpointMonitor], endpoint, membership, session)
+
+}
+
+class FepEndpointMonitor(endpoint: Endpoint, membership: CollectionMembership, session: Session) extends Actor with Logging {
+ import FepEndpointMonitor._
+
+ private var endpointBinding = Option.empty[SubscriptionBinding]
+
+ self ! MakeRequests
+
+ def receive = {
+
+ case MakeRequests => {
+ import context.dispatcher
+
+ val dataFut = queryEndpoint(session, endpoint)
+ dataFut.onSuccess { case config => self ! EndpointConfig(config._1, config._2) }
+ dataFut.onFailure { case ex => self ! RequestFailure(ex) }
+ }
+
+ case config: EndpointConfig => {
+ import config._
+
+ endpointBinding = Some(endpointSub)
+
+ endpointSub.start { self ! _ }
+
+ logger.info(s"Endpoint collection monitoring initialized for ${endpoint.getName}")
+ }
+
+ case endEvent: EndpointNotification => {
+ import io.greenbus.client.proto.Envelope.SubscriptionEventType._
+
+ // We need to help the collection manager know if our endpoint has fallen outside our set
+ // If the collection manager is subscribing by name/protocol, a change to those parameters may not be
+ // reflected in its subscription
+ endEvent.getEventType match {
+ case ADDED => logger.warn(s"Saw added in endpoint manager for ${endpoint.getName}; endpoint already existed")
+ case REMOVED => // not our responsibility (collection manager will handle)
+ case MODIFIED =>
+ val changed = endEvent.getValue
+ membership match {
+ case AnyAndAllMembership =>
+ case NamedMembership =>
+ if (endpoint.getName != changed.getName) {
+ throw new OutOfEndpointCollectionException(endpoint, "name changed")
+ }
+ case ProtocolMembership(valid) =>
+ if (!valid.contains(changed.getProtocol)) {
+ throw new OutOfEndpointCollectionException(endpoint, "protocol changed")
+ }
+ }
+ }
+ }
+
+ case RequestFailure(ex) =>
+ logger.warn(s"Error making requests for endpoint manager for ${endpoint.getName}: ${ex.getMessage}")
+ throw ex
+ }
+
+ override def postStop() {
+ endpointBinding.foreach(_.cancel())
+ }
+
+ override def supervisorStrategy: SupervisorStrategy = {
+ import akka.actor.SupervisorStrategy._
+ OneForOneStrategy() {
+ case _: UnauthorizedException => Escalate
+ case _: SessionUnusableException => Escalate
+ case _: Throwable => Escalate
+ }
+ }
+
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FrontEndSetManager.scala b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FrontEndSetManager.scala
new file mode 100644
index 0000000..4d4f28d
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FrontEndSetManager.scala
@@ -0,0 +1,155 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.frontend
+
+import akka.actor._
+import akka.pattern.gracefulStop
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.app.actor.RestartConnection
+import io.greenbus.msg.Session
+import io.greenbus.msg.amqp.AmqpServiceOperations
+import io.greenbus.app.actor.frontend.FepEndpointCollectionManager._
+import io.greenbus.app.actor.frontend.FrontEndSetManager._
+import io.greenbus.client.service.proto.Model.{ Endpoint, ModelUUID }
+
+import scala.concurrent.Future
+import scala.concurrent.duration._
+
+object FrontEndSetManager {
+
+ case object BusConnectionStop
+ case object BusConnectionStart
+
+ type ConnMgrFactory = (ActorRef) => Props
+ type ChildFactory[ProtocolConfig] = (Endpoint, ActorRef, MasterProtocol[ProtocolConfig]) => Props
+ type ProtocolMgrFactory[ProtocolConfig] = (ActorContext) => MasterProtocol[ProtocolConfig]
+
+ def props[ProtocolConfig](connMgrFactory: ConnMgrFactory, childFactory: ChildFactory[ProtocolConfig], protFactory: ProtocolMgrFactory[ProtocolConfig]): Props = {
+ Props(classOf[FrontEndSetManager[ProtocolConfig]], connMgrFactory, childFactory, protFactory)
+ }
+}
+class FrontEndSetManager[ProtocolConfig](connMgrFactory: ConnMgrFactory, childFactory: ChildFactory[ProtocolConfig], protFactory: ProtocolMgrFactory[ProtocolConfig]) extends Actor with Logging {
+
+ private val protocolMgr = protFactory(context)
+
+ private var connMgr: Option[ActorRef] = Some(context.actorOf(connMgrFactory(self)))
+
+ private var endpointMap = Map.empty[ModelUUID, ActorRef]
+
+ private var linkOpt = Option.empty[(Session, AmqpServiceOperations)]
+
+ def receive = {
+
+ case BusConnectionStop => {
+ sender ! connMgr.map(ref => gracefulStop(ref, 5000.milliseconds)).getOrElse(Future.successful(true))
+ connMgr = None
+ }
+
+ case BusConnectionStart => {
+ if (connMgr.isEmpty) {
+ connMgr = Some(context.actorOf(connMgrFactory(self)))
+ }
+ }
+
+ case ServiceSessionDead => {
+ linkOpt = None
+ connMgr.foreach { _ ! RestartConnection("session dead") }
+ }
+
+ case EndpointsResolved(session, serviceOps, endpoints) => {
+
+ linkOpt = Some((session, serviceOps))
+
+ // Check if any have been removed or disabled
+ val resolvedMap = endpoints.map(e => (e.getUuid, e)).toMap
+
+ val missingUuids = endpointMap.keySet.filterNot(resolvedMap.contains)
+
+ if (missingUuids.nonEmpty) {
+ logger.info("When resolved, the following endpoints were missing and are being stopped: " + missingUuids.flatMap(resolvedMap.get).map(_.getName).mkString(", "))
+ missingUuids.flatMap(endpointMap.get).foreach(_ ! PoisonPill)
+ endpointMap = endpointMap -- missingUuids
+ }
+
+ val disabledSet = endpoints.filter(_.getDisabled).map(_.getUuid).toSet
+ val nowDisabled = disabledSet.filter(endpointMap.contains)
+
+ if (nowDisabled.nonEmpty) {
+ logger.info("When resolved, the following endpoints were missing and are being stopped: " + missingUuids.flatMap(resolvedMap.get).map(_.getName).mkString(", "))
+ nowDisabled.flatMap(endpointMap.get).foreach(_ ! PoisonPill)
+ endpointMap = endpointMap -- nowDisabled
+ }
+
+ val notDisabledEndpoints = endpoints.filterNot(_.getDisabled)
+ val notStarted = notDisabledEndpoints.filterNot(e => endpointMap.contains(e.getUuid))
+
+ val created = notStarted.map(end => (end.getUuid, context.actorOf(childFactory(end, self, protocolMgr))))
+
+ endpointMap = endpointMap ++ created
+
+ endpointMap.values.foreach { _ ! FrontendProtocolEndpoint.Connected(session, serviceOps) }
+ }
+
+ case EndpointAdded(endpoint) => {
+ if (!endpoint.getDisabled && !endpointMap.contains(endpoint.getUuid)) {
+ addEndpoint(endpoint)
+ }
+ }
+
+ case EndpointModified(endpoint) => {
+ (endpoint.getDisabled, endpointMap.get(endpoint.getUuid)) match {
+ case (true, Some(ref)) =>
+ logger.info("Endpoint " + endpoint.getName + " disabled, shutting down protocol master")
+ ref ! PoisonPill
+ endpointMap -= endpoint.getUuid
+ case (false, None) =>
+ logger.info("Endpoint " + endpoint.getName + " not disabled, starting protocol master")
+ addEndpoint(endpoint)
+ case _ =>
+ }
+ }
+
+ case EndpointRemoved(endpoint) => {
+ endpointMap.get(endpoint.getUuid).foreach { ref =>
+ logger.info("Endpoint " + endpoint.getName + " removed, shutting down protocol master")
+ ref ! PoisonPill
+ endpointMap -= endpoint.getUuid
+ }
+ }
+
+ case EndpointOutOfCollection(endpoint) => {
+ endpointMap.get(endpoint.getUuid).foreach { ref =>
+ logger.info("Endpoint " + endpoint.getName + " removed from collection, shutting down protocol master")
+ ref ! PoisonPill
+ endpointMap -= endpoint.getUuid
+ }
+ }
+ }
+
+ override def postStop(): Unit = {
+ protocolMgr.shutdown()
+ }
+
+ private def addEndpoint(endpoint: Endpoint): Unit = {
+ val actor = context.actorOf(childFactory(endpoint, self, protocolMgr))
+ endpointMap = endpointMap.updated(endpoint.getUuid, actor)
+ linkOpt.foreach { case (sess, ops) => actor ! FrontendProtocolEndpoint.Connected(sess, ops) }
+ }
+
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FrontendConfigurer.scala b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FrontendConfigurer.scala
new file mode 100644
index 0000000..5d841c6
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FrontendConfigurer.scala
@@ -0,0 +1,339 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.frontend
+
+import akka.actor.{ Props, ActorRef, Actor }
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.msg.{ SessionUnusableException, Subscription, Session }
+import io.greenbus.app.actor.MessageScheduling
+import io.greenbus.client.exception.UnauthorizedException
+import io.greenbus.client.proto.Envelope.SubscriptionEventType._
+import io.greenbus.client.service.ModelService
+import io.greenbus.client.service.proto.Model._
+import io.greenbus.client.service.proto.ModelRequests._
+
+import scala.concurrent.{ ExecutionContext, Future }
+import scala.collection.JavaConversions._
+
+case class FrontendConfiguration(
+ points: Seq[Point],
+ commands: Seq[Command],
+ configs: Seq[EntityKeyValue],
+ modelEdgeSub: Subscription[EntityEdgeNotification],
+ configSub: Subscription[EntityKeyValueNotification]) {
+
+ def cancelAll(): Unit = {
+ modelEdgeSub.cancel()
+ configSub.cancel()
+ }
+}
+
+object FrontendConfigurer {
+
+ private def fetchPoints(session: Session, endpoint: Endpoint)(implicit context: ExecutionContext): Future[Seq[Point]] = {
+ val modelClient = ModelService.client(session)
+ fetchFrontEndType(session, endpoint, "Point", modelClient.getPoints)
+ }
+
+ private def fetchCommands(session: Session, endpoint: Endpoint)(implicit context: ExecutionContext): Future[Seq[Command]] = {
+ val modelClient = ModelService.client(session)
+ fetchFrontEndType(session, endpoint, "Command", modelClient.getCommands)
+ }
+
+ private def frontEndTypePage[A](session: Session, endpoint: Endpoint, results: Seq[A], lastUuid: Option[ModelUUID], typ: String, get: EntityKeySet => Future[Seq[A]])(implicit context: ExecutionContext): Future[Seq[A]] = {
+ val modelClient = ModelService.client(session)
+
+ val pageSize = 300
+
+ val objRelationBuilder = EntityRelationshipFlatQuery.newBuilder()
+ .addStartUuids(endpoint.getUuid)
+ .setDescendantOf(true)
+ .setRelationship("source")
+ .addEndTypes(typ)
+
+ val pageParamsB = EntityPagingParams.newBuilder()
+ .setPageSize(pageSize)
+
+ lastUuid.foreach(pageParamsB.setLastUuid)
+
+ val entFut = modelClient.relationshipFlatQuery(objRelationBuilder.setPagingParams(pageParamsB).build())
+
+ entFut.flatMap { ents =>
+ if (ents.isEmpty) {
+ Future.successful(results)
+ } else {
+ val entityKeySet = EntityKeySet.newBuilder().addAllUuids(ents.map(_.getUuid)).build()
+
+ get(entityKeySet).flatMap { objs =>
+ if (ents.size < pageSize) {
+ Future.successful(results ++ objs)
+ } else {
+ frontEndTypePage(session, endpoint, results ++ objs, Some(ents.last.getUuid), typ, get)
+ }
+ }
+ }
+ }
+
+ }
+
+ private def fetchFrontEndType[A](session: Session, endpoint: Endpoint, typ: String, get: EntityKeySet => Future[Seq[A]])(implicit context: ExecutionContext): Future[Seq[A]] = {
+ frontEndTypePage(session, endpoint, Seq(), None, typ, get)
+ }
+
+ private def fetchConfig(session: Session, endpoint: Endpoint, keys: Seq[String])(implicit context: ExecutionContext): Future[Seq[EntityKeyValue]] = {
+ val modelClient = ModelService.client(session)
+
+ val keyPairs = keys.map { key => EntityKeyPair.newBuilder().setUuid(endpoint.getUuid).setKey(key).build() }
+
+ if (keyPairs.nonEmpty) {
+ modelClient.getEntityKeyValues(keyPairs)
+ } else {
+ Future.successful(Seq())
+ }
+ }
+
+ private def fetchConfigSub(session: Session, endpoint: Endpoint, keys: Seq[String])(implicit context: ExecutionContext): Future[Subscription[EntityKeyValueNotification]] = {
+ val modelClient = ModelService.client(session)
+
+ val keyPairs = keys.map { k => EntityKeyPair.newBuilder().setUuid(endpoint.getUuid).setKey(k).build() }
+
+ val subQuery = EntityKeyValueSubscriptionQuery.newBuilder()
+ .addAllKeyPairs(keyPairs)
+ .build()
+
+ modelClient.subscribeToEntityKeyValues(subQuery).map(_._2)
+ }
+
+ private def fetchEndpointEdgesSub(session: Session, endpoint: Endpoint)(implicit context: ExecutionContext): Future[Subscription[EntityEdgeNotification]] = {
+ val modelClient = ModelService.client(session)
+
+ val edgeQuery = EntityEdgeSubscriptionQuery.newBuilder()
+ .addFilters(EntityEdgeFilter.newBuilder()
+ .setParentUuid(endpoint.getUuid)
+ .setRelationship("source")
+ .setDistance(1)
+ .build())
+ .build()
+
+ modelClient.subscribeToEdges(edgeQuery).map(_._2)
+ }
+
+ private def fetchSubs(session: Session, endpoint: Endpoint, keys: Seq[String])(implicit context: ExecutionContext): Future[(Subscription[EntityEdgeNotification], Subscription[EntityKeyValueNotification])] = {
+
+ val entEdgeSubFut = fetchEndpointEdgesSub(session, endpoint)
+
+ entEdgeSubFut.flatMap { entEdgeSub =>
+ val configSubFut = fetchConfigSub(session, endpoint, keys)
+ configSubFut.onFailure { case _ => entEdgeSub.cancel() }
+
+ configSubFut.map(configSub => (entEdgeSub, configSub))
+ }
+ }
+
+ def frontendConfiguration(session: Session, endpoint: Endpoint, keys: Seq[String])(implicit context: ExecutionContext): Future[FrontendConfiguration] = {
+ val subsFut = fetchSubs(session, endpoint, keys)
+
+ val staticFut = fetchPoints(session, endpoint) zip fetchCommands(session, endpoint) zip fetchConfig(session, endpoint, keys)
+
+ staticFut.onFailure { case _ => subsFut.foreach { case (modelSub, configSub) => modelSub.cancel(); configSub.cancel() } }
+
+ (staticFut zip subsFut).map {
+ case (((points, commands), configs), (modelSub, configSub)) =>
+ FrontendConfiguration(points, commands, configs, modelSub, configSub)
+ }
+ }
+
+}
+
+object FrontendConfigUpdater {
+
+ case class ConfigFilesUpdated(configKeyValues: Seq[EntityKeyValue])
+ case class PointsUpdated(points: Seq[Point])
+ case class CommandsUpdated(commands: Seq[Command])
+
+ private case class ObjectAdded(uuid: ModelUUID)
+ private case class ObjectRemoved(uuid: ModelUUID)
+
+ private case class ConfigAdded(config: EntityKeyValue)
+ private case class ConfigModified(config: EntityKeyValue)
+ private case class ConfigRemoved(uuid: ModelUUID, key: String)
+
+ private case class PointRetrieved(point: Point)
+ private case class CommandRetrieved(command: Command)
+
+ private case class ConfigRetrieveError(uuid: ModelUUID, ex: Throwable)
+ private case class PointCommandRetrieveError(uuid: ModelUUID, ex: Throwable)
+
+ private case class ObjectAddedRetry(uuid: ModelUUID)
+ private case class ConfigEdgeAddedRetry(uuid: ModelUUID)
+
+ def props(
+ subject: ActorRef,
+ endpoint: Endpoint,
+ session: Session,
+ config: FrontendConfiguration,
+ requestRetryMs: Long): Props = {
+
+ Props(classOf[FrontendConfigUpdater], subject, endpoint, session, config, requestRetryMs)
+ }
+}
+
+class FrontendConfigUpdater(
+ subject: ActorRef,
+ endpoint: Endpoint,
+ session: Session,
+ config: FrontendConfiguration,
+ requestRetryMs: Long) extends Actor with MessageScheduling with Logging {
+ import FrontendConfigUpdater._
+ import context.dispatcher
+
+ private var pointMap = config.points.map(p => (p.getUuid, p)).toMap
+ private var commandMap = config.commands.map(c => (c.getUuid, c)).toMap
+ private var configMap: Map[(ModelUUID, String), EntityKeyValue] = config.configs.map(cf => ((cf.getUuid, cf.getKey), cf)).toMap
+
+ private var outstandingSourceEnts = Set.empty[ModelUUID]
+
+ config.modelEdgeSub.start { edgeNotification =>
+ val objUuid = edgeNotification.getValue.getChild
+ edgeNotification.getEventType match {
+ case MODIFIED =>
+ case ADDED => self ! ObjectAdded(objUuid)
+ case REMOVED => self ! ObjectRemoved(objUuid)
+ }
+ }
+
+ config.configSub.start { configNotification =>
+ configNotification.getEventType match {
+ case ADDED => self ! ConfigAdded(configNotification.getValue)
+ case MODIFIED => self ! ConfigModified(configNotification.getValue)
+ case REMOVED => self ! ConfigRemoved(configNotification.getValue.getUuid, configNotification.getValue.getKey)
+ }
+ }
+
+ override def postStop(): Unit = {
+ config.modelEdgeSub.cancel()
+ config.configSub.cancel()
+ }
+
+ def receive = {
+ case ConfigAdded(kvp) =>
+ logger.debug("Saw config add for " + kvp.getKey + " on endpoint " + endpoint.getName)
+ configMap += ((kvp.getUuid, kvp.getKey) -> kvp)
+ subject ! ConfigFilesUpdated(configMap.values.toSeq)
+
+ case ConfigModified(kvp) =>
+ logger.debug("Saw config modified for " + kvp.getKey + " on endpoint " + endpoint.getName)
+ configMap += ((kvp.getUuid, kvp.getKey) -> kvp)
+ subject ! ConfigFilesUpdated(configMap.values.toSeq)
+
+ case ConfigRemoved(uuid, key) =>
+ logger.debug("Saw config remove for " + uuid.getValue + " on endpoint " + endpoint.getName)
+ configMap -= ((uuid, key))
+ subject ! ConfigFilesUpdated(configMap.values.toSeq)
+
+ case ObjectAdded(objUuid: ModelUUID) =>
+ logger.debug("Saw object add for " + objUuid.getValue + " on endpoint " + endpoint.getName)
+ outstandingSourceEnts += objUuid
+ retrieveEndpointAssociatedObject(objUuid)
+
+ case ObjectRemoved(objUuid: ModelUUID) =>
+ logger.debug("Saw object remove for " + objUuid.getValue + " on endpoint " + endpoint.getName)
+ outstandingSourceEnts -= objUuid
+ if (pointMap.contains(objUuid)) {
+ pointMap -= objUuid
+ subject ! PointsUpdated(pointMap.values.toSeq)
+ } else if (commandMap.contains(objUuid)) {
+ commandMap -= objUuid
+ subject ! CommandsUpdated(commandMap.values.toSeq)
+ } else {
+ logger.warn("Removed event for " + objUuid.getValue + " did not correspond to obj owned by endpoint " + endpoint.getName)
+ }
+
+ case PointRetrieved(point: Point) =>
+ logger.debug("Retrieved point for " + point.getName + " on endpoint " + endpoint.getName)
+ if (outstandingSourceEnts.contains(point.getUuid)) {
+ pointMap += (point.getUuid -> point)
+ subject ! PointsUpdated(pointMap.values.toSeq)
+ }
+
+ case CommandRetrieved(cmd: Command) =>
+ logger.debug("Retrieved command for " + cmd.getName + " on endpoint " + endpoint.getName)
+ if (outstandingSourceEnts.contains(cmd.getUuid)) {
+ commandMap += (cmd.getUuid -> cmd)
+ subject ! CommandsUpdated(commandMap.values.toSeq)
+ }
+
+ case PointCommandRetrieveError(uuid, ex) =>
+ logger.warn("Point/command lookup error in endpoint " + endpoint.getName)
+ ex match {
+ case ex: SessionUnusableException => throw ex
+ case ex: UnauthorizedException => throw ex
+ case ex: Throwable => scheduleMsg(requestRetryMs, ObjectAddedRetry(uuid))
+ }
+
+ case ObjectAddedRetry(uuid) =>
+ if (outstandingSourceEnts.contains(uuid)) {
+ retrieveEndpointAssociatedObject(uuid)
+ }
+ }
+
+ private def retrieveEndpointAssociatedObject(uuid: ModelUUID) {
+ val lookupFut = lookupFrontEndEntity(uuid).flatMap {
+ case None =>
+ logger.warn("Saw endpoint owned object, no entity found")
+ Future.successful(None)
+ case Some(ent) =>
+ if (ent.getTypesList.exists(_ == "Point")) {
+ lookupPoint(uuid)
+ } else if (ent.getTypesList.exists(_ == "Command")) {
+ lookupCommand(uuid)
+ } else {
+ logger.warn("Saw endpoint owned object that was not a point or command")
+ Future.successful(None)
+ }
+ }
+
+ lookupFut.onSuccess {
+ case None =>
+ case Some(p: Point) => self ! PointRetrieved(p)
+ case Some(c: Command) => self ! CommandRetrieved(c)
+ }
+
+ lookupFut.onFailure {
+ case ex: Throwable => self ! PointCommandRetrieveError(uuid, ex)
+ }
+ }
+
+ private def lookupFrontEndEntity(uuid: ModelUUID): Future[Option[Entity]] = {
+ val modelClient = ModelService.client(session)
+ val keySet = EntityKeySet.newBuilder().addUuids(uuid).build()
+ modelClient.get(keySet).map(_.headOption)
+ }
+
+ private def lookupPoint(uuid: ModelUUID): Future[Option[Point]] = {
+ val modelClient = ModelService.client(session)
+ val keySet = EntityKeySet.newBuilder().addUuids(uuid).build()
+ modelClient.getPoints(keySet).map(_.headOption)
+ }
+ private def lookupCommand(uuid: ModelUUID): Future[Option[Command]] = {
+ val modelClient = ModelService.client(session)
+ val keySet = EntityKeySet.newBuilder().addUuids(uuid).build()
+ modelClient.getCommands(keySet).map(_.headOption)
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FrontendFactory.scala b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FrontendFactory.scala
new file mode 100644
index 0000000..8764307
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FrontendFactory.scala
@@ -0,0 +1,101 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.frontend
+
+import akka.actor.{ ActorContext, ActorRef, Props }
+import io.greenbus.app.actor._
+import io.greenbus.client.ServiceConnection
+import io.greenbus.client.service.proto.Model.Endpoint
+import io.greenbus.msg.Session
+import io.greenbus.msg.amqp.AmqpServiceOperations
+
+import scala.concurrent.ExecutionContext.Implicits.global
+
+object FrontendFactory {
+
+ def create[ProtocolConfig](
+ amqpConfig: AmqpConnectionConfig,
+ userConfigPath: String,
+ endpointStrategy: EndpointCollectionStrategy,
+ protocolMgrFactory: ActorContext => MasterProtocol[ProtocolConfig],
+ configurer: ProtocolConfigurer[ProtocolConfig],
+ configKeys: Seq[String],
+ connectionRepresentsLock: Boolean,
+ nodeId: String,
+ registrationConfig: FrontendRegistrationConfig) = {
+
+ def connMgr(observer: ActorRef) = {
+
+ def endMon(endpoint: Endpoint, coll: CollectionMembership, session: Session, ops: AmqpServiceOperations) = {
+ FepEndpointMonitor.props(endpoint, coll, session)
+ }
+
+ def sessionMgr(conn: ServiceConnection): Props = {
+ SessionLoginManager.props("Endpoint Session Manager", userConfigPath, conn,
+ registrationConfig.loginRetryMs,
+ factory = FepEndpointCollectionManager.props(endpointStrategy, _, _, Some(observer), endMon))
+ }
+
+ FailoverConnectionManager.props("Endpoint Connection Bridge",
+ amqpConfig.amqpConfigFileList,
+ failureLimit = amqpConfig.failureLimit,
+ retryDelayMs = amqpConfig.retryDelayMs,
+ connectionTimeoutMs = amqpConfig.connectionTimeoutMs,
+ sessionMgr)
+ }
+
+ def child(endpoint: Endpoint, manager: ActorRef, masterProtocol: MasterProtocol[ProtocolConfig]) = {
+
+ def proxyFactory(subject: ActorRef, endpoint: Endpoint) = {
+ ProcessingProxy.props(
+ subject,
+ endpoint,
+ registrationConfig.statusHeartbeatPeriodMs,
+ registrationConfig.lapsedTimeMs,
+ registrationConfig.statusRetryPeriodMs,
+ registrationConfig.measRetryPeriodMs,
+ registrationConfig.measQueueLimit)
+ }
+
+ def updaterFactory(
+ subject: ActorRef,
+ endpoint: Endpoint,
+ session: Session,
+ config: FrontendConfiguration) = {
+ FrontendConfigUpdater.props(subject, endpoint, session, config, registrationConfig.configRequestRetryMs)
+ }
+
+ FrontendProtocolEndpoint.props(
+ endpoint,
+ manager,
+ masterProtocol,
+ configurer,
+ registrationConfig.registrationRetryMs,
+ registrationConfig.releaseTimeoutMs,
+ proxyFactory,
+ CommandProxy.props,
+ updaterFactory,
+ FrontendProtocolEndpoint.register(_, _, _, _, connectionRepresentsLock, nodeId),
+ FrontendConfigurer.frontendConfiguration(_, _, configKeys))
+ }
+
+ FrontEndSetManager.props(connMgr, child, protocolMgrFactory)
+
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FrontendProcessConfig.scala b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FrontendProcessConfig.scala
new file mode 100644
index 0000000..47026e9
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FrontendProcessConfig.scala
@@ -0,0 +1,74 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.frontend
+
+import io.greenbus.app.actor.{ EndpointCollectionStrategy, AmqpConnectionConfig }
+
+case class FrontendRegistrationConfig(
+ loginRetryMs: Long,
+ registrationRetryMs: Long,
+ releaseTimeoutMs: Long,
+ statusHeartbeatPeriodMs: Long,
+ lapsedTimeMs: Long,
+ statusRetryPeriodMs: Long,
+ measRetryPeriodMs: Int,
+ measQueueLimit: Int,
+ configRequestRetryMs: Int)
+
+object FrontendRegistrationConfig {
+
+ def defaults = FrontendRegistrationConfig(
+ loginRetryMs = 5000,
+ registrationRetryMs = 5000,
+ releaseTimeoutMs = 20000,
+ statusHeartbeatPeriodMs = 5000,
+ lapsedTimeMs = 11000,
+ statusRetryPeriodMs = 2000,
+ measRetryPeriodMs = 2000,
+ measQueueLimit = 1000,
+ configRequestRetryMs = 5000)
+
+ def build(loginRetryMs: Long,
+ registrationRetryMs: Long,
+ releaseTimeoutMs: Long,
+ statusHeartbeatPeriodMs: Long,
+ lapsedTimeMs: Long,
+ statusRetryPeriodMs: Long,
+ measRetryPeriodMs: Int,
+ measQueueLimit: Int,
+ configRequestRetryMs: Int): FrontendRegistrationConfig = {
+ FrontendRegistrationConfig(
+ loginRetryMs,
+ registrationRetryMs,
+ releaseTimeoutMs,
+ statusHeartbeatPeriodMs,
+ lapsedTimeMs,
+ statusRetryPeriodMs,
+ measRetryPeriodMs,
+ measQueueLimit,
+ configRequestRetryMs)
+ }
+}
+
+case class FrontendProcessConfig(
+ amqpConfig: AmqpConnectionConfig,
+ userConfigPath: String,
+ collectionStrategy: EndpointCollectionStrategy,
+ nodeId: String,
+ registrationConfig: FrontendRegistrationConfig)
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FrontendProtocolEndpoint.scala b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FrontendProtocolEndpoint.scala
new file mode 100644
index 0000000..de003a7
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/FrontendProtocolEndpoint.scala
@@ -0,0 +1,656 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.frontend
+
+import akka.actor.{ ActorRef, PoisonPill, Props }
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.msg.amqp.AmqpServiceOperations
+import io.greenbus.msg.service.ServiceHandlerSubscription
+import io.greenbus.msg.{ Session, SessionUnusableException }
+import io.greenbus.app.actor.MessageScheduling
+import io.greenbus.app.actor.frontend.CommandProxy.ProtocolInitialized
+import io.greenbus.app.actor.frontend.FrontendConfigUpdater.{ CommandsUpdated, ConfigFilesUpdated, PointsUpdated }
+import io.greenbus.app.actor.frontend.FrontendProtocolEndpoint._
+import io.greenbus.app.actor.frontend.ProcessingProxy.LinkLapsed
+import io.greenbus.app.actor.util.NestedStateMachine
+import io.greenbus.client.exception.{ LockedException, UnauthorizedException }
+import io.greenbus.client.service.FrontEndService
+import io.greenbus.client.service.proto.Commands.{ CommandRequest, CommandResult }
+import io.greenbus.client.service.proto.FrontEnd.{ FrontEndConnectionStatus, FrontEndRegistration }
+import io.greenbus.client.service.proto.FrontEndRequests.FrontEndRegistrationTemplate
+import io.greenbus.client.service.proto.Model.{ Endpoint, EntityKeyValue }
+
+import scala.concurrent.{ ExecutionContext, Future }
+
+case object ServiceSessionDead
+
+object FrontendProtocolEndpoint {
+
+ type ConfigUpdaterFactory = (ActorRef, Endpoint, Session, FrontendConfiguration) => Props
+ type ProxyFactory = (ActorRef, Endpoint) => Props
+ type CommandActorFactory = Endpoint => Props
+
+ type RegisterFunc = (Session, AmqpServiceOperations, Endpoint, Boolean) => (Future[FrontEndRegistration], ServiceHandlerSubscription)
+ type ConfigureFunc = (Session, Endpoint) => Future[FrontendConfiguration]
+
+ protected sealed trait BusState
+ protected trait BindingHoldingBusState extends BusState {
+ val commandSub: ServiceHandlerSubscription
+ def cleanup() { commandSub.cancel() }
+ }
+ protected case class ServicesDown(linkPendingSince: Option[Long]) extends BusState
+ protected case class ServicesUp(session: Session, serviceOps: AmqpServiceOperations, linkPendingSince: Option[Long]) extends BusState
+ protected case class RegistrationPending(session: Session, serviceOps: AmqpServiceOperations, commandSub: ServiceHandlerSubscription, linkPendingSince: Option[Long]) extends BindingHoldingBusState
+ protected case class Registered(session: Session, serviceOps: AmqpServiceOperations, commandSub: ServiceHandlerSubscription, inputAddress: String) extends BindingHoldingBusState
+ protected case class ConfigPending(session: Session, serviceOps: AmqpServiceOperations, commandSub: ServiceHandlerSubscription, inputAddress: String, configFut: Future[FrontendConfiguration]) extends BindingHoldingBusState
+ protected case class Configured(session: Session, serviceOps: AmqpServiceOperations, commandSub: ServiceHandlerSubscription, inputAddress: String, updater: ActorRef) extends BindingHoldingBusState
+
+ protected sealed trait ProtocolState
+ protected case object ProtocolUninit extends ProtocolState
+
+ protected sealed trait ProtocolActiveState extends ProtocolState {
+ val configs: Seq[EntityKeyValue]
+ val holdingLock: Boolean = false
+ }
+ protected case class ProtocolPending(configs: Seq[EntityKeyValue]) extends ProtocolActiveState
+ protected case class ProtocolError(configs: Seq[EntityKeyValue]) extends ProtocolActiveState
+ protected case class ProtocolUp(configs: Seq[EntityKeyValue]) extends ProtocolActiveState {
+ override val holdingLock = true
+ }
+ protected case class ProtocolDown(configs: Seq[EntityKeyValue]) extends ProtocolActiveState
+
+ case class Connected(session: Session, serviceOps: AmqpServiceOperations)
+ private case class RegistrationSuccess(conns: FrontEndRegistration)
+ private case class RegistrationFailure(ex: Throwable)
+ private case class ConfigurationSuccess(config: FrontendConfiguration)
+ private case class ConfigurationFailure(ex: Throwable)
+
+ private sealed trait Retry
+ private case object DoRegistrationRetry extends Retry
+ private case object DoConfigRetry extends Retry
+ private case class DoReleaseTimeoutCheck(lossTime: Long) extends Retry
+
+ private case class CommandIssued(req: CommandRequest, response: CommandResult => Unit)
+
+ def props[ProtocolConfig](
+ endpoint: Endpoint,
+ manager: ActorRef,
+ protocolMgr: MasterProtocol[ProtocolConfig],
+ configurer: ProtocolConfigurer[ProtocolConfig],
+ registrationRetryMs: Long,
+ releaseTimeoutMs: Long,
+ proxyFactory: ProxyFactory,
+ commandActorFactory: CommandActorFactory,
+ configUpdaterFactory: ConfigUpdaterFactory,
+ registerFunc: RegisterFunc,
+ configureFunc: ConfigureFunc): Props = {
+
+ Props(classOf[FrontendProtocolEndpoint[ProtocolConfig]], endpoint, manager, protocolMgr, configurer, registrationRetryMs, releaseTimeoutMs, proxyFactory, commandActorFactory, configUpdaterFactory, registerFunc, configureFunc)
+ }
+
+ def register(session: Session, serviceOps: AmqpServiceOperations, endpoint: Endpoint, holdingLock: Boolean, lockingEnabled: Boolean, nodeId: String)(implicit context: ExecutionContext): (Future[FrontEndRegistration], ServiceHandlerSubscription) = {
+ val serviceHandlerSub = serviceOps.routedServiceBinding()
+ val cmdAddress = serviceHandlerSub.getId()
+
+ try {
+ val client = FrontEndService.client(session)
+
+ val registration = FrontEndRegistrationTemplate.newBuilder()
+ .setEndpointUuid(endpoint.getUuid)
+ .setCommandAddress(cmdAddress)
+ .setHoldingLock(holdingLock && lockingEnabled)
+ .setFepNodeId(nodeId)
+ .build()
+
+ val regFut = client.putFrontEndRegistration(registration, endpoint.getUuid.getValue)
+
+ (regFut, serviceHandlerSub)
+
+ } catch {
+ case ex: Throwable =>
+ serviceHandlerSub.cancel()
+ throw ex
+ }
+ }
+
+ def configsEqual(l: Seq[EntityKeyValue], r: Seq[EntityKeyValue]): Boolean = {
+ l.map(_.hashCode()).sorted == r.map(_.hashCode()).sorted
+ }
+}
+
+class FrontendProtocolEndpoint[ProtocolConfig](
+ endpoint: Endpoint,
+ manager: ActorRef,
+ protocolMgr: MasterProtocol[ProtocolConfig],
+ configurer: ProtocolConfigurer[ProtocolConfig],
+ registrationRetryMs: Long,
+ releaseTimeoutMs: Long,
+ proxyFactory: ProxyFactory,
+ commandActorFactory: CommandActorFactory,
+ configUpdaterFactory: ConfigUpdaterFactory,
+ registerFunc: RegisterFunc,
+ configureFunc: ConfigureFunc) extends NestedStateMachine with MessageScheduling with Logging {
+ import context.dispatcher
+ import io.greenbus.app.actor.frontend.FrontendProtocolEndpoint._
+
+ private val proxy = context.actorOf(proxyFactory(self, endpoint))
+ private val commandActor = context.actorOf(commandActorFactory(endpoint))
+
+ protected type StateType = (BusState, ProtocolState)
+ protected def start: StateType = (ServicesDown(None), ProtocolUninit)
+ override protected def instanceId: Option[String] = Some(endpoint.getName)
+
+ protected def machine = {
+
+ case state @ (busState: ServicesDown, ProtocolUninit) => {
+
+ case Connected(session, serviceOps) => {
+ (doRegistration(session, serviceOps, holdingLock = false, busState.linkPendingSince), ProtocolUninit)
+ }
+ }
+
+ case state @ (busState: ServicesUp, ProtocolUninit) => {
+
+ case ServiceSessionDead => {
+ manager ! ServiceSessionDead
+ (ServicesDown(busState.linkPendingSince), ProtocolUninit)
+ }
+ case Connected(session, serviceOps) => {
+ (doRegistration(session, serviceOps, holdingLock = false, busState.linkPendingSince), ProtocolUninit)
+ }
+ case DoRegistrationRetry => {
+ (doRegistration(busState.session, busState.serviceOps, holdingLock = false, busState.linkPendingSince), ProtocolUninit)
+ }
+ }
+
+ case state @ (busState: RegistrationPending, ProtocolUninit) => {
+
+ case ServiceSessionDead => {
+ manager ! ServiceSessionDead
+ busState.commandSub.cancel()
+ (ServicesDown(busState.linkPendingSince), ProtocolUninit)
+ }
+ case Connected(session, serviceOps) => {
+ busState.commandSub.cancel()
+ (doRegistration(session, serviceOps, holdingLock = false, busState.linkPendingSince), ProtocolUninit)
+ }
+ case RegistrationFailure(ex) => {
+
+ def sessionDead(): StateType = {
+ manager ! ServiceSessionDead
+ busState.commandSub.cancel()
+ (ServicesDown(busState.linkPendingSince), ProtocolUninit)
+ }
+
+ def retry(): StateType = {
+ scheduleMsg(registrationRetryMs, DoRegistrationRetry)
+ busState.commandSub.cancel()
+ (ServicesUp(busState.session, busState.serviceOps, busState.linkPendingSince), ProtocolUninit)
+ }
+
+ ex match {
+ case ex: SessionUnusableException => sessionDead()
+ case ex: UnauthorizedException => sessionDead()
+ case ex: LockedException => retry()
+ case ex: Throwable => retry()
+ }
+ }
+ case RegistrationSuccess(registration) => {
+
+ proxy ! ProcessingProxy.LinkUp(busState.session, registration.getInputAddress)
+
+ commandActor ! CommandProxy.CommandSubscriptionRetrieved(busState.commandSub)
+
+ (doConfig(busState.session, busState.serviceOps, busState.commandSub, registration.getInputAddress), ProtocolUninit)
+ }
+ }
+
+ case state @ (busState: Registered, ProtocolUninit) => {
+
+ case ServiceSessionDead => {
+ busState.commandSub.cancel()
+ sessionDeadGoDown(ProtocolUninit)
+ }
+ case Connected(session, serviceOps) => {
+ busState.commandSub.cancel()
+ doRegistrationAndSwitch(session, serviceOps, holdingLock = false, ProtocolUninit)
+ }
+ case LinkLapsed => {
+ busState.commandSub.cancel()
+ doRegistrationAndSwitch(busState.session, busState.serviceOps, holdingLock = false, ProtocolUninit)
+ }
+ case DoConfigRetry => {
+ (doConfig(busState.session, busState.serviceOps, busState.commandSub, busState.inputAddress), ProtocolUninit)
+ }
+ }
+
+ case state @ (busState: ConfigPending, ProtocolUninit) => {
+
+ case ServiceSessionDead => {
+ busState.commandSub.cancel()
+ sessionDeadGoDown(ProtocolUninit)
+ }
+ case Connected(session, serviceOps) => {
+ busState.commandSub.cancel()
+ doRegistrationAndSwitch(session, serviceOps, holdingLock = false, ProtocolUninit)
+ }
+ case LinkLapsed => {
+ busState.commandSub.cancel()
+ doRegistrationAndSwitch(busState.session, busState.serviceOps, holdingLock = false, ProtocolUninit)
+ }
+ case ConfigurationFailure(ex) => {
+ logger.debug(s"Endpoint ${endpoint.getName} saw configuration failure: $ex")
+
+ def sessionDead(): StateType = {
+ busState.commandSub.cancel()
+ sessionDeadGoDown(ProtocolUninit)
+ }
+
+ def retry(): StateType = {
+ scheduleMsg(registrationRetryMs, DoConfigRetry)
+ (Registered(busState.session, busState.serviceOps, busState.commandSub, busState.inputAddress), ProtocolUninit)
+ }
+
+ ex match {
+ case ex: SessionUnusableException => sessionDead()
+ case ex: UnauthorizedException => sessionDead()
+ case ex: Throwable => retry()
+ }
+ }
+ case ConfigurationSuccess(config) => {
+
+ proxy ! ProcessingProxy.PointMapUpdated(config.points.map(p => (p.getName, p.getUuid)).toMap)
+ commandActor ! CommandProxy.CommandsUpdated(config.commands)
+
+ val configUpdaterChild = context.actorOf(configUpdaterFactory(self, endpoint, busState.session, config))
+
+ val resultBusState = Configured(busState.session, busState.serviceOps, busState.commandSub, busState.inputAddress, configUpdaterChild)
+
+ configurer.evaluate(endpoint, config.configs) match {
+ case None =>
+ logger.warn("Configuration couldn't be read for endpoint " + endpoint.getName)
+ (resultBusState, ProtocolUninit)
+ case Some(protocolConfig) => {
+
+ try {
+ val cmdAcceptor = protocolMgr.add(endpoint, protocolConfig, proxy ! _, proxy ! _)
+ commandActor ! ProtocolInitialized(cmdAcceptor)
+ (resultBusState, ProtocolPending(config.configs))
+ } catch {
+ case ex: Throwable =>
+ logger.error(s"Error adding protocol master for endpoint ${endpoint.getName}: " + ex)
+ (resultBusState, ProtocolUninit)
+ }
+ }
+ }
+ }
+ }
+
+ case state @ (busState: Configured, ProtocolUninit) => {
+
+ case ServiceSessionDead => {
+ busState.commandSub.cancel()
+ busState.updater ! PoisonPill
+ sessionDeadGoDown(ProtocolUninit)
+ }
+ case Connected(session, serviceOps) => {
+ busState.updater ! PoisonPill
+ busState.commandSub.cancel()
+ doRegistrationAndSwitch(session, serviceOps, holdingLock = false, ProtocolUninit)
+ }
+ case LinkLapsed => {
+ busState.updater ! PoisonPill
+ busState.commandSub.cancel()
+ doRegistrationAndSwitch(busState.session, busState.serviceOps, holdingLock = false, ProtocolUninit)
+ }
+ case ConfigFilesUpdated(configFiles) => {
+
+ configurer.evaluate(endpoint, configFiles) match {
+ case None =>
+ logger.warn("Configuration couldn't be read for endpoint " + endpoint.getName)
+ state
+ case Some(protocolConfig) => {
+ try {
+ val cmdAcceptor = protocolMgr.add(endpoint, protocolConfig, proxy ! _, proxy ! _)
+ commandActor ! ProtocolInitialized(cmdAcceptor)
+ (busState, ProtocolPending(configFiles))
+ } catch {
+ case ex: Throwable =>
+ logger.error(s"Error adding protocol master for endpoint ${endpoint.getName}: " + ex)
+ state
+ }
+ }
+ }
+ }
+ case PointsUpdated(points) => {
+ proxy ! ProcessingProxy.PointMapUpdated(points.map(p => (p.getName, p.getUuid)).toMap)
+ state
+ }
+ case CommandsUpdated(commands) => {
+ commandActor ! CommandProxy.CommandsUpdated(commands)
+ state
+ }
+ }
+
+ case state @ (busState: Configured, protocolState: ProtocolActiveState) => {
+
+ case ServiceSessionDead => {
+ busState.commandSub.cancel()
+ busState.updater ! PoisonPill
+ sessionDeadGoDown(protocolState)
+ }
+ case Connected(session, serviceOps) => {
+ busState.updater ! PoisonPill
+ busState.commandSub.cancel()
+ doRegistrationAndSwitch(session, serviceOps, holdingLock = protocolState.holdingLock, protocolState)
+ }
+ case LinkLapsed => {
+ busState.updater ! PoisonPill
+ busState.commandSub.cancel()
+ doRegistrationAndSwitch(busState.session, busState.serviceOps, holdingLock = protocolState.holdingLock, protocolState)
+ }
+ case update @ StackStatusUpdated(status) => {
+ handleProtocolStateUpdate(update, protocolState.configs, busState)
+ }
+ case ConfigFilesUpdated(configFiles) => {
+ val optStateChange = handleProtocolUpConfigUpdate(protocolState.configs, configFiles)
+ (busState, optStateChange.getOrElse(protocolState))
+ }
+ case PointsUpdated(points) => {
+ proxy ! ProcessingProxy.PointMapUpdated(points.map(p => (p.getName, p.getUuid)).toMap)
+ state
+ }
+ case CommandsUpdated(commands) => {
+ commandActor ! CommandProxy.CommandsUpdated(commands)
+ state
+ }
+ }
+
+ case state @ (busState: ServicesDown, protocolState: ProtocolActiveState) => {
+
+ case Connected(session, serviceOps) => {
+ (doRegistration(session, serviceOps, holdingLock = protocolState.holdingLock, busState.linkPendingSince), protocolState)
+ }
+ case update @ StackStatusUpdated(status) => {
+ handleProtocolStateUpdate(update, protocolState.configs, busState)
+ }
+ case DoReleaseTimeoutCheck(lossTime) => {
+ if (busState.linkPendingSince == Some(lossTime)) {
+ logger.info(s"Endpoint ${endpoint.getName} releasing protocol because release timeout expired")
+ commandActor ! CommandProxy.ProtocolUninitialized
+ protocolMgr.remove(endpoint.getUuid)
+ (busState, ProtocolUninit)
+ } else {
+ state
+ }
+ }
+ }
+
+ case state @ (busState: ServicesUp, protocolState: ProtocolActiveState) => {
+
+ case ServiceSessionDead => {
+ manager ! ServiceSessionDead
+ (ServicesDown(busState.linkPendingSince), protocolState)
+ }
+ case Connected(session, serviceOps) => {
+ (doRegistration(session, serviceOps, holdingLock = protocolState.holdingLock, busState.linkPendingSince), protocolState)
+ }
+ case DoRegistrationRetry => {
+ (doRegistration(busState.session, busState.serviceOps, holdingLock = protocolState.holdingLock, busState.linkPendingSince), protocolState)
+ }
+ case update @ StackStatusUpdated(status) => {
+ handleProtocolStateUpdate(update, protocolState.configs, busState)
+ }
+ case DoReleaseTimeoutCheck(lossTime) => {
+ if (busState.linkPendingSince == Some(lossTime)) {
+ logger.info(s"Endpoint ${endpoint.getName} releasing protocol because release timeout expired")
+ commandActor ! CommandProxy.ProtocolUninitialized
+ protocolMgr.remove(endpoint.getUuid)
+ (busState, ProtocolUninit)
+ } else {
+ state
+ }
+ }
+ }
+
+ case state @ (busState: RegistrationPending, protocolState: ProtocolActiveState) => {
+
+ case ServiceSessionDead => {
+ manager ! ServiceSessionDead
+ (ServicesDown(busState.linkPendingSince), protocolState)
+ }
+ case Connected(session, serviceOps) => {
+ (doRegistration(session, serviceOps, holdingLock = protocolState.holdingLock, busState.linkPendingSince), protocolState)
+ }
+ case update @ StackStatusUpdated(status) => {
+ handleProtocolStateUpdate(update, protocolState.configs, busState)
+ }
+ case RegistrationFailure(ex) => {
+
+ def sessionDead(): StateType = {
+ manager ! ServiceSessionDead
+ busState.commandSub.cancel()
+ (ServicesDown(busState.linkPendingSince), protocolState)
+ }
+
+ def retry(): StateType = {
+ scheduleMsg(registrationRetryMs, DoRegistrationRetry)
+ busState.commandSub.cancel()
+ (ServicesUp(busState.session, busState.serviceOps, busState.linkPendingSince), protocolState)
+ }
+
+ ex match {
+ case ex: SessionUnusableException => sessionDead()
+ case ex: UnauthorizedException => sessionDead()
+ case ex: LockedException =>
+ logger.info(s"Endpoint ${endpoint.getName} releasing protocol because registration returned locked")
+ busState.commandSub.cancel()
+ commandActor ! CommandProxy.ProtocolUninitialized
+ protocolMgr.remove(endpoint.getUuid)
+ scheduleMsg(registrationRetryMs, DoRegistrationRetry)
+ (ServicesUp(busState.session, busState.serviceOps, busState.linkPendingSince), ProtocolUninit)
+ case ex: Throwable => retry()
+ }
+ }
+ case RegistrationSuccess(registration) => {
+
+ proxy ! ProcessingProxy.LinkUp(busState.session, registration.getInputAddress)
+
+ commandActor ! CommandProxy.CommandSubscriptionRetrieved(busState.commandSub)
+
+ (doConfig(busState.session, busState.serviceOps, busState.commandSub, registration.getInputAddress), protocolState)
+ }
+ case DoReleaseTimeoutCheck(lossTime) => {
+ if (busState.linkPendingSince == Some(lossTime)) {
+ logger.info(s"Endpoint ${endpoint.getName} releasing protocol because release timeout expired")
+ commandActor ! CommandProxy.ProtocolUninitialized
+ protocolMgr.remove(endpoint.getUuid)
+ (busState, ProtocolUninit)
+ } else {
+ state
+ }
+ }
+ }
+
+ case state @ (busState: Registered, protocolState: ProtocolActiveState) => {
+
+ case ServiceSessionDead => {
+ busState.commandSub.cancel()
+ sessionDeadGoDown(protocolState)
+ }
+ case Connected(session, serviceOps) => {
+ busState.commandSub.cancel()
+ doRegistrationAndSwitch(session, serviceOps, holdingLock = protocolState.holdingLock, protocolState)
+ }
+ case LinkLapsed => {
+ busState.commandSub.cancel()
+ doRegistrationAndSwitch(busState.session, busState.serviceOps, holdingLock = protocolState.holdingLock, protocolState)
+ }
+ case update @ StackStatusUpdated(status) => {
+ handleProtocolStateUpdate(update, protocolState.configs, busState)
+ }
+ case DoConfigRetry => {
+ (doConfig(busState.session, busState.serviceOps, busState.commandSub, busState.inputAddress), protocolState)
+ }
+ }
+
+ case state @ (busState: ConfigPending, protocolState: ProtocolActiveState) => {
+
+ case ServiceSessionDead => {
+ busState.commandSub.cancel()
+ sessionDeadGoDown(protocolState)
+ }
+ case Connected(session, serviceOps) => {
+ busState.commandSub.cancel()
+ doRegistrationAndSwitch(session, serviceOps, holdingLock = protocolState.holdingLock, protocolState)
+ }
+ case LinkLapsed => {
+ busState.commandSub.cancel()
+ doRegistrationAndSwitch(busState.session, busState.serviceOps, holdingLock = protocolState.holdingLock, protocolState)
+ }
+ case update @ StackStatusUpdated(status) => {
+ handleProtocolStateUpdate(update, protocolState.configs, busState)
+ }
+ case DoConfigRetry => {
+ (doConfig(busState.session, busState.serviceOps, busState.commandSub, busState.inputAddress), protocolState)
+ }
+ case ConfigurationFailure(ex) => {
+ logger.debug(s"Endpoint ${endpoint.getName} saw configuration failure: $ex")
+
+ def sessionDead(): StateType = {
+ busState.commandSub.cancel()
+ sessionDeadGoDown(protocolState)
+ }
+
+ def retry(): StateType = {
+ scheduleMsg(registrationRetryMs, DoConfigRetry)
+ (Registered(busState.session, busState.serviceOps, busState.commandSub, busState.inputAddress), protocolState)
+ }
+
+ ex match {
+ case ex: SessionUnusableException => sessionDead()
+ case ex: UnauthorizedException => sessionDead()
+ case ex: Throwable => retry()
+ }
+ }
+ case ConfigurationSuccess(config) => {
+ logger.debug(s"Endpoint ${endpoint.getName} reconfigured while protocol running")
+ val configUpdaterChild = context.actorOf(configUpdaterFactory(self, endpoint, busState.session, config))
+ proxy ! ProcessingProxy.PointMapUpdated(config.points.map(p => (p.getName, p.getUuid)).toMap)
+ commandActor ! CommandProxy.CommandsUpdated(config.commands)
+
+ val resultBusState = Configured(busState.session, busState.serviceOps, busState.commandSub, busState.inputAddress, configUpdaterChild)
+ val optStateChange = handleProtocolUpConfigUpdate(protocolState.configs, config.configs)
+ (resultBusState, optStateChange.getOrElse(protocolState))
+ }
+ }
+ }
+
+ override protected def onShutdown(state: (BusState, ProtocolState)): Unit = {
+ logger.debug("Endpoint " + endpoint.getName + " saw shutdown in state " + NestedStateMachine.stateToString(state))
+ val (busState, protocolState) = state
+ protocolState match {
+ case _: ProtocolActiveState =>
+ logger.info("Endpoint " + endpoint.getName + " is removing protocol due to shutdown")
+ protocolMgr.remove(endpoint.getUuid)
+ case _ =>
+ }
+ busState match {
+ case s: BindingHoldingBusState => s.cleanup()
+ case _ =>
+ }
+ }
+
+ private def sessionDeadGoDown(protocolState: ProtocolState) = {
+ val now = System.currentTimeMillis()
+ scheduleMsg(releaseTimeoutMs, DoReleaseTimeoutCheck(now))
+ manager ! ServiceSessionDead
+ (ServicesDown(Some(now)), protocolState)
+ }
+
+ private def doRegistrationAndSwitch(session: Session, serviceOps: AmqpServiceOperations, holdingLock: Boolean, protocolState: ProtocolState) = {
+ val now = System.currentTimeMillis()
+ scheduleMsg(releaseTimeoutMs, DoReleaseTimeoutCheck(now))
+ (doRegistration(session, serviceOps, holdingLock = holdingLock, Some(System.currentTimeMillis())), protocolState)
+ }
+
+ override protected def unhandled(obj: Any, state: (BusState, ProtocolState)): Unit = {
+ obj match {
+ case r: Retry =>
+ case x => logger.warn("Saw unhandled event " + obj.getClass.getSimpleName + " in state " + NestedStateMachine.stateToString(state))
+ }
+ }
+
+ private def handleProtocolStateUpdate(update: StackStatusUpdated, configs: Seq[EntityKeyValue], busState: BusState) = {
+ proxy ! update
+ import io.greenbus.client.service.proto.FrontEnd.FrontEndConnectionStatus.Status._
+ update.state match {
+ case COMMS_UP => (busState, ProtocolUp(configs))
+ case COMMS_DOWN => (busState, ProtocolDown(configs))
+ case UNKNOWN => (busState, ProtocolDown(configs))
+ case ERROR => (busState, ProtocolError(configs))
+ }
+ }
+
+ private def handleProtocolUpConfigUpdate(oldConfigs: Seq[EntityKeyValue], latestConfigs: Seq[EntityKeyValue]): Option[ProtocolState] = {
+ if (!configsEqual(oldConfigs, latestConfigs)) {
+ configurer.evaluate(endpoint, latestConfigs) match {
+ case None =>
+ logger.info("Updated configuration couldn't be used " + endpoint.getName + " is removing protocol")
+ protocolMgr.remove(endpoint.getUuid)
+ commandActor ! CommandProxy.ProtocolUninitialized
+ proxy ! StackStatusUpdated(FrontEndConnectionStatus.Status.COMMS_DOWN)
+ Some(ProtocolUninit)
+ case Some(protocolConfig) => {
+ logger.info("Updated configuration, " + endpoint.getName + " is re-initializing protocol")
+ try {
+ commandActor ! CommandProxy.ProtocolUninitialized
+ protocolMgr.remove(endpoint.getUuid)
+ val cmdAcceptor = protocolMgr.add(endpoint, protocolConfig, proxy ! _, proxy ! _)
+ commandActor ! CommandProxy.ProtocolInitialized(cmdAcceptor)
+ Some(ProtocolPending(latestConfigs))
+ } catch {
+ case ex: Throwable =>
+ logger.error(s"Error adding protocol master for endpoint ${endpoint.getName}: " + ex)
+ Some(ProtocolUninit)
+ }
+ }
+ }
+ } else {
+ logger.debug(s"Endpoint ${endpoint.getName} saw byte-equivalent config update")
+ None
+ }
+ }
+
+ private def doConfig(session: Session, serviceOps: AmqpServiceOperations, commandSub: ServiceHandlerSubscription, inputAddress: String): ConfigPending = {
+ val configFut = configureFunc(session, endpoint)
+ configFut.onSuccess { case resp => self ! ConfigurationSuccess(resp) }
+ configFut.onFailure { case ex => self ! ConfigurationFailure(ex) }
+ ConfigPending(session, serviceOps, commandSub, inputAddress, configFut)
+ }
+
+ private def doRegistration(session: Session, serviceOps: AmqpServiceOperations, holdingLock: Boolean, linkPendingSince: Option[Long]): RegistrationPending = {
+ val (fut, sub) = registerFunc(session, serviceOps, endpoint, holdingLock)
+
+ fut.onSuccess { case conn => self ! RegistrationSuccess(conn) }
+ fut.onFailure { case ex => self ! RegistrationFailure(ex) }
+
+ RegistrationPending(session, serviceOps, sub, linkPendingSince)
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/frontend/MasterProtocol.scala b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/MasterProtocol.scala
new file mode 100644
index 0000000..81ff79a
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/MasterProtocol.scala
@@ -0,0 +1,104 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.frontend
+
+import akka.actor._
+import akka.pattern.{ AskTimeoutException, ask }
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.app.actor.util.TraceMessage
+import io.greenbus.client.service.proto.Commands.{ CommandRequest, CommandResult, CommandStatus }
+import io.greenbus.client.service.proto.FrontEnd.FrontEndConnectionStatus
+import io.greenbus.client.service.proto.Measurements.Measurement
+import io.greenbus.client.service.proto.Model.{ Endpoint, EntityKeyValue, ModelUUID }
+
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.Future
+import scala.concurrent.duration._
+
+trait ProtocolConfigurer[ProtocolConfig] {
+ def evaluate(endpoint: Endpoint, keyValues: Seq[EntityKeyValue]): Option[ProtocolConfig]
+ def equivalent(latest: ProtocolConfig, previous: ProtocolConfig): Boolean
+}
+
+case class MeasurementsPublished(wallTime: Long, idMeasurements: Seq[(ModelUUID, Measurement)], namedMeasurements: Seq[(String, Measurement)]) extends TraceMessage
+
+case class StackStatusUpdated(state: FrontEndConnectionStatus.Status)
+
+trait MasterProtocol[ProtocolConfig] {
+
+ def add(endpoint: Endpoint, protocolConfig: ProtocolConfig, publish: MeasurementsPublished => Unit, statusUpdate: StackStatusUpdated => Unit): ProtocolCommandAcceptor
+
+ def remove(endpointUuid: ModelUUID)
+
+ def shutdown()
+
+}
+
+object ActorProtocolManager {
+
+ case class CommandIssued(commandName: String, request: CommandRequest)
+
+ type ActorProtocolFactory[ProtocolConfig] = (ModelUUID, String, ProtocolConfig, MeasurementsPublished => Unit, StackStatusUpdated => Unit) => Props
+}
+
+class ActorProtocolManager[ProtocolConfig](context: ActorContext, factory: ActorProtocolManager.ActorProtocolFactory[ProtocolConfig]) extends MasterProtocol[ProtocolConfig] with Logging {
+ import io.greenbus.app.actor.frontend.ActorProtocolManager._
+
+ private var actors = Map.empty[ModelUUID, ActorRef]
+
+ def add(endpoint: Endpoint, protocolConfig: ProtocolConfig, publish: (MeasurementsPublished) => Unit, statusUpdate: (StackStatusUpdated) => Unit): ProtocolCommandAcceptor = {
+ actors.get(endpoint.getUuid).foreach(_ ! PoisonPill)
+ val ref = context.actorOf(factory(endpoint.getUuid, endpoint.getName, protocolConfig, publish, statusUpdate))
+ actors += (endpoint.getUuid -> ref)
+
+ new ProtocolCommandAcceptor {
+
+ def issue(commandName: String, request: CommandRequest): Future[CommandResult] = {
+ val askFut = ref.ask(CommandIssued(commandName, request))(4000.milliseconds)
+
+ askFut.flatMap {
+ case cmdFut: Future[_] => cmdFut.asInstanceOf[Future[CommandResult]]
+ case all =>
+ logger.error("Endpoint " + endpoint.getName + " received unknown response from protocol master actor: " + all)
+ Future.successful(CommandResult.newBuilder().setStatus(CommandStatus.UNDEFINED).build())
+ }.recover {
+ case at: AskTimeoutException =>
+ logger.warn("Endpoint " + endpoint.getName + " could not issue command to protocol master actor")
+ CommandResult.newBuilder().setStatus(CommandStatus.TIMEOUT).build()
+
+ case ex =>
+ logger.warn("Endpoint " + endpoint.getName + " had error communicating with protocol master actor: " + ex)
+ CommandResult.newBuilder().setStatus(CommandStatus.UNDEFINED).build()
+ }
+ }
+ }
+ }
+
+ def remove(endpointUuid: ModelUUID) {
+ actors.get(endpointUuid).foreach { ref =>
+ ref ! PoisonPill
+ actors -= endpointUuid
+ }
+ }
+
+ def shutdown() {
+ actors.foreach { case (_, ref) => ref ! PoisonPill }
+ }
+
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/frontend/ProcessingProxy.scala b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/ProcessingProxy.scala
new file mode 100644
index 0000000..1192ba5
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/ProcessingProxy.scala
@@ -0,0 +1,474 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.frontend
+
+import akka.actor.{ Actor, ActorRef, Props }
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.app.actor.MessageScheduling
+import io.greenbus.app.actor.frontend.ProcessingProxy.DoStackStatusHeartbeat
+import io.greenbus.app.actor.util.{ NestedStateMachine, TraceMessage }
+import io.greenbus.client.exception.UnauthorizedException
+import io.greenbus.client.service.proto.FrontEnd.FrontEndConnectionStatus
+import io.greenbus.client.service.proto.FrontEndRequests.FrontEndStatusUpdate
+import io.greenbus.client.service.proto.Measurements.{ NamedMeasurementValue, Measurement, MeasurementBatch, PointMeasurementValue }
+import io.greenbus.client.service.proto.Model.{ Endpoint, ModelUUID }
+import io.greenbus.client.service.{ FrontEndService, MeasurementService }
+import io.greenbus.msg.{ Session, SessionUnusableException }
+
+import scala.collection.JavaConversions._
+import scala.collection.immutable.VectorBuilder
+
+object ProcessingProxy extends Logging {
+
+ case class LinkUp(session: Session, inputAddress: String)
+ case class PointMapUpdated(pointMap: Map[String, ModelUUID])
+
+ case object LinkLapsed
+
+ object MeasLastValueMap {
+ def apply(): MeasLastValueMap = MeasLastValueMap(Map(), Map())
+ }
+ case class MeasLastValueMap(byUuid: Map[ModelUUID, Measurement], byName: Map[String, Measurement]) {
+ def update(moreByUuid: Seq[(ModelUUID, Measurement)], moreByName: Seq[(String, Measurement)]): MeasLastValueMap = {
+ MeasLastValueMap(byUuid ++ moreByUuid, byName ++ moreByName)
+ }
+ }
+
+ object MeasQueue {
+ def apply(): MeasQueue = MeasQueue(Seq(), Seq())
+ }
+ case class MeasQueue(byUuid: Seq[(ModelUUID, Measurement)], byName: Seq[(String, Measurement)]) {
+ def enqueue(moreByUuid: Seq[(ModelUUID, Measurement)], moreByName: Seq[(String, Measurement)]): MeasQueue = {
+ MeasQueue(byUuid ++ moreByUuid, byName ++ moreByName)
+ }
+ def enqueue(other: MeasQueue): MeasQueue = {
+ MeasQueue(byUuid ++ other.byUuid, byName ++ other.byName)
+ }
+
+ def nonEmpty: Boolean = byUuid.nonEmpty || byName.nonEmpty
+ def isEmpty: Boolean = !nonEmpty
+ }
+
+ protected case class LinkResources(session: Session, inputAddress: String)
+ protected case class StatusState(current: FrontEndConnectionStatus.Status, pending: Option[(FrontEndConnectionStatus.Status, Long)], lastStatusSuccess: Option[Long])
+ protected case class MeasState(
+ pointMap: Map[String, ModelUUID],
+ lastValueMap: MeasLastValueMap,
+ queued: MeasQueue,
+ outstanding: MeasQueue,
+ latestPublishTime: Option[Long],
+ successPendingSince: Option[Long])
+
+ protected sealed trait State
+ protected case class Unlinked(
+ currentStatus: FrontEndConnectionStatus.Status,
+ lastValueMap: MeasLastValueMap,
+ queued: MeasQueue,
+ pointMap: Map[String, ModelUUID]) extends State
+ protected case class Linked(resources: LinkResources, statusState: StatusState, measState: MeasState) extends State
+
+ private case class StackStatusSuccess(results: Seq[FrontEndConnectionStatus]) extends TraceMessage
+ private case class StackStatusFailure(ex: Throwable)
+ private case class StackStatusRequestExecutionFailure(ex: Throwable)
+
+ private case class MeasPublishSuccess(success: Boolean) extends TraceMessage
+ private case class MeasPublishFailure(ex: Throwable)
+ private case class MeasRequestExecutionFailure(ex: Throwable)
+
+ case object DoStackStatusHeartbeat extends TraceMessage
+ private case object DoMeasPublishRetry
+
+ def props(subject: ActorRef, endpoint: Endpoint, statusHeartbeatPeriodMs: Long, lapsedTimeMs: Long, statusRetryPeriodMs: Long, measRetryPeriodMs: Int, measQueueLimit: Int): Props = {
+ Props(classOf[ProcessingProxy], subject, endpoint, statusHeartbeatPeriodMs, lapsedTimeMs, statusRetryPeriodMs, measRetryPeriodMs, measQueueLimit)
+ }
+
+ def toPointMeases(tups: Seq[(ModelUUID, Measurement)]): Seq[PointMeasurementValue] = {
+ tups.map { case (u, m) => PointMeasurementValue.newBuilder().setPointUuid(u).setValue(m).build() }
+ }
+ def toNamedMeases(tups: Seq[(String, Measurement)]): Seq[NamedMeasurementValue] = {
+ tups.map { case (n, m) => NamedMeasurementValue.newBuilder().setPointName(n).setValue(m).build() }
+ }
+
+ def maintainQueueLimit[A](all: Seq[(A, Measurement)], queueLimit: Int): Seq[(A, Measurement)] = {
+
+ val b = new VectorBuilder[(A, Measurement)]
+ var numForKeys: Map[A, Int] = all.groupBy(_._1).mapValues(_.size)
+ var projectedSize = all.size
+
+ // Drop duplicates until we get to our queue limit
+ // We can drop as long as there is at least one (the latest) for every key
+ // Relying on an assumption that there is not unbounded # of keys
+ all.foreach { v =>
+ if (projectedSize > queueLimit) {
+ numForKeys.get(v._1) match {
+ case None => //illegal state
+ case Some(x) if x < 1 => //illegal state
+ case Some(1) =>
+ b += v
+ case Some(num) =>
+ numForKeys = numForKeys.updated(v._1, num - 1)
+ projectedSize -= 1
+ }
+ } else {
+ b += v
+ }
+ }
+
+ b.result()
+ }
+ def maintainQueueLimit(queue: MeasQueue, queueLimit: Int): MeasQueue = {
+ MeasQueue(maintainQueueLimit(queue.byUuid, queueLimit), maintainQueueLimit(queue.byName, queueLimit))
+ }
+
+ def addWallTime(wallTime: Long, m: Measurement): Measurement = {
+ if (m.hasTime) m else m.toBuilder.setTime(wallTime).build()
+ }
+ def addWallTime[A](wallTime: Long, seq: Seq[(A, Measurement)]): Seq[(A, Measurement)] = {
+ seq.map { case (n, m) => (n, addWallTime(wallTime, m)) }
+ }
+ def addWallTime(wallTime: Long, queue: MeasQueue): MeasQueue = {
+ MeasQueue(addWallTime(wallTime, queue.byUuid), addWallTime(wallTime, queue.byName))
+ }
+
+ def simplePrintMeas(m: Measurement): String = {
+ m.getType match {
+ case Measurement.Type.BOOL =>
+ m.getBoolVal + ", " + m.getTime
+ case Measurement.Type.INT =>
+ m.getIntVal + ", " + m.getTime
+ case Measurement.Type.DOUBLE =>
+ m.getDoubleVal + ", " + m.getTime
+ case Measurement.Type.STRING =>
+ m.getStringVal + ", " + m.getTime
+ case _ => "???, " + m.getTime
+ }
+ }
+
+ def simpleIdMeas(tup: (ModelUUID, Measurement)) = tup._1.getValue + ", " + simplePrintMeas(tup._2)
+
+ def mergeQueueAndLastValues(queue: MeasQueue, lastValues: MeasLastValueMap): MeasQueue = {
+ val uuidsInQueue = queue.byUuid.map(_._1).toSet
+ val namesInQueue = queue.byName.map(_._1).toSet
+ val missingUuidValues = lastValues.byUuid.filterKeys(k => !uuidsInQueue.contains(k))
+ val missingNameValues = lastValues.byName.filterKeys(k => !namesInQueue.contains(k))
+
+ val updatedUuidValues = missingUuidValues.map(tup => (tup._1, tup._2.toBuilder.setTime(System.currentTimeMillis()).build())).toSeq
+ val updatedNameValues = missingNameValues.map(tup => (tup._1, tup._2.toBuilder.setTime(System.currentTimeMillis()).build())).toSeq
+
+ queue.enqueue(updatedUuidValues, updatedNameValues)
+ }
+}
+
+class ProcessingProxy(subject: ActorRef, endpoint: Endpoint, statusHeartbeatPeriodMs: Long, lapsedTimeMs: Long, statusRetryPeriodMs: Long, measRetryPeriodMs: Int, measQueueLimit: Int) extends NestedStateMachine with MessageScheduling with Logging {
+ import context.dispatcher
+ import io.greenbus.app.actor.frontend.ProcessingProxy._
+
+ protected type StateType = State
+ protected def start: StateType = Unlinked(FrontEndConnectionStatus.Status.COMMS_DOWN, MeasLastValueMap(), MeasQueue(), Map())
+ override protected def instanceId: Option[String] = Some(endpoint.getName)
+
+ private val heartbeater = context.actorOf(HeartbeatActor.props(self, statusHeartbeatPeriodMs))
+
+ protected def machine = {
+ case state: Unlinked => {
+
+ case LinkUp(session, inputAddress) => {
+ self ! DoStackStatusHeartbeat
+ val updates = mergeQueueAndLastValues(state.queued, state.lastValueMap)
+ val pushOpt = if (updates.nonEmpty) {
+ logger.debug("Frontend bridge for endpoint " + endpoint.getName + " pushing queued measurements")
+ pushMeasurements(session, updates, inputAddress)
+ Some(System.currentTimeMillis())
+ } else {
+ None
+ }
+ Linked(LinkResources(session, inputAddress),
+ StatusState(state.currentStatus, None, None),
+ MeasState(state.pointMap, state.lastValueMap, queued = MeasQueue(), outstanding = updates, pushOpt, pushOpt))
+ }
+
+ case MeasurementsPublished(wallTime, idMeasurements, namedMeasurements) => {
+ val measMap = state.lastValueMap.update(idMeasurements, namedMeasurements)
+ val latestBatch = MeasQueue(idMeasurements, namedMeasurements)
+
+ logger.trace("Frontend bridge for endpoint " + endpoint.getName + " queueing measurements for when link returns: " + (idMeasurements.size + namedMeasurements.size))
+ val updatedQueue = maintainQueueLimit(state.queued.enqueue(addWallTime(wallTime, latestBatch)), measQueueLimit)
+ state.copy(queued = updatedQueue, lastValueMap = measMap)
+ }
+
+ case StackStatusUpdated(status) => {
+ state.copy(currentStatus = status)
+ }
+
+ case PointMapUpdated(map) => {
+ state.copy(pointMap = map)
+ }
+ case DoStackStatusHeartbeat => {
+ state
+ }
+ }
+
+ case state: Linked => {
+
+ case PointMapUpdated(map) => {
+ state.copy(measState = state.measState.copy(pointMap = map))
+ }
+
+ case MeasurementsPublished(wallTime, idMeasurements, namedMeasurements) => {
+ val measMap = state.measState.lastValueMap.update(idMeasurements, namedMeasurements)
+ val latestBatch = MeasQueue(idMeasurements, namedMeasurements)
+
+ val queued = state.measState.queued enqueue addWallTime(wallTime, latestBatch)
+ if (state.measState.outstanding.isEmpty) {
+ logger.trace("Frontend bridge for endpoint " + endpoint.getName + " pushing measurements immediately")
+ pushMeasurements(state.resources.session, queued, state.resources.inputAddress)
+ state.copy(measState = state.measState.copy(outstanding = queued, queued = MeasQueue(), lastValueMap = measMap, latestPublishTime = Some(System.currentTimeMillis()), successPendingSince = Some(System.currentTimeMillis())))
+ } else {
+ logger.trace("Frontend bridge for endpoint " + endpoint.getName + " queueing measurements for when outstanding publish finishes")
+ state.copy(measState = state.measState.copy(queued = queued, lastValueMap = measMap))
+ }
+ }
+
+ case MeasPublishSuccess(success) => {
+ success match {
+ case false =>
+ logger.warn("Frontend bridge for endpoint " + endpoint.getName + " had got false on publish response")
+ state.measState.latestPublishTime match {
+ case None => scheduleMsg(measRetryPeriodMs, DoMeasPublishRetry)
+ case Some(prev) => scheduleRelativeToPrevious(prev, measRetryPeriodMs, DoMeasPublishRetry)
+ }
+ state.copy(measState = state.measState.copy(queued = state.measState.outstanding.enqueue(state.measState.queued), outstanding = MeasQueue()))
+ case true =>
+ if (state.measState.queued.nonEmpty) {
+ logger.trace("Frontend bridge for endpoint " + endpoint.getName + " had publish success, pushing queued measurements")
+ pushMeasurements(state.resources.session, state.measState.queued, state.resources.inputAddress)
+ state.copy(measState = state.measState.copy(outstanding = state.measState.queued, queued = MeasQueue(), latestPublishTime = Some(System.currentTimeMillis()), successPendingSince = Some(System.currentTimeMillis())))
+ } else {
+ logger.trace("Frontend bridge for endpoint " + endpoint.getName + " had publish success")
+ state.copy(measState = state.measState.copy(queued = MeasQueue(), outstanding = MeasQueue(), successPendingSince = None))
+ }
+ }
+ }
+
+ case MeasPublishFailure(ex) => {
+ logger.warn("Frontend bridge for endpoint " + endpoint.getName + " had error publishing measurements")
+ val requeued = state.measState.outstanding enqueue state.measState.queued
+
+ ex match {
+ case ex: SessionUnusableException =>
+ subject ! ServiceSessionDead
+ Unlinked(state.statusState.current, state.measState.lastValueMap, requeued, state.measState.pointMap)
+ case ex: UnauthorizedException =>
+ subject ! ServiceSessionDead
+ Unlinked(state.statusState.current, state.measState.lastValueMap, requeued, state.measState.pointMap)
+ case ex: Throwable => {
+ if (state.measState.successPendingSince.exists(last => System.currentTimeMillis() - last > lapsedTimeMs)) {
+ logger.info(s"Link for endpoint ${endpoint.getName} lapsed due to measurement publishing timeouts")
+ logger.trace(s"Dropping link for endpoint ${endpoint.getName} with the current measurement state: " + requeued)
+ subject ! LinkLapsed
+ Unlinked(state.statusState.current, state.measState.lastValueMap, requeued, state.measState.pointMap)
+ } else {
+ state.measState.latestPublishTime match {
+ case None => scheduleMsg(measRetryPeriodMs, DoMeasPublishRetry)
+ case Some(prev) => scheduleRelativeToPrevious(prev, measRetryPeriodMs, DoMeasPublishRetry)
+ }
+ state.copy(measState = state.measState.copy(queued = requeued, outstanding = MeasQueue()))
+ }
+ }
+ }
+ }
+
+ case MeasRequestExecutionFailure(ex) => {
+ logger.warn("Frontend bridge for endpoint " + endpoint.getName + " had request execution error publishing measurements")
+ val requeued = state.measState.outstanding enqueue state.measState.queued
+ subject ! ServiceSessionDead
+ Unlinked(state.statusState.current, state.measState.lastValueMap, requeued, state.measState.pointMap)
+ }
+
+ case DoMeasPublishRetry => {
+ if (state.measState.outstanding.isEmpty && state.measState.queued.nonEmpty) {
+ pushMeasurements(state.resources.session, state.measState.queued, state.resources.inputAddress)
+ state.copy(measState = state.measState.copy(outstanding = state.measState.queued, queued = MeasQueue(), latestPublishTime = Some(System.currentTimeMillis())))
+ } else {
+ state
+ }
+ }
+
+ case StackStatusUpdated(status) => {
+ logger.debug("Frontend bridge for endpoint " + endpoint.getName + " saw comms status update: " + status)
+
+ state.statusState.pending match {
+ case None =>
+ pushStatusUpdate(state.resources.session, endpoint, status, state.resources.inputAddress)
+ state.copy(statusState = state.statusState.copy(current = status, pending = Some(status, System.currentTimeMillis())))
+ case Some(_) =>
+ state.copy(statusState = state.statusState.copy(current = status))
+ }
+ }
+
+ case StackStatusSuccess(results) => {
+ logger.trace("Frontend bridge for endpoint " + endpoint.getName + " successfully published stack status heartbeat")
+
+ val now = System.currentTimeMillis()
+ state.statusState.pending match {
+ case None =>
+ logger.warn(s"Got stack status success for endpoint ${endpoint.getName} with no pending status")
+ state.copy(statusState = state.statusState.copy(lastStatusSuccess = Some(now)))
+ case Some((previousStatus, sentTime)) => {
+ if (state.statusState.current != previousStatus) {
+ pushStatusUpdate(state.resources.session, endpoint, state.statusState.current, state.resources.inputAddress)
+ state.copy(statusState = state.statusState.copy(pending = Some(state.statusState.current, now), lastStatusSuccess = Some(now)))
+ } else {
+ state.copy(statusState = state.statusState.copy(pending = None, lastStatusSuccess = Some(now)))
+ }
+ }
+ }
+ }
+
+ // TODO: handle case where outstanding meas publish succeeds, thus eventually causing duplicate measurements
+ case StackStatusFailure(ex) => {
+ logger.warn("Frontend bridge for endpoint " + endpoint.getName + " had error publishing stack status heartbeat")
+ ex match {
+ case ex: SessionUnusableException =>
+ subject ! ServiceSessionDead
+ Unlinked(state.statusState.current, state.measState.lastValueMap, state.measState.outstanding enqueue state.measState.queued, state.measState.pointMap)
+ case ex: UnauthorizedException =>
+ subject ! ServiceSessionDead
+ Unlinked(state.statusState.current, state.measState.lastValueMap, state.measState.outstanding enqueue state.measState.queued, state.measState.pointMap)
+ case ex: Throwable => {
+ if (state.statusState.lastStatusSuccess.exists(last => System.currentTimeMillis() - last > lapsedTimeMs)) {
+ logger.info(s"Link for endpoint ${endpoint.getName} lapsed due to heartbeat publishing timeouts")
+ logger.trace(s"Dropping link for endpoint ${endpoint.getName} with the current measurement state: " + (state.measState.outstanding enqueue state.measState.queued))
+ subject ! LinkLapsed
+ Unlinked(state.statusState.current, state.measState.lastValueMap, state.measState.outstanding enqueue state.measState.queued, state.measState.pointMap)
+ } else {
+ state.statusState.pending match {
+ case None =>
+ logger.warn(s"Got stack status failure for endpoint ${endpoint.getName} with no pending status")
+ state
+ case Some((previousStatus, sentTime)) => {
+ scheduleRelativeToPrevious(sentTime, statusRetryPeriodMs, DoStackStatusHeartbeat)
+ state.copy(statusState = state.statusState.copy(pending = None))
+ }
+ }
+ }
+ }
+ }
+ }
+
+ case StackStatusRequestExecutionFailure(ex) => {
+ logger.warn("Frontend bridge for endpoint " + endpoint.getName + " had request execution error publishing heartbeat")
+ val requeued = state.measState.outstanding enqueue state.measState.queued
+ subject ! ServiceSessionDead
+ Unlinked(state.statusState.current, state.measState.lastValueMap, requeued, state.measState.pointMap)
+ }
+
+ case DoStackStatusHeartbeat => {
+ state.statusState.pending match {
+ case None =>
+ pushStatusUpdate(state.resources.session, endpoint, state.statusState.current, state.resources.inputAddress)
+ state.copy(statusState = state.statusState.copy(pending = Some(state.statusState.current, System.currentTimeMillis())))
+ case Some(_) => state
+ }
+ }
+ }
+ }
+
+ private def resolveMeases(namedMeasurements: Seq[(String, Measurement)], pointMap: Map[String, ModelUUID]): Seq[(ModelUUID, Measurement)] = {
+ namedMeasurements.flatMap {
+ case (n, m) => pointMap.get(n) match {
+ case None =>
+ logger.warn("Unrecognized point name for measurement: " + n)
+ None
+ case Some(uuid) => Some((uuid, m))
+ }
+ }
+ }
+
+ private def scheduleRelativeToPrevious(previousTime: Long, offset: Long, msg: AnyRef): Unit = {
+ val shiftedFromSentTime = (previousTime + offset) - System.currentTimeMillis()
+ if (shiftedFromSentTime > 0) {
+ scheduleMsg(shiftedFromSentTime, msg)
+ } else {
+ self ! msg
+ }
+ }
+
+ private def pushMeasurements(session: Session, queued: MeasQueue, address: String): Unit = {
+
+ val pointMeasurements = toPointMeases(queued.byUuid)
+ val namedMeasurements = toNamedMeases(queued.byName)
+
+ logger.trace("Frontend bridge for endpoint " + endpoint.getName + " saw measurement updates: " + (pointMeasurements.size + namedMeasurements.size))
+ val measClient = MeasurementService.client(session)
+
+ val batch = MeasurementBatch.newBuilder()
+ .addAllPointMeasurements(pointMeasurements)
+ .addAllNamedMeasurements(namedMeasurements)
+ .build()
+
+ try {
+ val postFut = measClient.postMeasurements(batch, address)
+ postFut.onSuccess { case result => self ! MeasPublishSuccess(result) }
+ postFut.onFailure { case ex => self ! MeasPublishFailure(ex) }
+ } catch {
+ case ex: Throwable =>
+ self ! MeasRequestExecutionFailure(ex)
+ }
+ }
+
+ private def pushStatusUpdate(session: Session, endpoint: Endpoint, currentStatus: FrontEndConnectionStatus.Status, address: String): Unit = {
+ logger.trace(s"Frontend bridge for endpoint ${endpoint.getName} pushing status: $currentStatus")
+ val frontEndClient = FrontEndService.client(session)
+
+ val update = FrontEndStatusUpdate.newBuilder()
+ .setEndpointUuid(endpoint.getUuid)
+ .setStatus(currentStatus)
+ .build()
+
+ try {
+ val putFut = frontEndClient.putFrontEndConnectionStatuses(List(update), address)
+
+ putFut.onSuccess { case results => self ! StackStatusSuccess(results) }
+ putFut.onFailure { case ex: Throwable => self ! StackStatusFailure(ex) }
+ } catch {
+ case ex: Throwable =>
+ self ! StackStatusRequestExecutionFailure(ex)
+ }
+ }
+
+}
+
+object HeartbeatActor {
+ case object Heartbeat
+
+ def props(subject: ActorRef, intervalMs: Long): Props = {
+ Props(classOf[HeartbeatActor], subject, intervalMs)
+ }
+}
+class HeartbeatActor(subject: ActorRef, intervalMs: Long) extends Actor with MessageScheduling with Logging {
+ import HeartbeatActor._
+
+ self ! Heartbeat
+ def receive = {
+ case Heartbeat =>
+ subject ! DoStackStatusHeartbeat
+ scheduleMsg(intervalMs, Heartbeat)
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/frontend/ProtocolCommandAcceptor.scala b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/ProtocolCommandAcceptor.scala
new file mode 100644
index 0000000..123f090
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/ProtocolCommandAcceptor.scala
@@ -0,0 +1,26 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.frontend
+
+import io.greenbus.client.service.proto.Commands
+import scala.concurrent.Future
+
+trait ProtocolCommandAcceptor {
+ def issue(commandName: String, request: Commands.CommandRequest): Future[Commands.CommandResult]
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/frontend/SimpleCommandProxy.scala b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/SimpleCommandProxy.scala
new file mode 100644
index 0000000..0656ee9
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/SimpleCommandProxy.scala
@@ -0,0 +1,91 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.frontend
+
+import akka.actor.{ Actor, ActorRef, Props, _ }
+import akka.pattern.ask
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.msg.SubscriptionBinding
+import io.greenbus.app.actor.frontend.CommandProxy.CommandSubscriptionRetrieved
+import io.greenbus.client.service.proto.Commands.{ CommandRequest, CommandResult, CommandStatus }
+import io.greenbus.client.service.proto.Model.Endpoint
+
+import scala.concurrent.Future
+import scala.concurrent.duration._
+
+object SimpleCommandProxy {
+
+ case class ProtocolInitialized(subject: ActorRef)
+ case object ProtocolUninitialized
+
+ private case class ReceivedCommandIssued(req: CommandRequest, response: CommandResult => Unit)
+
+ def props(endpoint: Endpoint): Props = Props(classOf[SimpleCommandProxy], endpoint)
+}
+
+class SimpleCommandProxy(endpoint: Endpoint) extends Actor with Logging {
+ import io.greenbus.app.actor.frontend.SimpleCommandProxy._
+ import context.dispatcher
+
+ private var commandSub = Option.empty[SubscriptionBinding]
+ private var subjectOpt = Option.empty[ActorRef]
+
+ def receive = {
+ case ReceivedCommandIssued(req, responseHandler) => {
+ subjectOpt match {
+ case None =>
+ logger.debug(s"Endpoint ${endpoint.getName} saw command request while protocol uninitialized: " + req.getCommandUuid.getValue)
+ responseHandler(CommandResult.newBuilder().setStatus(CommandStatus.TIMEOUT).build())
+ case Some(subject) =>
+ logger.debug(s"Endpoint ${endpoint.getName} handling command request")
+
+ val askFut = subject.ask(req)(5000.milliseconds)
+
+ val resultFut = askFut.flatMap {
+ case fut: Future[_] => fut.asInstanceOf[Future[CommandResult]]
+ case all =>
+ logger.error("Endpoint " + endpoint.getName + " received unknown response from protocol master actor: " + all)
+ Future.successful(CommandResult.newBuilder().setStatus(CommandStatus.UNDEFINED).build())
+ }
+
+ resultFut.onSuccess {
+ case result: CommandResult =>
+ logger.debug(s"Endpoint ${endpoint.getName} saw command result: " + result.getStatus)
+ responseHandler(result)
+ }
+ resultFut.onFailure {
+ case ex: Throwable =>
+ logger.error(s"Command acceptor for endpoint ${endpoint.getName} failed: " + ex)
+ responseHandler(CommandResult.newBuilder().setStatus(CommandStatus.UNDEFINED).build())
+ }
+ }
+ }
+
+ case CommandSubscriptionRetrieved(sub) => {
+ commandSub.foreach(_.cancel())
+ commandSub = Some(sub)
+ val cmdDelegate = new DelegatingCommandHandler((req, resultAccept) => self ! ReceivedCommandIssued(req, resultAccept))
+ sub.start(cmdDelegate)
+ }
+
+ case ProtocolInitialized(subject) => subjectOpt = Some(subject)
+
+ case ProtocolUninitialized => subjectOpt = None
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/frontend/json/JsonFrontendConfiguration.scala b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/json/JsonFrontendConfiguration.scala
new file mode 100644
index 0000000..330a037
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/json/JsonFrontendConfiguration.scala
@@ -0,0 +1,42 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.frontend.json
+
+import io.greenbus.app.actor.json.JsonAmqpConfig
+
+object JsonFrontendConfiguration {
+ import play.api.libs.json._
+ implicit val writer = Json.writes[JsonFrontendConfiguration]
+ implicit val reader = Json.reads[JsonFrontendConfiguration]
+
+ def load(input: Array[Byte]): Option[JsonFrontendConfiguration] = {
+
+ val jsObj = Json.parse(input)
+
+ Json.fromJson(jsObj)(JsonFrontendConfiguration.reader).asOpt
+ }
+}
+
+case class JsonFrontendConfiguration(
+ amqpConfig: Option[JsonAmqpConfig],
+ userConfigPath: Option[String],
+ endpointWhitelist: Option[Seq[String]],
+ nodeId: Option[String],
+ registrationConfig: Option[JsonFrontendRegistrationConfig])
+
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/frontend/json/JsonFrontendRegistrationConfig.scala b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/json/JsonFrontendRegistrationConfig.scala
new file mode 100644
index 0000000..e3b5c6b
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/frontend/json/JsonFrontendRegistrationConfig.scala
@@ -0,0 +1,59 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.frontend.json
+
+import io.greenbus.app.actor.frontend.FrontendRegistrationConfig
+
+case class JsonFrontendRegistrationConfig(
+ loginRetryMs: Option[Long],
+ registrationRetryMs: Option[Long],
+ releaseTimeoutMs: Option[Long],
+ statusHeartbeatPeriodMs: Option[Long],
+ lapsedTimeMs: Option[Long],
+ statusRetryPeriodMs: Option[Long],
+ measRetryPeriodMs: Option[Int],
+ measQueueLimit: Option[Int],
+ configRequestRetryMs: Option[Int])
+
+object JsonFrontendRegistrationConfig {
+ import play.api.libs.json._
+ implicit val writer = Json.writes[JsonFrontendRegistrationConfig]
+ implicit val reader = Json.reads[JsonFrontendRegistrationConfig]
+
+ def load(input: Array[Byte]): Option[JsonFrontendConfiguration] = {
+
+ val jsObj = Json.parse(input)
+
+ Json.fromJson(jsObj)(JsonFrontendConfiguration.reader).asOpt
+ }
+
+ def read(json: JsonFrontendRegistrationConfig): FrontendRegistrationConfig = {
+ val default = FrontendRegistrationConfig.defaults
+ FrontendRegistrationConfig(
+ loginRetryMs = json.loginRetryMs.getOrElse(default.loginRetryMs),
+ registrationRetryMs = json.registrationRetryMs.getOrElse(default.registrationRetryMs),
+ releaseTimeoutMs = json.releaseTimeoutMs.getOrElse(default.releaseTimeoutMs),
+ statusHeartbeatPeriodMs = json.statusHeartbeatPeriodMs.getOrElse(default.statusHeartbeatPeriodMs),
+ lapsedTimeMs = json.lapsedTimeMs.getOrElse(default.lapsedTimeMs),
+ statusRetryPeriodMs = json.statusRetryPeriodMs.getOrElse(default.statusRetryPeriodMs),
+ measRetryPeriodMs = json.measRetryPeriodMs.getOrElse(default.measRetryPeriodMs),
+ measQueueLimit = json.measQueueLimit.getOrElse(default.measQueueLimit),
+ configRequestRetryMs = json.configRequestRetryMs.getOrElse(default.configRequestRetryMs))
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/json/JsonAmqpConfig.scala b/app-framework/src/main/scala/io/greenbus/app/actor/json/JsonAmqpConfig.scala
new file mode 100644
index 0000000..8858c20
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/json/JsonAmqpConfig.scala
@@ -0,0 +1,50 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.json
+
+import io.greenbus.app.actor.AmqpConnectionConfig
+
+case class JsonAmqpConfig(amqpConfigFileList: Option[IndexedSeq[String]],
+ failureLimit: Option[Int],
+ retryDelayMs: Option[Long],
+ connectionTimeoutMs: Option[Long])
+
+object JsonAmqpConfig {
+ import play.api.libs.json._
+ implicit val writer = Json.writes[JsonAmqpConfig]
+ implicit val reader = Json.reads[JsonAmqpConfig]
+
+ def load(input: Array[Byte]): Option[JsonAmqpConfig] = {
+
+ val jsObj = Json.parse(input)
+
+ Json.fromJson(jsObj)(JsonAmqpConfig.reader).asOpt
+ }
+
+ def read(json: JsonAmqpConfig, defaultPaths: Seq[String]): AmqpConnectionConfig = {
+
+ val default = AmqpConnectionConfig.default(defaultPaths)
+
+ AmqpConnectionConfig(
+ json.amqpConfigFileList.getOrElse(default.amqpConfigFileList),
+ failureLimit = json.failureLimit.getOrElse(default.failureLimit),
+ retryDelayMs = json.retryDelayMs.getOrElse(default.retryDelayMs),
+ connectionTimeoutMs = json.connectionTimeoutMs.getOrElse(default.connectionTimeoutMs))
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/app/actor/util/StateMachine.scala b/app-framework/src/main/scala/io/greenbus/app/actor/util/StateMachine.scala
new file mode 100644
index 0000000..1dd44c1
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/app/actor/util/StateMachine.scala
@@ -0,0 +1,113 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.util
+
+import akka.actor.Actor
+import com.typesafe.scalalogging.slf4j.Logging
+
+trait StateMachine extends Actor {
+ protected type StateType
+ protected def start: StateType
+ private var current: StateType = start
+
+ protected def machine: PartialFunction[(Any, StateType), StateType]
+
+ protected def unhandled(obj: Any, state: StateType)
+
+ def receive = {
+ case obj =>
+ if (machine.isDefinedAt((obj, current))) {
+ current = machine.apply((obj, current))
+ } else {
+ unhandled(obj, current)
+ }
+ }
+
+}
+
+trait TraceMessage
+
+object NestedStateMachine {
+
+ def stateToString(state: Any): String = {
+ def prod(pr: Product) = pr.productIterator.map(_.getClass.getSimpleName).mkString("(", ", ", ")")
+ state match {
+ case pr: Tuple2[_, _] => prod(pr)
+ case pr: Tuple3[_, _, _] => prod(pr)
+ case pr: Tuple4[_, _, _, _] => prod(pr)
+ case pr: Tuple5[_, _, _, _, _] => prod(pr)
+ case o => o.getClass.getSimpleName
+ }
+ }
+}
+trait NestedStateMachine extends Actor with Logging {
+ import NestedStateMachine._
+
+ protected def instanceId: Option[String] = None
+ private def instancePrefix: String = instanceId.map(_ + ": ").getOrElse("")
+ protected type StateType
+ protected def start: StateType
+ private var current: StateType = start
+
+ protected def machine: PartialFunction[StateType, PartialFunction[Any, StateType]]
+
+ protected def unhandled(obj: Any, state: StateType): Unit = {
+ logger.warn(instancePrefix + "Saw unhandled event " + obj.getClass.getSimpleName + " in state " + stateToString(state))
+ }
+
+ final override def postStop(): Unit = {
+ onShutdown(current)
+ }
+
+ protected def onShutdown(state: StateType): Unit = {
+ logger.debug(instancePrefix + s"Shut down in state ${stateToString(current)} ($instanceId)")
+ }
+
+ def receive = {
+ case obj =>
+ obj match {
+ case _: TraceMessage => if (logger.underlying.isTraceEnabled) {
+ logger.trace(instancePrefix + s"Event ${obj.getClass.getSimpleName} in state ${stateToString(current)}")
+ }
+ case _ => if (logger.underlying.isDebugEnabled) {
+ logger.debug(instancePrefix + s"Event ${obj.getClass.getSimpleName} in state ${stateToString(current)}")
+ }
+ }
+ if (machine.isDefinedAt(current)) {
+ val handler = machine.apply(current)
+ if (handler.isDefinedAt(obj)) {
+ val result = handler.apply(obj)
+
+ if (logger.underlying.isDebugEnabled) {
+ val resultStr = stateToString(result)
+ val currentStr = stateToString(current)
+ if (resultStr != currentStr) {
+ logger.debug(instancePrefix + s"State change $currentStr to $resultStr")
+ }
+ }
+ current = result
+ } else {
+ unhandled(obj, current)
+ }
+ } else {
+ unhandled(obj, current)
+ }
+ }
+
+}
diff --git a/app-framework/src/main/scala/io/greenbus/japi/frontend/BusDrivenFrontendProtocolManager.java b/app-framework/src/main/scala/io/greenbus/japi/frontend/BusDrivenFrontendProtocolManager.java
new file mode 100644
index 0000000..511a72d
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/japi/frontend/BusDrivenFrontendProtocolManager.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.japi.frontend;
+
+import io.greenbus.app.actor.AmqpConnectionConfig;
+import io.greenbus.app.actor.EndpointCollectionStrategy;
+import io.greenbus.app.actor.frontend.FrontendRegistrationConfig;
+import io.greenbus.japi.frontend.impl.BusDrivenFrontendProtocolManagerImpl;
+
+// Wrapped to keep as many Java-accessible classes in JavaDoc as possible
+
+/**
+ * Hosts a set of front-end connections. Is responsible for establishing the connection
+ * to the services, requesting and subscribing to the relevant parts of the system model,
+ * and notifying protocol implementations of Endpoints they should provide service for.
+ */
+public class BusDrivenFrontendProtocolManager {
+
+ private final BusDrivenFrontendProtocolManagerImpl impl;
+
+ /**
+ * @param processName Name of the process for logging purposes.
+ * @param amqpConfig Configuration for AMQP, with failover.
+ * @param userConfigPath Path to find the user configuration.
+ * @param registrationConfig Configuration parameters for front-end registration process.
+ * @param nodeId Node ID used to re-register front-end immediately on restart. Set to random UUID as a default.
+ * @param endpointStrategy Strategy for the subset of Endpoints the protocol implementation should provide services for.
+ * @param factory Factory for bus-driven protocols.
+ */
+ public BusDrivenFrontendProtocolManager(
+ String processName,
+ AmqpConnectionConfig amqpConfig,
+ String userConfigPath,
+ FrontendRegistrationConfig registrationConfig,
+ String nodeId,
+ EndpointCollectionStrategy endpointStrategy,
+ BusDrivenProtocolFactory factory) {
+
+ this.impl = new BusDrivenFrontendProtocolManagerImpl(processName, amqpConfig, userConfigPath, registrationConfig, nodeId, endpointStrategy, factory);
+ }
+
+ /**
+ *
+ */
+ public void start() {
+ impl.start();
+ }
+
+ /**
+ * Notifies all protocol implementations they should stop, and closes the connection to the services.
+ */
+ public void shutdown() {
+ impl.shutdown();
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/japi/frontend/BusDrivenProtocol.java b/app-framework/src/main/scala/io/greenbus/japi/frontend/BusDrivenProtocol.java
new file mode 100644
index 0000000..39c8e1c
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/japi/frontend/BusDrivenProtocol.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.japi.frontend;
+
+import com.google.common.util.concurrent.SettableFuture;
+import io.greenbus.client.service.proto.Commands;
+
+/**
+ * Represents the protocol framework library's interface to a bus-driven protocol implementation.
+ */
+public interface BusDrivenProtocol {
+
+ /**
+ * Callback for the protocol implementation to handle command requests.
+ *
+ * @param commandRequest Command request to be handled.
+ * @param promise Promise of a CommandResult, used to asynchronously notify the library that the request has been handled.
+ */
+ void handleCommandRequest(Commands.CommandRequest commandRequest, SettableFuture promise);
+
+ /**
+ * Callback for the protocol implementation to clean up when connection to the bus has been lost.
+ */
+ void onClose();
+}
diff --git a/app-framework/src/main/scala/io/greenbus/japi/frontend/BusDrivenProtocolFactory.java b/app-framework/src/main/scala/io/greenbus/japi/frontend/BusDrivenProtocolFactory.java
new file mode 100644
index 0000000..95ccff6
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/japi/frontend/BusDrivenProtocolFactory.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.japi.frontend;
+
+import io.greenbus.client.service.proto.Model;
+import io.greenbus.msg.japi.Session;
+
+/**
+ * Factory handed to protocol registration library to instantiate protocol implementations upon registration.
+ */
+public interface BusDrivenProtocolFactory {
+
+ /**
+ * Used to instantiate protocol implementation.
+ *
+ * @param endpoint Endpoint the protocol implementation is serving.
+ * @param session Logged-in session from the protocol registration library.
+ * @param protocolUpdater Interface for publishg measurements and updating frontend communication status.
+ * @return Handle for protocol management.
+ */
+ BusDrivenProtocol initialize(Model.Endpoint endpoint, Session session, ProtocolUpdater protocolUpdater);
+}
diff --git a/app-framework/src/main/scala/io/greenbus/japi/frontend/EndpointCollectionStrategyFactory.java b/app-framework/src/main/scala/io/greenbus/japi/frontend/EndpointCollectionStrategyFactory.java
new file mode 100644
index 0000000..6f14978
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/japi/frontend/EndpointCollectionStrategyFactory.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.japi.frontend;
+
+import io.greenbus.app.actor.AllEndpointsStrategy;
+import io.greenbus.app.actor.EndpointCollectionStrategy;
+import io.greenbus.app.actor.ProtocolsEndpointStrategy;
+import scala.Option;
+import scala.collection.JavaConversions;
+
+import java.util.List;
+
+/**
+ * Factory for strategies that determine what subset of Endpoints the management library retrieves
+ * configuration for.
+ */
+public class EndpointCollectionStrategyFactory {
+
+ private EndpointCollectionStrategyFactory() {
+ }
+
+ /**
+ * Strategy for all Endpoints in the system.
+ *
+ * @param endpointNames Names of Endpoints that are whitelisted. Null to allow all Endpoints.
+ * @return Endpoint collection strategy
+ */
+ public static EndpointCollectionStrategy anyAndAll(List endpointNames) {
+ return new AllEndpointsStrategy(convertToOptionalSet(endpointNames));
+ }
+
+ /**
+ * Strategy for only Endpoints with one of a set of specified protocols.
+ *
+ * @param protocolNames Names of Endpoint protocols
+ * @param endpointNames Names of Endpoints that are whitelisted. Null to allow all Endpoints.
+ * @return Endpoint collection strategy
+ */
+ public static EndpointCollectionStrategy protocolStrategy(List protocolNames, List endpointNames) {
+ scala.collection.Iterable stringIterable = JavaConversions.asScalaIterable(protocolNames);
+ scala.collection.immutable.Set set = stringIterable.toSet();
+ return new ProtocolsEndpointStrategy(set, convertToOptionalSet(endpointNames));
+ }
+
+
+ private static scala.Option> convertToOptionalSet(List objs) {
+
+ if (objs != null) {
+
+ scala.collection.Iterable stringIterable = JavaConversions.asScalaIterable(objs);
+ scala.collection.immutable.Set set = stringIterable.toSet();
+ return Option.apply(set);
+
+ } else {
+
+ scala.collection.immutable.Set nullvar = null;
+ return Option.apply(nullvar);
+ }
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/japi/frontend/FrontendConfiguration.java b/app-framework/src/main/scala/io/greenbus/japi/frontend/FrontendConfiguration.java
new file mode 100644
index 0000000..e9be939
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/japi/frontend/FrontendConfiguration.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.japi.frontend;
+
+import io.greenbus.app.actor.frontend.FepConfigLoader;
+import io.greenbus.app.actor.frontend.FrontendProcessConfig;
+import scala.collection.JavaConversions;
+
+import java.util.Set;
+
+/**
+ * Helper class for loading frontend process configuration from JSON, with fallbacks and default settings.
+ */
+public class FrontendConfiguration {
+
+ /**
+ *
+ * @param jsonConfigPath Path of frontend JSON configuration format.
+ * @param defaultAmqpConfigPath Path of an AMQP configuration file if defaults are to be used.
+ * @param defaultUserConfigPath Path of a user configuration file if defaults are to be used.
+ * @param protocols Set of protocols the protocol implementation provides for.
+ * @return Configuration structure for protocol frontends.
+ */
+ public static FrontendProcessConfig load(String jsonConfigPath, String defaultAmqpConfigPath, String defaultUserConfigPath, Set protocols) {
+ return FepConfigLoader.loadConfig(jsonConfigPath, defaultAmqpConfigPath, defaultUserConfigPath, JavaConversions.asScalaSet(protocols).toSet());
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/japi/frontend/FrontendProtocolManager.java b/app-framework/src/main/scala/io/greenbus/japi/frontend/FrontendProtocolManager.java
new file mode 100644
index 0000000..fbd9060
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/japi/frontend/FrontendProtocolManager.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.japi.frontend;
+
+import io.greenbus.app.actor.EndpointCollectionStrategy;
+import io.greenbus.japi.frontend.impl.FrontendProtocolManagerImpl;
+
+import java.util.List;
+
+// Wrapped to keep as many Java-accessible classes in JavaDoc as possible
+
+/**
+ * Hosts a set of front-end connections. Is responsible for establishing the connection
+ * to the services, requesting and subscribing to the relevant parts of the system model,
+ * and notifying protocol implementations of Endpoints they should provide service for.
+ *
+ * @param
+ */
+public class FrontendProtocolManager {
+
+ private final FrontendProtocolManagerImpl impl;
+
+ /**
+ *
+ * @param protocol The protocol implementation.
+ * @param protocolConfigurer Used by the library to determine whether a valid configuration exists.
+ * @param endpointStrategy Strategy for the subset of Endpoints the protocol implementation should provide services for.
+ * @param amqpConfigPath Path to find the AMQP connection configuration.
+ * @param userConfigPath Path to find the user configuration.
+ */
+ public FrontendProtocolManager(
+ MasterProtocol protocol,
+ ProtocolConfigurer protocolConfigurer,
+ List configKeys,
+ EndpointCollectionStrategy endpointStrategy,
+ String amqpConfigPath,
+ String userConfigPath) {
+
+ this.impl = new FrontendProtocolManagerImpl(protocol, protocolConfigurer, configKeys, endpointStrategy, amqpConfigPath, userConfigPath);
+ }
+
+ /**
+ *
+ */
+ public void start() {
+ impl.start();
+ }
+
+ /**
+ * Notifies all protocol implementations they should stop, and closes the connection to the services.
+ */
+ public void shutdown() {
+ impl.shutdown();
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/japi/frontend/MasterProtocol.java b/app-framework/src/main/scala/io/greenbus/japi/frontend/MasterProtocol.java
new file mode 100644
index 0000000..3d8275a
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/japi/frontend/MasterProtocol.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.japi.frontend;
+
+import io.greenbus.client.service.proto.Model;
+
+/**
+ *
+ * Interface for a protocol manager.
+ *
+ * The underlying library uses add() and remove() callbacks to notify the user that service for
+ * Endpoints is required.
+ *
+ * @param Type of the assembled configuration for the protocol.
+ */
+public interface MasterProtocol {
+
+ /**
+ * Called by the library to notify user code that service is requested for a particular Endpoint.
+ *
+ * @param endpoint Endpoint the front-end connection is for.
+ * @param protocolConfig The assembled configuration for the protocol.
+ * @param updater An interface for the front-end to notify the library of measurement and status updates.
+ * @return An interface for the underlying library to notify the front-end of command requests.
+ */
+ ProtocolCommandAcceptor add(Model.Endpoint endpoint, ProtocolConfig protocolConfig, ProtocolUpdater updater);
+
+ /**
+ * Called by the library to notify user code that service should no longer be provided for a particular Endpoint.
+ *
+ * @param endpointUuid UUID of the Endpoint service should no longer be provided for.
+ */
+ void remove(Model.ModelUUID endpointUuid);
+
+ /**
+ * Called when the entire system is shutting down. All front-end connections should be closed and cleaned up.
+ */
+ void shutdown();
+}
diff --git a/app-framework/src/main/scala/io/greenbus/japi/frontend/NamedMeasurement.java b/app-framework/src/main/scala/io/greenbus/japi/frontend/NamedMeasurement.java
new file mode 100644
index 0000000..5d9c202
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/japi/frontend/NamedMeasurement.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.japi.frontend;
+
+import io.greenbus.client.service.proto.Measurements;
+
+/**
+ * Structure that holds a measurement value and the name of the Point it applies to.
+ */
+public class NamedMeasurement {
+ final private String name;
+ final private Measurements.Measurement value;
+
+ public NamedMeasurement(String name, Measurements.Measurement value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ /**
+ * Gets the name of the Point associated with the Measurement.
+ *
+ * @return The name of the Point.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Gets the Measurement value.
+ *
+ * @return The Measurement value.
+ */
+ public Measurements.Measurement getValue() {
+ return value;
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/japi/frontend/ProtocolCommandAcceptor.java b/app-framework/src/main/scala/io/greenbus/japi/frontend/ProtocolCommandAcceptor.java
new file mode 100644
index 0000000..2a9fda7
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/japi/frontend/ProtocolCommandAcceptor.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.japi.frontend;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import io.greenbus.client.service.proto.Commands;
+
+/**
+ * Handle for the manager to notify protocol implementations of command requests.
+ */
+public interface ProtocolCommandAcceptor {
+
+ /**
+ * Notifies the protocol implementation of command requests.
+ *
+ * @param commandName Name of the Command the request applies to.
+ * @param request The CommandRequest object the protocol implementation should handle.
+ * @return A future containing the result of handling the command request.
+ */
+ ListenableFuture issue(String commandName, Commands.CommandRequest request);
+}
diff --git a/app-framework/src/main/scala/io/greenbus/japi/frontend/ProtocolConfigurer.java b/app-framework/src/main/scala/io/greenbus/japi/frontend/ProtocolConfigurer.java
new file mode 100644
index 0000000..b183219
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/japi/frontend/ProtocolConfigurer.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.japi.frontend;
+
+import io.greenbus.client.service.proto.Model;
+
+import java.util.List;
+
+/**
+ * Used by the library to manage the configuration for an Endpoint's front-end connection.
+ *
+ * @param Type of the assembled configuration for the protocol.
+ */
+public interface ProtocolConfigurer {
+
+ /**
+ * Provided by a protocol implementation to transform the list of ConfigFiles to the
+ * protocol-specific form of configuration.
+ *
+ * Should return null if the configuration is missing or can't be parsed.
+ *
+ * @param endpoint Endpoint the configuration is for.
+ * @param configFiles The ConfigFiles associated with the Endpoint.
+ * @return The extracted configuration, of null if not found.
+ */
+ ProtocolConfig evaluate(Model.Endpoint endpoint, List configFiles);
+
+ /**
+ * Provided by a protocol implementation to inform the library if two instances of the
+ * protocol configuration are equivalent or represent an update that requires re-initialization.
+ *
+ * @param latest The latest instance received by the library.
+ * @param previous The previous instance delivered to the protocol implementation.
+ * @return true if the front-end does not need to be re-initialized, false if it does.
+ */
+ boolean equivalent(ProtocolConfig latest, ProtocolConfig previous);
+}
diff --git a/app-framework/src/main/scala/io/greenbus/japi/frontend/ProtocolUpdater.java b/app-framework/src/main/scala/io/greenbus/japi/frontend/ProtocolUpdater.java
new file mode 100644
index 0000000..ca205bf
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/japi/frontend/ProtocolUpdater.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.japi.frontend;
+
+import io.greenbus.client.service.proto.FrontEnd;
+
+import java.util.List;
+
+/**
+ * A handle used by the protocol implementation to notify the library of measurement updates and communication
+ * status updates.
+ */
+public interface ProtocolUpdater {
+
+ /**
+ * Publishes measurement updates.
+ *
+ * @param wallTime System time that will be used in measurements that do not contain their own specific time.
+ * @param updates List of Point name and measurement pairs that represent the updates.
+ */
+ void publish(long wallTime, List updates);
+
+ /**
+ * Publishes updates to the connection status.
+ *
+ * @param status The status the connection should be.
+ */
+ void updateStatus(FrontEnd.FrontEndConnectionStatus.Status status);
+}
diff --git a/app-framework/src/main/scala/io/greenbus/japi/frontend/impl/BusDrivenFrontendProtocolManagerImpl.scala b/app-framework/src/main/scala/io/greenbus/japi/frontend/impl/BusDrivenFrontendProtocolManagerImpl.scala
new file mode 100644
index 0000000..290c5fd
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/japi/frontend/impl/BusDrivenFrontendProtocolManagerImpl.scala
@@ -0,0 +1,89 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.japi.frontend.impl
+
+import java.util.UUID
+
+import akka.actor.{ ActorRef, Props, ActorContext, ActorSystem }
+import io.greenbus.app.actor._
+import io.greenbus.app.actor.frontend._
+import io.greenbus.client.ServiceConnection
+import io.greenbus.client.service.proto.Model.Endpoint
+import io.greenbus.japi.frontend.BusDrivenProtocolFactory
+import io.greenbus.msg.Session
+import io.greenbus.msg.amqp.AmqpServiceOperations
+
+class BusDrivenFrontendProtocolManagerImpl(
+ processName: String,
+ amqpConfig: AmqpConnectionConfig,
+ userConfigPath: String,
+ registrationConfig: FrontendRegistrationConfig,
+ nodeId: String,
+ endpointStrategy: EndpointCollectionStrategy,
+ factory: BusDrivenProtocolFactory) {
+
+ private val system = ActorSystem(processName)
+
+ def start(): Unit = {
+
+ def factoryForEndpointMonitor(endpoint: Endpoint, session: Session, serviceOps: AmqpServiceOperations): Props = {
+
+ BusDrivenWithCommandsProtocolEndpoint.props(
+ endpoint,
+ session,
+ serviceOps,
+ BusDrivenWithCommandsProtocolEndpoint.register(_, _, _, _, lockingEnabled = false, nodeId)(scala.concurrent.ExecutionContext.Implicits.global),
+ ProcessingProxy.props(_, _,
+ statusHeartbeatPeriodMs = 5000,
+ lapsedTimeMs = 11000,
+ statusRetryPeriodMs = 2000,
+ measRetryPeriodMs = 2000,
+ measQueueLimit = 1000),
+ SimpleCommandProxy.props,
+ BusDrivenProtocolHost.props(_, _, _, _, factory),
+ registrationRetryMs = 5000)
+ }
+
+ def connMgr() = {
+
+ def endMon(endpoint: Endpoint, coll: CollectionMembership, session: Session, ops: AmqpServiceOperations) = {
+ EndpointMonitor.props(endpoint, coll, session, ops, factoryForEndpointMonitor)
+ }
+
+ def sessionMgr(conn: ServiceConnection): Props = {
+ SessionLoginManager.props("Endpoint Session Manager", userConfigPath, conn,
+ registrationConfig.loginRetryMs,
+ factory = EndpointCollectionManager.props(endpointStrategy, _, _, None, endMon))
+ }
+
+ FailoverConnectionManager.props("Endpoint Connection Bridge",
+ amqpConfig.amqpConfigFileList,
+ failureLimit = amqpConfig.failureLimit,
+ retryDelayMs = amqpConfig.retryDelayMs,
+ connectionTimeoutMs = amqpConfig.connectionTimeoutMs,
+ sessionMgr)
+ }
+
+ val mgr = system.actorOf(connMgr())
+ }
+
+ def shutdown(): Unit = {
+ system.shutdown()
+ }
+}
\ No newline at end of file
diff --git a/app-framework/src/main/scala/io/greenbus/japi/frontend/impl/BusDrivenProtocolHost.scala b/app-framework/src/main/scala/io/greenbus/japi/frontend/impl/BusDrivenProtocolHost.scala
new file mode 100644
index 0000000..7fff628
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/japi/frontend/impl/BusDrivenProtocolHost.scala
@@ -0,0 +1,85 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.japi.frontend.impl
+
+import akka.actor.{ Props, Actor }
+import com.google.common.util.concurrent.{ FutureCallback, Futures, SettableFuture }
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.app.actor.frontend.{ StackStatusUpdated, MeasurementsPublished }
+import io.greenbus.client.service.proto.Commands.{ CommandStatus, CommandResult, CommandRequest }
+import io.greenbus.client.service.proto.Model.Endpoint
+import io.greenbus.japi.frontend.BusDrivenProtocolFactory
+import io.greenbus.msg.Session
+import io.greenbus.msg.japi.impl.SessionShim
+
+import scala.concurrent.Future
+import scala.concurrent.promise
+
+object BusDrivenProtocolHost {
+
+ def props(endpoint: Endpoint,
+ session: Session,
+ publishMeasurements: MeasurementsPublished => Unit,
+ updateStatus: StackStatusUpdated => Unit,
+ factory: BusDrivenProtocolFactory): Props = {
+ Props(classOf[BusDrivenProtocolHost], endpoint, session, publishMeasurements, updateStatus, factory)
+ }
+}
+
+class BusDrivenProtocolHost(
+ endpoint: Endpoint,
+ session: Session,
+ publishMeasurements: MeasurementsPublished => Unit,
+ updateStatus: StackStatusUpdated => Unit,
+ factory: BusDrivenProtocolFactory) extends Actor with Logging {
+
+ private val protocol = factory.initialize(endpoint, new SessionShim(session), new ProtocolUpdaterImpl(publishMeasurements, updateStatus))
+
+ def receive = {
+ case cmdReq: CommandRequest => {
+
+ val scalaPromise = promise[CommandResult]
+ val javaPromise: SettableFuture[CommandResult] = SettableFuture.create[CommandResult]()
+
+ protocol.handleCommandRequest(cmdReq, javaPromise)
+
+ Futures.addCallback(javaPromise, new FutureCallback[CommandResult] {
+
+ override def onSuccess(v: CommandResult): Unit = {
+ scalaPromise.success(v)
+ }
+
+ override def onFailure(throwable: Throwable): Unit = {
+ scalaPromise.failure(throwable)
+ }
+ })
+
+ sender ! scalaPromise.future
+ }
+ }
+
+ override def postStop(): Unit = {
+ try {
+ protocol.onClose()
+ } catch {
+ case ex: Throwable =>
+ logger.warn(s"Exception thrown for onClose for endpoint ${endpoint.getName}")
+ }
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/japi/frontend/impl/FrontendProtocolManagerImpl.scala b/app-framework/src/main/scala/io/greenbus/japi/frontend/impl/FrontendProtocolManagerImpl.scala
new file mode 100644
index 0000000..522be00
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/japi/frontend/impl/FrontendProtocolManagerImpl.scala
@@ -0,0 +1,55 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.japi.frontend.impl
+
+import java.util.UUID
+
+import akka.actor.{ ActorContext, ActorSystem }
+import io.greenbus.app.actor._
+import io.greenbus.app.actor.frontend.{ MasterProtocol => ScalaMasterProtocol, FrontendRegistrationConfig, FrontendRegistrationConfig$, FrontendFactory }
+import io.greenbus.japi.frontend.{ MasterProtocol => JavaMasterProtocol, ProtocolConfigurer => JavaProtocolConfigurer }
+import scala.collection.JavaConversions._
+
+class FrontendProtocolManagerImpl[ProtocolConfig](
+ protocol: JavaMasterProtocol[ProtocolConfig],
+ protocolConfigurer: JavaProtocolConfigurer[ProtocolConfig],
+ configKeys: java.util.List[String],
+ endpointStrategy: EndpointCollectionStrategy,
+ amqpConfigPath: String,
+ userConfigPath: String) {
+
+ private val system = ActorSystem("FrontendProtocolManager")
+
+ def start(): Unit = {
+
+ val configurer = new ProtocolConfigurerShim(protocolConfigurer)
+
+ def protocolMgrFactory(context: ActorContext): ScalaMasterProtocol[ProtocolConfig] = new MasterProtocolShim[ProtocolConfig](protocol)
+
+ system.actorOf(FrontendFactory.create(
+ AmqpConnectionConfig.default(amqpConfigPath), userConfigPath, endpointStrategy, protocolMgrFactory, configurer, configKeys.toSeq,
+ connectionRepresentsLock = false,
+ nodeId = UUID.randomUUID().toString,
+ FrontendRegistrationConfig.defaults))
+ }
+
+ def shutdown(): Unit = {
+ system.shutdown()
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/japi/frontend/impl/MasterProtocolShim.scala b/app-framework/src/main/scala/io/greenbus/japi/frontend/impl/MasterProtocolShim.scala
new file mode 100644
index 0000000..6ded8c2
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/japi/frontend/impl/MasterProtocolShim.scala
@@ -0,0 +1,79 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.japi.frontend.impl
+
+import com.google.common.util.concurrent.{ ListenableFuture, FutureCallback, Futures }
+import io.greenbus.app.actor.frontend.{ ProtocolCommandAcceptor => ScalaCommandAcceptor, StackStatusUpdated, MeasurementsPublished }
+import io.greenbus.client.service.proto.Commands.{ CommandResult, CommandRequest }
+import io.greenbus.client.service.proto.FrontEnd.FrontEndConnectionStatus
+import io.greenbus.client.service.proto.Model.Endpoint
+import io.greenbus.client.service.proto.Model.ModelUUID
+import io.greenbus.japi.frontend.{ NamedMeasurement, ProtocolUpdater }
+import scala.collection.JavaConversions._
+import scala.concurrent.Future
+import scala.concurrent.promise
+
+class MasterProtocolShim[ProtocolConfig](javaProtocol: io.greenbus.japi.frontend.MasterProtocol[ProtocolConfig])
+ extends io.greenbus.app.actor.frontend.MasterProtocol[ProtocolConfig] {
+
+ def add(endpoint: Endpoint, protocolConfig: ProtocolConfig, scalaPublish: (MeasurementsPublished) => Unit, statusUpdate: (StackStatusUpdated) => Unit): ScalaCommandAcceptor = {
+
+ val updater = new ProtocolUpdater {
+ def updateStatus(status: FrontEndConnectionStatus.Status): Unit = {
+ statusUpdate(StackStatusUpdated(status))
+ }
+
+ def publish(wallTime: Long, updates: java.util.List[NamedMeasurement]): Unit = {
+ scalaPublish(MeasurementsPublished(wallTime, Seq(), updates.map(nm => (nm.getName, nm.getValue))))
+ }
+ }
+
+ val javaAcceptor = javaProtocol.add(endpoint, protocolConfig, updater)
+
+ new ScalaCommandAcceptor {
+ def issue(commandName: String, request: CommandRequest): Future[CommandResult] = {
+
+ val scalaPromise = promise[CommandResult]()
+
+ val javaFuture: ListenableFuture[CommandResult] = javaAcceptor.issue(commandName, request)
+
+ Futures.addCallback(javaFuture, new FutureCallback[CommandResult] {
+ def onSuccess(v: CommandResult): Unit = {
+ scalaPromise.success(v)
+ }
+
+ def onFailure(ex: Throwable): Unit = {
+ scalaPromise.failure(ex)
+ }
+ })
+
+ scalaPromise.future
+ }
+ }
+
+ }
+
+ def remove(endpointUuid: ModelUUID): Unit = {
+ javaProtocol.remove(endpointUuid)
+ }
+
+ def shutdown(): Unit = {
+ javaProtocol.shutdown()
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/japi/frontend/impl/ProtocolConfigurerShim.scala b/app-framework/src/main/scala/io/greenbus/japi/frontend/impl/ProtocolConfigurerShim.scala
new file mode 100644
index 0000000..e1e4a90
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/japi/frontend/impl/ProtocolConfigurerShim.scala
@@ -0,0 +1,36 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.japi.frontend.impl
+
+import io.greenbus.app.actor.frontend.{ ProtocolConfigurer => ScalaProtocolConfigurer }
+import io.greenbus.client.service.proto.Model.{ EntityKeyValue, Endpoint }
+import io.greenbus.japi.frontend.{ ProtocolConfigurer => JavaProtocolConfigurer }
+
+import scala.collection.JavaConversions._
+
+class ProtocolConfigurerShim[ProtocolConfig](javaConfigurer: JavaProtocolConfigurer[ProtocolConfig]) extends ScalaProtocolConfigurer[ProtocolConfig] {
+
+ def evaluate(endpoint: Endpoint, configFiles: Seq[EntityKeyValue]): Option[ProtocolConfig] = {
+ Option(javaConfigurer.evaluate(endpoint, configFiles))
+ }
+
+ def equivalent(latest: ProtocolConfig, previous: ProtocolConfig): Boolean = {
+ javaConfigurer.equivalent(latest, previous)
+ }
+}
diff --git a/app-framework/src/main/scala/io/greenbus/japi/frontend/impl/ProtocolUpdaterImpl.scala b/app-framework/src/main/scala/io/greenbus/japi/frontend/impl/ProtocolUpdaterImpl.scala
new file mode 100644
index 0000000..6e194db
--- /dev/null
+++ b/app-framework/src/main/scala/io/greenbus/japi/frontend/impl/ProtocolUpdaterImpl.scala
@@ -0,0 +1,34 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.japi.frontend.impl
+
+import io.greenbus.app.actor.frontend.{ StackStatusUpdated, MeasurementsPublished }
+import io.greenbus.client.service.proto.FrontEnd.FrontEndConnectionStatus
+import io.greenbus.japi.frontend.{ NamedMeasurement, ProtocolUpdater }
+import scala.collection.JavaConversions._
+
+class ProtocolUpdaterImpl(scalaPublish: (MeasurementsPublished) => Unit, statusUpdate: (StackStatusUpdated) => Unit) extends ProtocolUpdater {
+ def updateStatus(status: FrontEndConnectionStatus.Status): Unit = {
+ statusUpdate(StackStatusUpdated(status))
+ }
+
+ def publish(wallTime: Long, updates: java.util.List[NamedMeasurement]): Unit = {
+ scalaPublish(MeasurementsPublished(wallTime, Seq(), updates.map(nm => (nm.getName, nm.getValue))))
+ }
+}
\ No newline at end of file
diff --git a/app-framework/src/test/scala/io/greenbus/app/actor/frontend/FrontendProtocolEndpointTest.scala b/app-framework/src/test/scala/io/greenbus/app/actor/frontend/FrontendProtocolEndpointTest.scala
new file mode 100644
index 0000000..0d32232
--- /dev/null
+++ b/app-framework/src/test/scala/io/greenbus/app/actor/frontend/FrontendProtocolEndpointTest.scala
@@ -0,0 +1,651 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.frontend
+
+import java.util.concurrent.atomic.AtomicReference
+
+import akka.actor.{ Actor, ActorRef, ActorSystem, Props }
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.matchers.ShouldMatchers
+import org.scalatest.{ BeforeAndAfterEach, FunSuite }
+import io.greenbus.msg.amqp.{ AmqpAddressedMessage, AmqpServiceOperations }
+import io.greenbus.msg.service.{ ServiceHandler, ServiceHandlerSubscription }
+import io.greenbus.msg.{ Session, SessionUnusableException, Subscription, SubscriptionBinding }
+import io.greenbus.app.actor.frontend.CommandProxy.{ CommandSubscriptionRetrieved, CommandsUpdated, ProtocolInitialized, ProtocolUninitialized }
+import io.greenbus.app.actor.frontend.EventActor.SubCanceled
+import io.greenbus.app.actor.frontend.FrontendProtocolEndpoint.Connected
+import io.greenbus.app.actor.frontend.ProcessingProxy.{ LinkUp, PointMapUpdated }
+import io.greenbus.client.exception.{ BadRequestException, LockedException, UnauthorizedException }
+import io.greenbus.client.service.proto.Commands.{ CommandRequest, CommandResult }
+import io.greenbus.client.service.proto.FrontEnd.{ FrontEndConnectionStatus, FrontEndRegistration }
+import io.greenbus.client.service.proto.Model._
+
+import scala.annotation.tailrec
+import scala.concurrent.duration._
+import scala.concurrent.{ Await, Future, Promise, promise }
+
+case class MockSession(id: String) extends Session {
+ def headers: Map[String, String] = ???
+
+ def addHeader(key: String, value: String): Unit = ???
+
+ def removeHeader(key: String, value: String): Unit = ???
+
+ def spawn(): Session = ???
+
+ def clearHeaders(): Unit = ???
+
+ def addHeaders(headers: Seq[(String, String)]): Unit = ???
+
+ def subscribe(requestId: String, headers: Map[String, String], destination: Option[String], payload: Array[Byte]): Future[(Array[Byte], Subscription[Array[Byte]])] = ???
+
+ def request(requestId: String, headers: Map[String, String], destination: Option[String], payload: Array[Byte]): Future[Array[Byte]] = ???
+}
+
+case class MockServiceOps(id: String) extends AmqpServiceOperations {
+ override def publishEvent(exchange: String, msg: Array[Byte], key: String): Unit = ???
+
+ override def publishBatch(messages: Seq[AmqpAddressedMessage]): Unit = ???
+
+ override def bindRoutedService(handler: ServiceHandler): SubscriptionBinding = ???
+
+ override def competingServiceBinding(exchange: String): ServiceHandlerSubscription = ???
+
+ override def declareExchange(exchange: String): Unit = ???
+
+ override def bindQueue(queue: String, exchange: String, key: String): Unit = ???
+
+ override def bindCompetingService(handler: ServiceHandler, exchange: String): SubscriptionBinding = ???
+
+ override def simpleSubscription(): Subscription[Array[Byte]] = ???
+
+ override def routedServiceBinding(): ServiceHandlerSubscription = ???
+}
+
+case class MockServiceHandlerSubscription(id: String, obs: ActorRef) extends ServiceHandlerSubscription {
+ override def start(handler: ServiceHandler): Unit = ???
+
+ override def cancel(): Unit = obs ! SubCanceled(id)
+
+ override def getId(): String = ???
+}
+
+case class MockSub[A](id: String) extends Subscription[A] {
+ override def start(handler: (A) => Unit): Unit = ???
+
+ override def cancel(): Unit = ???
+
+ override def getId(): String = ???
+}
+
+case class FakeConfig(id: String)
+
+class EventQueue {
+ val queue = new AtomicReference(Seq.empty[Any])
+ private val chk = new AtomicReference(Option.empty[(Promise[Boolean], Seq[Any] => Boolean)])
+
+ def received(obj: Any): Unit = {
+ var worked = false
+ var appended = Seq.empty[Any]
+ while (!worked) {
+ val prev = queue.get()
+ appended = prev ++ Vector(obj)
+ worked = queue.compareAndSet(prev, appended)
+ }
+ chk.get().foreach {
+ case (prom, checkFun) => if (checkFun(appended)) prom.success(true)
+ }
+ }
+
+ def listen(check: Seq[Any] => Boolean): Future[Boolean] = {
+ val prom = promise[Boolean]
+ chk.set(Some((prom, check)))
+
+ if (check(queue.get())) prom.success(true)
+
+ prom.future
+ }
+}
+
+object EventActor {
+
+ case class ProtAdded(end: String, config: FakeConfig)
+ case class ProtRemoved(end: String)
+ case object ProtShutdown
+
+ case class ConfigEvaluate(end: String)
+
+ case object ConfigStarted
+ case object ConfigStopped
+
+ case class CmdIssued(cmdName: String, request: String)
+
+ case class RegCalled(end: String, holdingLock: Boolean)
+ case class ConfigCalled(end: String)
+
+ case class SubCanceled(id: String)
+
+ def props(queue: EventQueue): Props = Props(classOf[EventActor], queue)
+}
+class EventActor(queue: EventQueue) extends Actor {
+
+ def receive = {
+ case obj => queue.received(obj)
+ }
+}
+
+import io.greenbus.app.actor.frontend.EventActor._
+
+class ProtocolInterfaces(observer: ActorRef) extends MasterProtocol[FakeConfig] with ProtocolConfigurer[FakeConfig] with ProtocolCommandAcceptor {
+
+ val measPublish = new AtomicReference(Option.empty[(MeasurementsPublished) => Unit])
+ val statusUpdate = new AtomicReference(Option.empty[(StackStatusUpdated) => Unit])
+
+ val config = new AtomicReference(Option.empty[FakeConfig])
+
+ val cmdResult = new AtomicReference(Option.empty[CommandResult])
+
+ val regResult = new AtomicReference(Option.empty[(Future[FrontEndRegistration], ServiceHandlerSubscription)])
+ val cfgResult = new AtomicReference(Option.empty[Future[FrontendConfiguration]])
+
+ def add(endpoint: Endpoint, protocolConfig: FakeConfig, publish: (MeasurementsPublished) => Unit, status: (StackStatusUpdated) => Unit): ProtocolCommandAcceptor = {
+ measPublish.set(Some(publish))
+ statusUpdate.set(Some(status))
+ observer ! ProtAdded(endpoint.getUuid.getValue, protocolConfig)
+ this
+ }
+
+ def remove(endpointUuid: ModelUUID): Unit = {
+ observer ! ProtRemoved(endpointUuid.getValue)
+ }
+
+ def shutdown(): Unit = {
+ observer ! ProtShutdown
+ }
+
+ def evaluate(endpoint: Endpoint, configFiles: Seq[EntityKeyValue]): Option[FakeConfig] = {
+ observer ! ConfigEvaluate(endpoint.getUuid.getValue)
+ config.get()
+ }
+
+ def equivalent(latest: FakeConfig, previous: FakeConfig): Boolean = {
+ false
+ }
+
+ def issue(commandName: String, request: CommandRequest): Future[CommandResult] = {
+ observer ! CmdIssued(commandName, request.getCommandUuid.getValue)
+ Future.successful(cmdResult.get().get)
+ }
+
+ def register(session: Session, serviceOps: AmqpServiceOperations, endpoint: Endpoint, holdingLock: Boolean): (Future[FrontEndRegistration], ServiceHandlerSubscription) = {
+ observer ! RegCalled(endpoint.getUuid.getValue, holdingLock)
+ regResult.get().get
+ }
+
+ def configure(session: Session, endpoint: Endpoint): Future[FrontendConfiguration] = {
+ observer ! ConfigCalled(endpoint.getUuid.getValue)
+ cfgResult.get().get
+ }
+}
+
+object ConfigUpdater {
+ def props(test: ActorRef): Props = Props(classOf[ConfigUpdater], test)
+}
+class ConfigUpdater(test: ActorRef) extends Actor {
+
+ test ! ConfigStarted
+
+ def receive = {
+ case x => test ! x
+ }
+
+ override def postStop(): Unit = {
+ test ! ConfigStopped
+ }
+}
+
+object EventForwarder {
+ def props(test: ActorRef): Props = Props(classOf[EventForwarder], test)
+}
+class EventForwarder(test: ActorRef) extends Actor {
+
+ def receive = {
+ case x => test ! x
+ }
+}
+
+class FepTestRig(system: ActorSystem) {
+
+ val events = new EventQueue
+ val eventActor = system.actorOf(EventActor.props(events))
+
+ val mock = new ProtocolInterfaces(eventActor)
+
+ def configFactory(ref: ActorRef, endpoint: Endpoint, session: Session, config: FrontendConfiguration) = {
+ ConfigUpdater.props(eventActor)
+ }
+
+ def proxyFactory(ref: ActorRef, endpoint: Endpoint) = {
+ EventForwarder.props(eventActor)
+ }
+
+ def cmdFactory(endpoint: Endpoint) = {
+ EventForwarder.props(eventActor)
+ }
+
+ val uuid = "uuid01"
+ val endpoint = Endpoint.newBuilder()
+ .setUuid(ModelUUID.newBuilder().setValue(uuid).build())
+ .setName("end01")
+ .addTypes("Endpoint")
+ .setDisabled(false)
+ .setProtocol("protocol01")
+ .build()
+
+ val fep = system.actorOf(FrontendProtocolEndpoint.props(endpoint, eventActor, mock, mock, 50, 500, proxyFactory, cmdFactory, configFactory, mock.register, mock.configure))
+
+ import io.greenbus.app.actor.frontend.FrontendProtocolEndpointTest._
+ def checkFut(compare: Seq[SubSeq]): Future[Boolean] = events.listen { q => /*println(q);*/ checkSeq(q, compare) }
+}
+
+object FrontendProtocolEndpointTest {
+
+ sealed trait SubSeq
+ case class Ordered(values: Seq[Any]) extends SubSeq
+ case class Unordered(values: Seq[Any]) extends SubSeq
+
+ @tailrec
+ def unorderedEq(a: Seq[Any], b: Seq[Any]): Boolean = {
+ if (a.size != b.size) {
+ false
+ } else if (a.isEmpty) {
+ true
+ } else {
+ val (c, d) = b.span(_ != a.head)
+ if (d.isEmpty) {
+ false
+ } else {
+ unorderedEq(a.drop(1), c ++ d.drop(1))
+ }
+ }
+ }
+
+ @tailrec
+ def checkSeq(results: Seq[Any], compare: Seq[SubSeq]): Boolean = {
+ compare match {
+ case Seq() => results.isEmpty
+ case cmp =>
+ cmp.head match {
+ case Ordered(values) =>
+ results.size >= values.size &&
+ results.take(values.size) == values &&
+ checkSeq(results.drop(values.size), cmp.drop(1))
+ case Unordered(values) =>
+ results.size >= values.size &&
+ unorderedEq(results.take(values.size), values) &&
+ checkSeq(results.drop(values.size), cmp.drop(1))
+ }
+ }
+ }
+
+ def registration(uuid: String, input: String): FrontEndRegistration = FrontEndRegistration.newBuilder().setEndpointUuid(ModelUUID.newBuilder().setValue(uuid).build()).setInputAddress(input).build()
+}
+
+@RunWith(classOf[JUnitRunner])
+class FrontendProtocolEndpointTest extends FunSuite with ShouldMatchers with BeforeAndAfterEach {
+ import io.greenbus.app.actor.frontend.FrontendProtocolEndpointTest._
+
+ private var as = Option.empty[ActorSystem]
+
+ override protected def beforeEach(): Unit = {
+ as = Some(ActorSystem("test"))
+ }
+
+ override protected def afterEach(): Unit = {
+ as.get.shutdown()
+ }
+
+ def comeUpSequence(r: FepTestRig, handlerId: String, sessId: String, input: String, holdingLock: Boolean = false, unorderedPrefix: Seq[Any] = Seq.empty[Any]): Unit = {
+ import r._
+
+ events.queue.set(Seq())
+ mock.regResult.set(Some((Future.successful(registration(uuid, input)), MockServiceHandlerSubscription(handlerId, eventActor))))
+ mock.cfgResult.set(Some(Future.successful(FrontendConfiguration(Seq(), Seq(), Seq(), MockSub[EntityEdgeNotification]("modelEdgeSub01"), MockSub[EntityKeyValueNotification]("configSub01")))))
+
+ mock.config.set(Some(FakeConfig("config01")))
+
+ val comingUp = Seq(
+ Unordered(unorderedPrefix ++ Seq(
+ RegCalled(uuid, holdingLock))),
+ Unordered(Seq(
+ ConfigCalled(uuid),
+ ConfigStarted,
+ LinkUp(MockSession(sessId), input),
+ CommandSubscriptionRetrieved(MockServiceHandlerSubscription(handlerId, eventActor)),
+ PointMapUpdated(Map()),
+ CommandsUpdated(List()),
+ ConfigEvaluate(uuid),
+ ProtAdded(uuid, FakeConfig("config01")),
+ ProtocolInitialized(mock))))
+
+ fep ! Connected(MockSession(sessId), MockServiceOps("ops01"))
+
+ Await.result(checkFut(comingUp), 5000.milliseconds) should equal(true)
+ }
+
+ def serviceComeUpProtocolUp(r: FepTestRig, handlerId: String, sessId: String, input: String, holdingLock: Boolean = false): Unit = {
+ import r._
+
+ events.queue.set(Seq())
+ mock.regResult.set(Some((Future.successful(registration(uuid, input)), MockServiceHandlerSubscription(handlerId, eventActor))))
+ mock.cfgResult.set(Some(Future.successful(FrontendConfiguration(Seq(), Seq(), Seq(), MockSub[EntityEdgeNotification]("modelEdgeSub01"), MockSub[EntityKeyValueNotification]("configSub01")))))
+
+ mock.config.set(Some(FakeConfig("config01")))
+
+ val comingUp = Seq(
+ Ordered(Seq(
+ RegCalled(uuid, holdingLock))),
+ Unordered(Seq(
+ ConfigCalled(uuid),
+ ConfigStarted,
+ LinkUp(MockSession(sessId), input),
+ CommandSubscriptionRetrieved(MockServiceHandlerSubscription(handlerId, eventActor)),
+ PointMapUpdated(Map()),
+ CommandsUpdated(List()))))
+
+ fep ! Connected(MockSession(sessId), MockServiceOps("ops01"))
+
+ Await.result(checkFut(comingUp), 5000.milliseconds) should equal(true)
+ }
+
+ test("coming up process") {
+ val r = new FepTestRig(as.get)
+ comeUpSequence(r, "handler01", "sess01", "input01")
+ }
+
+ def regFailureTest(exFun: => Throwable) = {
+ val r = new FepTestRig(as.get)
+ import r._
+
+ mock.regResult.set(Some((Future.failed(exFun), MockServiceHandlerSubscription("sub01", eventActor))))
+
+ fep ! Connected(MockSession("sess01"), MockServiceOps("ops01"))
+
+ val initialFailure = Seq(
+ Ordered(Seq(
+ RegCalled(uuid, false),
+ SubCanceled("sub01"))))
+
+ Await.result(checkFut(initialFailure), 5000.milliseconds) should equal(true)
+
+ events.queue.set(Seq())
+ mock.regResult.set(Some((Future.failed(exFun), MockServiceHandlerSubscription("sub02", eventActor))))
+
+ val secondFailure = Seq(
+ Ordered(Seq(
+ RegCalled(uuid, false),
+ SubCanceled("sub02"))))
+
+ Await.result(checkFut(secondFailure), 5000.milliseconds) should equal(true)
+
+ comeUpSequence(r, "handler01", "sess01", "input01")
+ }
+
+ test("pre-reg, locked") {
+ regFailureTest(new LockedException("msg01"))
+ }
+
+ test("pre-reg, bad request") {
+ regFailureTest(new BadRequestException("msg01"))
+ }
+
+ test("pre-reg, other") {
+ regFailureTest(new RuntimeException("msg01"))
+ }
+
+ test("pre-reg, session crap") {
+ val r = new FepTestRig(as.get)
+ import r._
+
+ mock.regResult.set(Some((Future.failed(new SessionUnusableException("msg01")), MockServiceHandlerSubscription("sub01", eventActor))))
+
+ fep ! Connected(MockSession("sess01"), MockServiceOps("ops01"))
+
+ val initialFailure = Seq(
+ Ordered(Seq(
+ RegCalled(uuid, false))),
+ Unordered(Seq(
+ SubCanceled("sub01"),
+ ServiceSessionDead)))
+
+ Await.result(checkFut(initialFailure), 5000.milliseconds) should equal(true)
+
+ events.queue.set(Seq())
+ mock.regResult.set(Some((Future.failed(new UnauthorizedException("msg01")), MockServiceHandlerSubscription("sub02", eventActor))))
+
+ fep ! Connected(MockSession("sess02"), MockServiceOps("ops02"))
+
+ val secondFailure = Seq(
+ Ordered(Seq(
+ RegCalled(uuid, false))),
+ Unordered(Seq(
+ SubCanceled("sub02"),
+ ServiceSessionDead)))
+
+ Await.result(checkFut(secondFailure), 5000.milliseconds) should equal(true)
+
+ comeUpSequence(r, "handler01", "sess03", "input01")
+ }
+
+ test("pre-reg, fail before") {
+ val r = new FepTestRig(as.get)
+ import r._
+
+ val prom = promise[FrontEndRegistration]
+ mock.regResult.set(Some((prom.future, MockServiceHandlerSubscription("sub01", eventActor))))
+
+ fep ! Connected(MockSession("sess01"), MockServiceOps("ops01"))
+
+ val comingUp = Seq(
+ Ordered(Seq(
+ RegCalled(uuid, holdingLock = false))))
+
+ Await.result(checkFut(comingUp), 5000.milliseconds) should equal(true)
+
+ comeUpSequence(r, "sub02", "sess02", "input01", holdingLock = false, Seq(SubCanceled("sub01")))
+ }
+
+ /*test("pre-reg, fail before") {
+ val r = new FepTestRig(as.get)
+ import r._
+
+ val prom = promise[FrontEndRegistration]
+ mock.regResult.set(Some((prom.future, MockServiceHandlerSubscription("sub01", eventActor))))
+
+ fep ! Connected(MockSession("sess01"), MockServiceOps("ops01"))
+
+ val comingUp = Seq(
+ Ordered(Seq(
+ RegCalled(uuid, holdingLock = false))))
+
+ Await.result(checkFut(comingUp), 5000.milliseconds) should equal(true)
+
+ comeUpSequence(r, "sub02", "sess02", "input01", holdingLock = false, Seq(SubCanceled("sub01")))
+ }*/
+
+ def sessionDiesWhileProtocolUp(status: FrontEndConnectionStatus.Status, holdingLock: Boolean): Unit = {
+ val r = new FepTestRig(as.get)
+ import r._
+
+ comeUpSequence(r, "handler01", "sess01", "input01")
+
+ events.queue.set(Seq())
+
+ fep ! StackStatusUpdated(status)
+
+ val onCommsUp = Seq(
+ Unordered(Seq(StackStatusUpdated(status))))
+ Await.result(checkFut(onCommsUp), 5000.milliseconds) should equal(true)
+
+ events.queue.set(Seq())
+
+ fep ! ServiceSessionDead
+
+ val onSessionDead = Seq(
+ Unordered(Seq(
+ ServiceSessionDead,
+ SubCanceled("handler01"),
+ ConfigStopped)))
+
+ Await.result(checkFut(onSessionDead), 5000.milliseconds) should equal(true)
+
+ serviceComeUpProtocolUp(r, "handler02", "sess02", "input02", holdingLock = holdingLock)
+ }
+
+ test("comms up, session lost, back up") {
+ sessionDiesWhileProtocolUp(FrontEndConnectionStatus.Status.COMMS_UP, holdingLock = true)
+ }
+ test("comms down, session lost, back up") {
+ sessionDiesWhileProtocolUp(FrontEndConnectionStatus.Status.COMMS_DOWN, holdingLock = false)
+ }
+ test("comms unknown, session lost, back up") {
+ sessionDiesWhileProtocolUp(FrontEndConnectionStatus.Status.UNKNOWN, holdingLock = false)
+ }
+ test("comms error, session lost, back up") {
+ sessionDiesWhileProtocolUp(FrontEndConnectionStatus.Status.ERROR, holdingLock = false)
+ }
+
+ def sessionRefresh(status: FrontEndConnectionStatus.Status, holdingLock: Boolean): Unit = {
+ val r = new FepTestRig(as.get)
+ import r._
+
+ comeUpSequence(r, "handler01", "sess01", "input01")
+
+ events.queue.set(Seq())
+
+ fep ! StackStatusUpdated(status)
+
+ val onCommsUp = Seq(
+ Unordered(Seq(StackStatusUpdated(status))))
+ Await.result(checkFut(onCommsUp), 5000.milliseconds) should equal(true)
+
+ events.queue.set(Seq())
+ mock.regResult.set(Some((Future.successful(registration(uuid, "input02")), MockServiceHandlerSubscription("handler02", eventActor))))
+ mock.cfgResult.set(Some(Future.successful(FrontendConfiguration(Seq(), Seq(), Seq(), MockSub[EntityEdgeNotification]("modelEdgeSub01"), MockSub[EntityKeyValueNotification]("configSub01")))))
+
+ mock.config.set(Some(FakeConfig("config01")))
+
+ val comingUp = Seq(
+ Unordered(Seq(
+ SubCanceled("handler01"),
+ ConfigStopped,
+ RegCalled(uuid, holdingLock),
+ ConfigCalled(uuid),
+ ConfigStarted,
+ LinkUp(MockSession("sess02"), "input02"),
+ CommandSubscriptionRetrieved(MockServiceHandlerSubscription("handler02", eventActor)),
+ PointMapUpdated(Map()),
+ CommandsUpdated(List()))))
+
+ fep ! Connected(MockSession("sess02"), MockServiceOps("ops01"))
+
+ Await.result(checkFut(comingUp), 5000.milliseconds) should equal(true)
+ }
+
+ test("comms up, session refresh, back up") {
+ sessionRefresh(FrontEndConnectionStatus.Status.COMMS_UP, holdingLock = true)
+ }
+ test("comms down, session refresh, back up") {
+ sessionRefresh(FrontEndConnectionStatus.Status.COMMS_DOWN, holdingLock = false)
+ }
+ test("comms unknown, session refresh, back up") {
+ sessionRefresh(FrontEndConnectionStatus.Status.UNKNOWN, holdingLock = false)
+ }
+ test("comms error, session refresh, back up") {
+ sessionRefresh(FrontEndConnectionStatus.Status.ERROR, holdingLock = false)
+ }
+
+ test("session dead, eventual protocol remove") {
+ val r = new FepTestRig(as.get)
+ import r._
+
+ comeUpSequence(r, "handler01", "sess01", "input01")
+
+ events.queue.set(Seq())
+ fep ! StackStatusUpdated(FrontEndConnectionStatus.Status.COMMS_UP)
+
+ val onCommsUp = Seq(
+ Unordered(Seq(StackStatusUpdated(FrontEndConnectionStatus.Status.COMMS_UP))))
+ Await.result(checkFut(onCommsUp), 5000.milliseconds) should equal(true)
+
+ events.queue.set(Seq())
+ fep ! ServiceSessionDead
+
+ val onSessionDead = Seq(
+ Unordered(Seq(
+ ServiceSessionDead,
+ SubCanceled("handler01"),
+ ConfigStopped)),
+ Unordered(Seq(
+ ProtocolUninitialized,
+ ProtRemoved(uuid))))
+
+ Await.result(checkFut(onSessionDead), 5000.milliseconds) should equal(true)
+ }
+
+ test("session dead, locked, protocol remove") {
+ val r = new FepTestRig(as.get)
+ import r._
+
+ comeUpSequence(r, "handler01", "sess01", "input01")
+
+ events.queue.set(Seq())
+ fep ! StackStatusUpdated(FrontEndConnectionStatus.Status.COMMS_UP)
+
+ val onCommsUp = Seq(
+ Unordered(Seq(StackStatusUpdated(FrontEndConnectionStatus.Status.COMMS_UP))))
+ Await.result(checkFut(onCommsUp), 5000.milliseconds) should equal(true)
+
+ events.queue.set(Seq())
+ fep ! ServiceSessionDead
+
+ val onSessionDead = Seq(
+ Unordered(Seq(
+ ServiceSessionDead,
+ SubCanceled("handler01"),
+ ConfigStopped)))
+
+ Await.result(checkFut(onSessionDead), 5000.milliseconds) should equal(true)
+
+ events.queue.set(Seq())
+ mock.regResult.set(Some((Future.failed(new LockedException("msg01")), MockServiceHandlerSubscription("handler02", eventActor))))
+
+ fep ! Connected(MockSession("sess02"), MockServiceOps("ops01"))
+
+ val onBackUp = Seq(
+ Unordered(Seq(
+ RegCalled(uuid, holdingLock = true),
+ SubCanceled("handler02"),
+ ProtocolUninitialized,
+ ProtRemoved(uuid))))
+
+ Await.result(checkFut(onBackUp), 50.milliseconds) should equal(true)
+ }
+}
diff --git a/app-framework/src/test/scala/io/greenbus/app/actor/frontend/QueueLimitTest.scala b/app-framework/src/test/scala/io/greenbus/app/actor/frontend/QueueLimitTest.scala
new file mode 100644
index 0000000..30818ab
--- /dev/null
+++ b/app-framework/src/test/scala/io/greenbus/app/actor/frontend/QueueLimitTest.scala
@@ -0,0 +1,64 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.app.actor.frontend
+
+import org.junit.runner.RunWith
+import org.scalatest.FunSuite
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.matchers.ShouldMatchers
+import io.greenbus.client.service.proto.Measurements.Measurement
+
+@RunWith(classOf[JUnitRunner])
+class QueueLimitTest extends FunSuite with ShouldMatchers {
+
+ def meas(t: Int) = Measurement.newBuilder().setType(Measurement.Type.INT).setIntVal(5).setTime(t).build()
+
+ test("Queue limit, leave one in") {
+ val original = Seq(
+ ("nameA", meas(0)),
+ ("nameB", meas(1)),
+ ("nameC", meas(2)),
+ ("nameA", meas(3)),
+ ("nameA", meas(4)),
+ ("nameA", meas(5)),
+ ("nameA", meas(6)),
+ ("nameC", meas(7)))
+
+ val result = ProcessingProxy.maintainQueueLimit(original, 3)
+
+ result.map(_._2.getTime) should equal(Seq(1, 6, 7))
+ }
+
+ test("Queue limit, more than limit left") {
+ val original = Seq(
+ ("nameA", meas(0)),
+ ("nameB", meas(1)),
+ ("nameC", meas(2)),
+ ("nameD", meas(3)),
+ ("nameE", meas(4)),
+ ("nameF", meas(5)),
+ ("nameA", meas(6)),
+ ("nameC", meas(7)))
+
+ val result = ProcessingProxy.maintainQueueLimit(original, 3)
+
+ result.map(_._2.getTime) should equal(Seq(1, 3, 4, 5, 6, 7))
+ }
+
+}
diff --git a/calculation/pom.xml b/calculation/pom.xml
new file mode 100755
index 0000000..4cf1745
--- /dev/null
+++ b/calculation/pom.xml
@@ -0,0 +1,69 @@
+
+ 4.0.0
+
+
+ io.greenbus
+ greenbus-scala-base
+ 3.0.0
+ ../scala-base
+
+
+ greenbus-calculation
+ jar
+
+
+
+ AGPLv3
+ http://www.gnu.org/licenses/agpl-3.0.txt
+
+
+
+
+
+
+
+ com.mycila.maven-license-plugin
+ maven-license-plugin
+
+
+
+
+
+
+
+
+
+ io.greenbus
+ greenbus-client
+ 3.0.0
+
+
+ io.greenbus
+ greenbus-app-framework
+ 3.0.0
+
+
+ io.greenbus
+ greenbus-util
+ 3.0.0
+
+
+ io.greenbus.msg
+ greenbus-msg-qpid
+ 1.0.0
+
+
+ com.typesafe.akka
+ akka-actor_2.10
+ 2.2.0
+
+
+ com.typesafe.akka
+ akka-slf4j_2.10
+ 2.2.0
+
+
+
+
+
diff --git a/calculation/src/main/scala/io/greenbus/calc/CalculationEndpointManager.scala b/calculation/src/main/scala/io/greenbus/calc/CalculationEndpointManager.scala
new file mode 100644
index 0000000..d810885
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/CalculationEndpointManager.scala
@@ -0,0 +1,305 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc
+
+import akka.actor._
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.msg.{ Session, SessionUnusableException, Subscription, SubscriptionBinding }
+import io.greenbus.app.actor.frontend.{ MeasurementsPublished, StackStatusUpdated }
+import io.greenbus.calc.lib.eval.OperationSource
+import io.greenbus.client.exception.{ BusUnavailableException, ServiceException, UnauthorizedException }
+import io.greenbus.client.proto.Envelope.SubscriptionEventType
+import io.greenbus.client.service.ModelService
+import io.greenbus.client.service.proto.Calculations.CalculationDescriptor
+import io.greenbus.client.service.proto.FrontEnd.FrontEndConnectionStatus
+import io.greenbus.client.service.proto.Measurements.Measurement
+import io.greenbus.client.service.proto.Model._
+import io.greenbus.client.service.proto.ModelRequests._
+
+import scala.concurrent.Future
+import scala.concurrent.duration._
+
+object CalculationEndpointManager {
+
+ val retrieveRetryMs = 5000
+
+ case class ObjectAdded(uuid: ModelUUID)
+ case class ObjectRemoved(uuid: ModelUUID)
+
+ case class CalculationRetrieved(uuid: ModelUUID, calcs: Seq[CalculationDescriptor])
+ case class CalculationRetrieveError(uuid: ModelUUID, ex: Throwable)
+
+ case class CalcEvent(eventType: SubscriptionEventType, uuid: ModelUUID, calc: CalculationDescriptor)
+
+ case class SubResult(current: Seq[(ModelUUID, CalculationDescriptor)], subscription: Subscription[EntityKeyValueNotification], edgeCurrent: Seq[EntityEdge], edgeSub: Subscription[EntityEdgeNotification])
+ case class SubError(ex: Throwable)
+
+ def props(endpoint: Endpoint, session: Session, publishMeasurements: MeasurementsPublished => Unit, updateStatus: StackStatusUpdated => Unit, opSource: OperationSource): Props = {
+ Props(classOf[CalculationEndpointManager], endpoint, session, publishMeasurements, updateStatus, opSource)
+ }
+
+}
+
+class CalculationEndpointManager(endpoint: Endpoint, session: Session, publishMeasurements: MeasurementsPublished => Unit, updateStatus: StackStatusUpdated => Unit, opSource: OperationSource) extends Actor with Logging {
+ import io.greenbus.calc.CalculationEndpointManager._
+
+ private var calcSubscription = Option.empty[SubscriptionBinding]
+ private var edgeSubscription = Option.empty[SubscriptionBinding]
+
+ private var calcMap = Map.empty[ModelUUID, ActorRef]
+
+ // a check for if removed between the time point is added and calc comes in
+ private var outstandingPoints = Set.empty[ModelUUID]
+
+ private def publish(uuid: ModelUUID, m: Measurement): Unit = publishMeasurements(MeasurementsPublished(System.currentTimeMillis(), Seq((uuid, m)), Seq()))
+
+ setupConfig()
+
+ def receive = {
+ case SubResult(current, sub, currentEdges, edgeSub) =>
+ calcSubscription = Some(sub)
+ edgeSubscription = Some(edgeSub)
+
+ sub.start { calcEvent =>
+ keyValueToUuidAndCalc(calcEvent.getValue).foreach {
+ case (uuid, calc) =>
+ self ! CalcEvent(calcEvent.getEventType, uuid, calc)
+ }
+
+ }
+
+ edgeSub.start { ev =>
+ ev.getEventType match {
+ case SubscriptionEventType.ADDED => self ! ObjectAdded(ev.getValue.getChild)
+ case SubscriptionEventType.REMOVED => self ! ObjectRemoved(ev.getValue.getChild)
+ case SubscriptionEventType.MODIFIED =>
+ }
+ }
+
+ logger.info(s"Calculations initialized for ${endpoint.getName}")
+ logger.debug(s"Calculations (${current.size}) configured for ${endpoint.getName}: " + current.map(_._1.getValue))
+ updateStatus(StackStatusUpdated(FrontEndConnectionStatus.Status.COMMS_UP))
+
+ updateCalcs(current)
+
+ case SubError(ex: Throwable) =>
+ logger.warn(s"Error setting up subscription for endpoint ${endpoint.getUuid}")
+ throw ex
+
+ case CalcEvent(eventType, uuid, calc) =>
+ import io.greenbus.client.proto.Envelope.SubscriptionEventType._
+ eventType match {
+ case ADDED =>
+ logger.debug(s"Calculation ${uuid.getValue} added on calculation endpoint ${endpoint.getName}")
+ updateCalcs(Seq((uuid, calc)))
+ case MODIFIED =>
+ logger.debug(s"Calculation ${uuid.getValue} modified on calculation endpoint ${endpoint.getName}")
+ updateCalcs(Seq((uuid, calc)))
+ case REMOVED =>
+ logger.debug(s"Calculation ${uuid.getValue} removed on calculation endpoint ${endpoint.getName}")
+ removeCalcs(Seq(uuid))
+ }
+
+ case ObjectAdded(uuid) =>
+ logger.debug(s"Point ${uuid.getValue} added on calculation endpoint ${endpoint.getName}")
+ outstandingPoints += uuid
+ retrieveCalc(uuid)
+
+ case ObjectRemoved(uuid) =>
+ logger.debug(s"Point ${uuid.getValue} removed on calculation endpoint ${endpoint.getName}")
+ outstandingPoints -= uuid
+ removeCalcs(Seq(uuid))
+
+ case CalculationRetrieved(uuid, calcs) =>
+ calcs.headOption match {
+ case None =>
+ logger.debug(s"Calculation config empty for ${uuid.getValue} on calculation endpoint ${endpoint.getName}")
+ outstandingPoints -= uuid
+ case Some(calc) =>
+ logger.debug(s"Calculation retrieved for ${uuid.getValue} on calculation endpoint ${endpoint.getName}")
+ updateCalcs(Seq((uuid, calc)))
+ outstandingPoints -= uuid
+ }
+
+ case CalculationRetrieveError(uuid, error) =>
+ error match {
+ case ex: BusUnavailableException => scheduleMsg(retrieveRetryMs, ObjectAdded(uuid))
+ case ex: UnauthorizedException => throw ex
+ case ex: ServiceException =>
+ // We assume this means the configuration is bad, treat it like a point that failed to start
+ logger.warn(s"Failed to get calculation for ${uuid.getValue} for endpoint ${endpoint.getName}")
+ case ex => throw ex
+ }
+
+ }
+
+ override def postStop(): Unit = {
+ calcSubscription.foreach(_.cancel())
+ edgeSubscription.foreach(_.cancel())
+ }
+
+ private def removeCalcs(uuids: Seq[ModelUUID]) {
+ uuids.flatMap(calcMap.get).foreach(_ ! PoisonPill)
+ calcMap --= uuids
+ }
+
+ private def retrieveCalc(uuid: ModelUUID) {
+ import context.dispatcher
+ val modelClient = ModelService.client(session)
+
+ val keyPair = EntityKeyPair.newBuilder()
+ .setUuid(uuid)
+ .setKey("calculation")
+ .build()
+
+ val calcFut = modelClient.getEntityKeyValues(Seq(keyPair)).map { results =>
+ results.flatMap(keyValueToCalc)
+ }
+
+ import context.dispatcher
+ calcFut.onSuccess { case calcs => self ! CalculationRetrieved(uuid, calcs) }
+ calcFut.onFailure { case ex => self ! CalculationRetrieveError(uuid, ex) }
+ }
+
+ private def updateCalcs(current: Seq[(ModelUUID, CalculationDescriptor)]) {
+
+ removeCalcs(current.map(_._1))
+
+ val created = current.flatMap {
+ case (uuid, calc) =>
+ try {
+ val actor = context.actorOf(CalculationPointManager.props(uuid, calc, session, opSource, publish))
+ Some(uuid, actor)
+ } catch {
+ case ex: Throwable =>
+ logger.warn(s"Couldn't initialize point in endpoint ${endpoint.getName}: " + ex.getMessage)
+ None
+ }
+ }
+
+ calcMap = calcMap ++ created
+ }
+
+ private def keyValueToCalc(kvp: EntityKeyValue): Option[CalculationDescriptor] = {
+ if (kvp.hasValue && kvp.getValue.hasByteArrayValue) {
+ try {
+ Some(CalculationDescriptor.parseFrom(kvp.getValue.getByteArrayValue))
+ } catch {
+ case ex: Throwable =>
+ logger.warn(s"Calculation set for uuid ${kvp.getUuid} couldn't parse")
+ None
+ }
+ } else {
+ None
+ }
+ }
+
+ private def keyValueToUuidAndCalc(kvp: EntityKeyValue): Option[(ModelUUID, CalculationDescriptor)] = {
+ keyValueToCalc(kvp).map(c => (kvp.getUuid, c))
+ }
+
+ private def getCalcFuture(): Future[Seq[(ModelUUID, CalculationDescriptor)]] = {
+ import context.dispatcher
+ val modelClient = ModelService.client(session)
+
+ val pageSize = Int.MaxValue // TODO: Breaks on large endpoints?
+
+ val pointQueryTemplate = EntityRelationshipFlatQuery.newBuilder()
+ .addStartUuids(endpoint.getUuid)
+ .setRelationship("source")
+ .setDescendantOf(true)
+ .addEndTypes("Point")
+ .setPagingParams(
+ EntityPagingParams.newBuilder()
+ .setPageSize(pageSize))
+
+ val entFuture = modelClient.relationshipFlatQuery(pointQueryTemplate.build())
+
+ entFuture.flatMap {
+ case Seq() => Future.successful(Nil)
+ case ents: Seq[Entity] =>
+ val keys = ents.map { e =>
+ EntityKeyPair.newBuilder()
+ .setUuid(e.getUuid)
+ .setKey("calculation")
+ .build()
+ }
+
+ modelClient.getEntityKeyValues(keys).map { results =>
+ results.flatMap(keyValueToUuidAndCalc)
+ }
+ }
+ }
+
+ private def setupConfig() {
+ import context.dispatcher
+
+ val modelClient = ModelService.client(session)
+
+ val edgeQuery = EntityEdgeSubscriptionQuery.newBuilder()
+ .addFilters(EntityEdgeFilter.newBuilder()
+ .setParentUuid(endpoint.getUuid)
+ .setRelationship("source")
+ .setDistance(1)
+ .build())
+ .build()
+
+ val edgeFut = modelClient.subscribeToEdges(edgeQuery)
+
+ val allFut: Future[SubResult] = edgeFut.flatMap { edgeResult =>
+
+ val subFut = modelClient.subscribeToEntityKeyValues(EntityKeyValueSubscriptionQuery.newBuilder().addEndpointUuids(endpoint.getUuid).build())
+
+ subFut.onFailure { case _ => edgeResult._2.cancel() }
+
+ subFut.flatMap {
+ case (emptyCurrent, sub) =>
+ val currentFut = getCalcFuture()
+
+ currentFut.onFailure { case ex => sub.cancel() }
+
+ currentFut.map { calcs =>
+ (calcs, sub)
+ }
+ }.map {
+ case (calcCurrent, sub) => SubResult(calcCurrent, sub, edgeResult._1, edgeResult._2)
+ }
+ }
+
+ allFut.onSuccess { case result => self ! result }
+ allFut.onFailure { case ex => self ! SubError(ex) }
+ }
+
+ override def supervisorStrategy: SupervisorStrategy = {
+ import akka.actor.SupervisorStrategy._
+ OneForOneStrategy(maxNrOfRetries = 1, withinTimeRange = 3.seconds) {
+ case _: SessionUnusableException => Escalate
+ case _: UnauthorizedException => Escalate
+ case _: Throwable => Restart
+ }
+ }
+
+ private def scheduleMsg(timeMs: Long, msg: AnyRef) {
+ import context.dispatcher
+ context.system.scheduler.scheduleOnce(
+ Duration(timeMs, MILLISECONDS),
+ self,
+ msg)
+ }
+}
diff --git a/calculation/src/main/scala/io/greenbus/calc/CalculationEngine.scala b/calculation/src/main/scala/io/greenbus/calc/CalculationEngine.scala
new file mode 100644
index 0000000..dddd29f
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/CalculationEngine.scala
@@ -0,0 +1,75 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc
+
+import java.util.UUID
+
+import akka.actor.{ ActorSystem, Props }
+import com.typesafe.config.ConfigFactory
+import io.greenbus.msg.Session
+import io.greenbus.msg.amqp.AmqpServiceOperations
+import io.greenbus.app.actor._
+import io.greenbus.app.actor.frontend.{ BusDrivenProtocolEndpoint, ProcessingProxy }
+import io.greenbus.calc.lib.eval.BasicOperations
+import io.greenbus.client.service.proto.Model.Endpoint
+
+object CalculationEngine {
+
+ def main(args: Array[String]) {
+
+ val opSource = BasicOperations.getSource
+
+ val rootConfig = ConfigFactory.load()
+ val slf4jConfig = ConfigFactory.parseString("""akka { loggers = ["akka.event.slf4j.Slf4jLogger"] }""")
+ val akkaConfig = slf4jConfig.withFallback(rootConfig)
+ val system = ActorSystem("calculation", akkaConfig)
+
+ val nodeId = UUID.randomUUID().toString
+
+ val endpointStrategy = new ProtocolsEndpointStrategy(Set("calculator"))
+
+ val baseDir = Option(System.getProperty("io.greenbus.config.base")).getOrElse("")
+ val amqpConfigPath = Option(System.getProperty("io.greenbus.config.amqp")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.msg.amqp.cfg")
+ val userConfigPath = Option(System.getProperty("io.greenbus.config.user")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.user.cfg")
+
+ def factoryForEndpointMonitor(endpoint: Endpoint, session: Session, serviceOps: AmqpServiceOperations): Props = {
+
+ BusDrivenProtocolEndpoint.props(
+ endpoint,
+ session,
+ BusDrivenProtocolEndpoint.register(_, _, _, lockingEnabled = false, nodeId)(scala.concurrent.ExecutionContext.Implicits.global),
+ ProcessingProxy.props(_, _,
+ statusHeartbeatPeriodMs = 5000,
+ lapsedTimeMs = 11000,
+ statusRetryPeriodMs = 2000,
+ measRetryPeriodMs = 2000,
+ measQueueLimit = 1000),
+ CalculationEndpointManager.props(_, _, _, _, opSource),
+ registrationRetryMs = 5000)
+ }
+
+ val mgr = system.actorOf(
+ ConnectedApplicationManager.props(
+ "Calculation Engine",
+ amqpConfigPath,
+ userConfigPath,
+ EndpointCollectionManager.props(endpointStrategy, _, _, None,
+ EndpointMonitor.props(_, _, _, _, factoryForEndpointMonitor))))
+ }
+}
diff --git a/calculation/src/main/scala/io/greenbus/calc/CalculationPointManager.scala b/calculation/src/main/scala/io/greenbus/calc/CalculationPointManager.scala
new file mode 100644
index 0000000..6353f36
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/CalculationPointManager.scala
@@ -0,0 +1,276 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc
+
+import akka.actor.{ Actor, Props }
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.msg.{ Session, Subscription, SubscriptionBinding }
+import io.greenbus.calc.lib.eval.OperationSource
+import io.greenbus.calc.lib.{ CalculationEvaluator, ErrorMeasurement, InputBucket }
+import io.greenbus.client.service.MeasurementService
+import io.greenbus.client.service.proto.Calculations.{ CalculationDescriptor, CalculationInput }
+import io.greenbus.client.service.proto.MeasurementRequests.MeasurementHistoryQuery
+import io.greenbus.client.service.proto.Measurements.{ Measurement, MeasurementNotification, PointMeasurementValue, PointMeasurementValues }
+import io.greenbus.client.service.proto.Model.ModelUUID
+
+import scala.collection.JavaConversions._
+import scala.concurrent.Future
+import scala.concurrent.duration._
+
+class CalcConfigException(msg: String) extends Exception(msg)
+
+object CalculationPointManager extends Logging {
+
+ case object OnPeriodUpdate
+
+ case class RequestsCompleted(current: Seq[PointMeasurementValue], sub: Subscription[MeasurementNotification], histories: Seq[PointMeasurementValues])
+ case class RequestsFailure(ex: Throwable)
+
+ case object ConstantValue
+
+ sealed trait TriggerStrategy
+ case object OnAnyUpdate extends TriggerStrategy
+ case class PeriodUpdate(periodMs: Long) extends TriggerStrategy
+
+ def props(pointUuid: ModelUUID, config: CalculationDescriptor, session: Session, opSource: OperationSource, measSink: (ModelUUID, Measurement) => Unit): Props = {
+ Props(classOf[CalculationPointManager], pointUuid, config, session, opSource, measSink)
+ }
+
+ def buildBuckets(config: Seq[CalculationInput]): Map[ModelUUID, InputBucket] = {
+ config.map { calc =>
+ val point = calc.getPointUuid
+ val bucket = InputBucket.build(calc)
+ (point, bucket)
+ }.toMap
+ }
+
+ def getTriggerStrategy(config: CalculationDescriptor): TriggerStrategy = {
+
+ def error = new CalcConfigException("Calculation must have a valid trigger strategy")
+
+ if (config.hasTriggering) {
+ val triggering = config.getTriggering
+ if (triggering.hasUpdateAny && triggering.getUpdateAny) {
+ OnAnyUpdate
+ } else if (triggering.hasPeriodMs) {
+ PeriodUpdate(triggering.getPeriodMs)
+ } else {
+ throw error
+ }
+ } else {
+ throw error
+ }
+ }
+
+ def requestForInput(calc: CalculationInput): Option[MeasurementHistoryQuery] = {
+ import io.greenbus.util.Optional._
+
+ if (calc.hasSingle || calc.hasRange && calc.getRange.hasSinceLast && calc.getRange.getSinceLast) {
+ None
+ } else {
+
+ val range = optGet(calc.hasRange, calc.getRange) getOrElse {
+ throw new CalcConfigException("Non-singular calc input must include range")
+ }
+
+ val pointUuid = optGet(calc.hasPointUuid, calc.getPointUuid).getOrElse {
+ throw new CalcConfigException("Must include point uuid for calc input")
+ }
+
+ val limit = optGet(range.hasLimit, range.getLimit).getOrElse(100)
+
+ val timeFromMs = if (range.hasFromMs) Some(range.getFromMs) else None
+
+ val b = MeasurementHistoryQuery.newBuilder()
+ .setPointUuid(pointUuid)
+ .setLatest(true)
+ .setLimit(limit)
+
+ timeFromMs.foreach(offset => b.setTimeFrom(System.currentTimeMillis() + offset))
+
+ Some(b.build())
+ }
+ }
+
+}
+
+class CalculationPointManager(pointUuid: ModelUUID, config: CalculationDescriptor, session: Session, opSource: OperationSource, measSink: (ModelUUID, Measurement) => Unit) extends Actor with Logging {
+ import io.greenbus.calc.CalculationPointManager._
+
+ private var inputPoints: Map[ModelUUID, InputBucket] = buildBuckets(config.getCalcInputsList.toSeq) //Map.empty[ModelUUID, InputBucket]
+
+ private val triggerStrategy: TriggerStrategy = getTriggerStrategy(config)
+
+ private val evaluator: CalculationEvaluator = CalculationEvaluator.build(config, opSource)
+
+ private var binding = Option.empty[SubscriptionBinding]
+
+ setupData()
+
+ def receive = {
+ case ConstantValue =>
+ evaluate()
+
+ case RequestsCompleted(current, sub, histories) =>
+ binding = Some(sub)
+
+ initBuckets(current, histories)
+
+ sub.start { measEvent =>
+ self ! measEvent
+ }
+
+ logger.info(s"Calculation initialized for point ${pointUuid.getValue}")
+
+ triggerStrategy match {
+ case PeriodUpdate(periodMs) => scheduleMsg(periodMs, OnPeriodUpdate)
+ case _ =>
+ }
+
+ case RequestsFailure(ex) =>
+ logger.warn(s"Failed to get data for calculation point ${pointUuid.getValue}")
+ throw ex
+
+ case m: MeasurementNotification =>
+ val point = m.getPointUuid
+ val meas = m.getValue
+
+ logger.trace(s"Got measurement update for point ${point.getValue} on ${pointUuid.getValue}")
+
+ inputPoints.get(point) match {
+ case None => logger.warn("Got measurement event for point without bucket: " + m.getPointName)
+ case Some(current) =>
+ inputPoints = inputPoints.updated(point, current.added(meas))
+ }
+
+ triggerStrategy match {
+ case PeriodUpdate(_) =>
+ case OnAnyUpdate => evaluate()
+ }
+
+ case OnPeriodUpdate =>
+ evaluate()
+
+ triggerStrategy match {
+ case PeriodUpdate(periodMs) => scheduleMsg(periodMs, OnPeriodUpdate)
+ case other => throw new IllegalStateException("Got period update but strategy was on any update")
+ }
+ }
+
+ private def evaluate() {
+
+ logger.debug(s"Evaluating - ${pointUuid.getValue}")
+
+ val snapshot = inputPoints.mapValues(_.snapshot())
+
+ val containerMap = snapshot.mapValues(_._1).flatMap { case (k, vOpt) => vOpt.map(v => (k, v)) }
+ val updatedInputs = snapshot.mapValues(_._2)
+
+ try {
+ evaluator.evaluate(containerMap) match {
+ case None =>
+ case Some(result) =>
+ inputPoints = updatedInputs
+ measSink(pointUuid, result)
+ }
+ } catch {
+ case ex: Throwable =>
+ logger.warn("Calculation error: " + ex)
+ ex.printStackTrace()
+ measSink(pointUuid, ErrorMeasurement.build())
+ }
+ }
+
+ private def setupData() {
+ import context.dispatcher
+
+ val inputs = config.getCalcInputsList.toSeq
+
+ if (inputs.nonEmpty) {
+
+ val measClient = MeasurementService.client(session)
+
+ val allPointUuids = inputs.map(_.getPointUuid)
+
+ val subFut = measClient.getCurrentValuesAndSubscribe(allPointUuids)
+
+ val queries = inputs.flatMap(requestForInput)
+
+ val histsFut: Future[Seq[PointMeasurementValues]] = Future.sequence {
+ queries.map(measClient.getHistory)
+ }
+
+ val allFut = subFut zip histsFut
+
+ allFut.onSuccess {
+ case ((current, sub), hists) => self ! RequestsCompleted(current, sub, hists)
+ }
+ subFut.onFailure {
+ case ex =>
+ subFut.foreach(_._2.cancel())
+ self ! RequestsFailure(ex)
+ }
+
+ } else {
+
+ self ! ConstantValue
+ }
+ }
+
+ private def initBuckets(current: Seq[PointMeasurementValue], histories: Seq[PointMeasurementValues]) {
+
+ val uuidToHistMap: Map[ModelUUID, Seq[Measurement]] = histories.map(pvs => (pvs.getPointUuid, pvs.getValueList.toSeq)).toMap
+
+ val mapUpdates = current.flatMap { pv =>
+ val point = pv.getPointUuid
+ val m = pv.getValue
+
+ val histOpt = uuidToHistMap.get(point)
+
+ val measSeq = histOpt.map { hist =>
+ if (hist.last.getTime < m.getTime) {
+ hist :+ m
+ } else {
+ hist
+ }
+ } getOrElse {
+ Seq(m)
+ }
+
+ inputPoints.get(point).map { bucket =>
+ (point, bucket.added(measSeq))
+ }
+ }
+
+ inputPoints = inputPoints ++ mapUpdates
+ }
+
+ override def postStop() {
+ logger.debug("Canceling subscription binding for " + pointUuid.getValue)
+ binding.foreach(_.cancel())
+ }
+
+ private def scheduleMsg(timeMs: Long, msg: AnyRef) {
+ import context.dispatcher
+ context.system.scheduler.scheduleOnce(
+ Duration(timeMs, MILLISECONDS),
+ self,
+ msg)
+ }
+}
\ No newline at end of file
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/CalculationEvaluator.scala b/calculation/src/main/scala/io/greenbus/calc/lib/CalculationEvaluator.scala
new file mode 100755
index 0000000..50c93fe
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/CalculationEvaluator.scala
@@ -0,0 +1,129 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib
+
+import io.greenbus.calc.CalcConfigException
+import io.greenbus.calc.lib.eval.{ ValueRange, _ }
+import io.greenbus.client.service.proto.Calculations.CalculationDescriptor
+import io.greenbus.client.service.proto.Measurements.Measurement
+import io.greenbus.client.service.proto.Model.ModelUUID
+
+import scala.collection.JavaConversions._
+
+object CalculationEvaluator {
+
+ def containerToOpValue(mc: MeasContainer): OperationValue = {
+ mc match {
+ case SingleMeas(m) => MeasurementConverter.convertMeasurement(m)
+ case MeasRange(seq) => ValueRange(seq map MeasurementConverter.convertMeasurement)
+ }
+ }
+
+ def containerToSeq(mc: MeasContainer): Seq[Measurement] = {
+ mc match {
+ case SingleMeas(m) => Seq(m)
+ case MeasRange(seq) => seq
+ }
+ }
+
+ def build(calc: CalculationDescriptor, operations: OperationSource): CalculationEvaluator = {
+
+ val expr = if (calc.hasFormula) {
+ OperationParser.parseFormula(calc.getFormula)
+ } else {
+ throw new CalcConfigException("Need formula in calculation config")
+ }
+
+ val formula = Formula(expr, operations)
+
+ val qualInputStrat = if (calc.hasTriggeringQuality && calc.getTriggeringQuality.hasStrategy) {
+ QualityInputStrategy.build(calc.getTriggeringQuality.getStrategy)
+ } else {
+ throw new CalcConfigException("Need quality input strategy in calculation config")
+ }
+
+ val qualOutputStrat = if (calc.hasQualityOutput && calc.getQualityOutput.hasStrategy) {
+ QualityOutputStrategy.build(calc.getQualityOutput.getStrategy)
+ } else {
+ throw new CalcConfigException("Need quality output strategy in calculation config")
+ }
+
+ val varNameToPointMap = calc.getCalcInputsList.map { in =>
+ if (!in.hasPointUuid) {
+ throw new CalcConfigException("Input must contain point uuid")
+ }
+ if (!in.hasVariableName) {
+ throw new CalcConfigException("Input must contain variable name")
+ }
+
+ (in.getVariableName, in.getPointUuid)
+ }.toMap
+
+ new CalculationEvaluator(formula, varNameToPointMap, qualInputStrat, qualOutputStrat)
+ }
+}
+
+class CalculationEvaluator(formula: Formula,
+ varNameToPointMap: Map[String, ModelUUID],
+ qualInputStrategy: QualityInputStrategy,
+ qualOutputStrategy: QualityOutputStrategy) {
+
+ import io.greenbus.calc.lib.CalculationEvaluator._
+
+ def evaluate(inputs: Map[ModelUUID, MeasContainer]): Option[Measurement] = {
+
+ // Only calculate if all inputs are present and nonempty
+ val filteredInputs: Option[Map[ModelUUID, MeasContainer]] =
+ qualInputStrategy.checkInputs(inputs)
+
+ val variablesOpt: Option[Map[String, MeasContainer]] =
+ filteredInputs.flatMap(mapPointsToVars)
+
+ variablesOpt.map { variableMap =>
+
+ val inputOpValues = variableMap.mapValues(containerToOpValue)
+
+ val result = formula.evaluate(new MappedVariableSource(inputOpValues))
+
+ val inputMeases = variableMap.mapValues(containerToSeq)
+
+ val qual = qualOutputStrategy.getQuality(inputMeases)
+
+ val time = TimeStrategy.getTime(inputMeases)
+
+ MeasurementConverter.convertOperationValue(result)
+ .setQuality(qual)
+ .setTime(time)
+ .build()
+ }
+ }
+
+ private def mapPointsToVars(inputs: Map[ModelUUID, MeasContainer]): Option[Map[String, MeasContainer]] = {
+
+ if (varNameToPointMap.values.forall(inputs.contains)) {
+ val toVarNames = varNameToPointMap.map {
+ case (varName, pointUuid) => (varName, inputs(pointUuid))
+ }
+ Some(toVarNames)
+ } else {
+ None
+ }
+ }
+}
+
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/CalculationMetrics.scala b/calculation/src/main/scala/io/greenbus/calc/lib/CalculationMetrics.scala
new file mode 100755
index 0000000..70693ac
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/CalculationMetrics.scala
@@ -0,0 +1,40 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib
+
+import io.greenbus.jmx.{ MetricsSource, Metrics }
+
+class CalculationMetrics(metrics: Metrics) {
+
+ val attempts = metrics.counter("Attempts")
+
+ val evals = metrics.counter("Evals")
+
+ val errors = metrics.counter("Errors")
+
+ val evalTime = metrics.timer("Time")
+}
+
+class CalculationMetricsSource(source: MetricsSource, shared: Boolean = false) {
+
+ private lazy val sharedSink = new CalculationMetrics(source.metrics("all"))
+
+ def getCalcMetrics(calcName: String) = if (shared) sharedSink else new CalculationMetrics(source.metrics(calcName))
+
+}
\ No newline at end of file
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/ErrorMeasurement.scala b/calculation/src/main/scala/io/greenbus/calc/lib/ErrorMeasurement.scala
new file mode 100644
index 0000000..8ce82c7
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/ErrorMeasurement.scala
@@ -0,0 +1,32 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib
+
+import io.greenbus.client.service.proto.Measurements.{ Quality, Measurement }
+
+object ErrorMeasurement {
+ def build() = {
+ Measurement.newBuilder
+ .setType(Measurement.Type.STRING)
+ .setStringVal("#ERROR")
+ .setQuality(Quality.newBuilder.setValidity(Quality.Validity.QUESTIONABLE).setSource(Quality.Source.PROCESS))
+ .setTime(System.currentTimeMillis())
+ .build()
+ }
+}
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/InputBucket.scala b/calculation/src/main/scala/io/greenbus/calc/lib/InputBucket.scala
new file mode 100755
index 0000000..0598a8f
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/InputBucket.scala
@@ -0,0 +1,174 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib
+
+import io.greenbus.client.service.proto.Measurements.Measurement
+import io.greenbus.calc.lib.eval.{ ValueRange, OperationValue }
+import io.greenbus.client.service.proto.Calculations.CalculationInput
+import scala.collection.immutable.Queue
+import io.greenbus.client.service.proto.Calculations.SingleMeasurement.MeasurementStrategy
+
+// Difference between singular input and collection that happens to have length 1
+sealed trait MeasContainer
+case class MeasRange(range: Seq[Measurement]) extends MeasContainer
+case class SingleMeas(m: Measurement) extends MeasContainer
+
+sealed trait MeasRequest
+case object SingleLatest extends MeasRequest
+case class MultiSince(from: Long, limit: Int) extends MeasRequest
+case class MultiLimit(count: Int) extends MeasRequest
+case class SincePublishing(count: Int) extends MeasRequest
+
+trait InputBucket {
+ def added(m: Measurement): InputBucket
+ def snapshot(): (Option[MeasContainer], InputBucket)
+
+ def added(seq: Seq[Measurement]): InputBucket = {
+ seq.foldLeft(this) { (b, m) => b.added(m) }
+ }
+}
+
+object InputBucket {
+
+ def build(calc: CalculationInput): InputBucket = {
+ import io.greenbus.util.Optional._
+
+ val limitOpt = for {
+ range <- optGet(calc.hasRange, calc.getRange)
+ limit <- optGet(range.hasLimit, range.getLimit)
+ } yield limit
+
+ val limit = limitOpt.getOrElse(100)
+
+ if (calc.hasSingle && calc.getSingle.hasStrategy) {
+ calc.getSingle.getStrategy match {
+ case MeasurementStrategy.MOST_RECENT => new SingleLatestBucket(None)
+ case x => throw new Exception("Unknown single measurement strategy: " + x)
+ }
+ } else if (calc.hasRange && calc.getRange.hasFromMs) {
+ new FromRangeBucket(Queue.empty[Measurement], SystemTimeSource, calc.getRange.getFromMs, limit)
+ } else if (calc.hasRange && calc.getRange.hasLimit) {
+ new LimitRangeBucket(Queue.empty[Measurement], limit)
+ } else if (calc.hasRange && calc.getRange.hasSinceLast && calc.getRange.getSinceLast) {
+ new NoStorageBucket(Queue.empty[Measurement], limit)
+ } else {
+ throw new Exception("Cannot build input from configuration")
+ }
+ }
+
+ class FromRangeBucket(queue: Queue[Measurement], timeSource: TimeSource, from: Long, limit: Int, minimum: Int = 1) extends InputBucket {
+
+ private def copy(q: Queue[Measurement]) = {
+ new FromRangeBucket(q, timeSource, from, limit, minimum)
+ }
+
+ private def prune(current: Queue[Measurement]): Queue[Measurement] = {
+ val horizon = timeSource.now + from
+
+ val extra = current.size - limit
+ val trimmed = if (extra > 0) {
+ current.drop(extra)
+ } else {
+ current
+ }
+
+ trimmed.dropWhile(m => m.getTime <= horizon)
+ }
+
+ def added(m: Measurement): InputBucket = {
+ val q = queue.enqueue(m)
+ new FromRangeBucket(prune(q), timeSource, from, limit)
+ }
+
+ def snapshot(): (Option[MeasContainer], InputBucket) = {
+ val pruned = prune(queue)
+
+ val bucket = if (pruned == queue) this else copy(pruned)
+
+ val opVal = if (pruned.nonEmpty && pruned.size >= minimum) {
+ Some(MeasRange(pruned))
+ } else {
+ None
+ }
+
+ (opVal, bucket)
+ }
+ }
+
+ class LimitRangeBucket(queue: Queue[Measurement], limit: Int, minimum: Int = 1) extends InputBucket {
+
+ private def copy(q: Queue[Measurement]) = {
+ new LimitRangeBucket(q, limit, minimum)
+ }
+
+ def added(m: Measurement): InputBucket = {
+ val q = queue.enqueue(m)
+ val extra = q.size - limit
+ val trimmed = if (extra > 0) {
+ q.drop(extra)
+ } else {
+ q
+ }
+ copy(trimmed)
+ }
+
+ def snapshot(): (Option[MeasContainer], InputBucket) = {
+ val opVal = if (queue.nonEmpty && queue.size >= minimum) {
+ Some(MeasRange(queue))
+ } else {
+ None
+ }
+
+ (opVal, this)
+ }
+ }
+
+ case class NoStorageBucket(queue: Queue[Measurement], limit: Int) extends InputBucket {
+
+ def added(m: Measurement): InputBucket = {
+ var q = queue.enqueue(m)
+ while (q.size > limit) {
+ val (_, temp) = q.dequeue
+ q = temp
+ }
+ copy(q)
+ }
+
+ def snapshot(): (Option[MeasContainer], InputBucket) = {
+ val opVal = if (queue.nonEmpty) {
+ Some(MeasRange(queue))
+ } else {
+ None
+ }
+
+ (opVal, copy(queue = Queue.empty[Measurement]))
+ }
+ }
+
+ class SingleLatestBucket(meas: Option[Measurement]) extends InputBucket {
+
+ def added(m: Measurement): InputBucket = {
+ new SingleLatestBucket(Some(m))
+ }
+
+ def snapshot(): (Option[MeasContainer], InputBucket) = {
+ (meas.map(SingleMeas), this)
+ }
+ }
+}
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/MappedVariableSource.scala b/calculation/src/main/scala/io/greenbus/calc/lib/MappedVariableSource.scala
new file mode 100644
index 0000000..8b9421a
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/MappedVariableSource.scala
@@ -0,0 +1,30 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib
+
+import eval.{ ValueRange, VariableSource, OperationValue, EvalException }
+import io.greenbus.client.service.proto.Measurements.Measurement
+
+class MappedVariableSource(map: Map[String, OperationValue]) extends VariableSource {
+ def forName(name: String): OperationValue = {
+ map.get(name).getOrElse {
+ throw new EvalException("Variable does not exist: " + name + " " + map)
+ }
+ }
+}
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/MeasurementConverter.scala b/calculation/src/main/scala/io/greenbus/calc/lib/MeasurementConverter.scala
new file mode 100755
index 0000000..0e1eae7
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/MeasurementConverter.scala
@@ -0,0 +1,61 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib
+
+import eval._
+import io.greenbus.client.service.proto.Measurements.Measurement
+
+object MeasurementConverter {
+
+ def convertMeasurement(m: Measurement): OperationValue = {
+ if (m.getType == Measurement.Type.DOUBLE || (m.getType == Measurement.Type.STRING && m.hasDoubleVal)) {
+ NumericMeas(m.getDoubleVal, m.getTime)
+ } else if (m.getType == Measurement.Type.INT || (m.getType == Measurement.Type.STRING && m.hasIntVal)) {
+ LongMeas(m.getIntVal, m.getTime)
+ } else if (m.getType == Measurement.Type.BOOL || (m.getType == Measurement.Type.STRING && m.hasBoolVal)) {
+ BooleanMeas(m.getBoolVal, m.getTime)
+ } else {
+ throw new EvalException("Cannot use measurement as input: " + m)
+ }
+ }
+
+ def convertOperationValue(v: OperationValue): Measurement.Builder = {
+ val b = Measurement.newBuilder
+
+ // weird bug in case matching, see LongValue.unapply
+ v match {
+ case LongValue(d) =>
+ b.setType(Measurement.Type.INT)
+ b.setIntVal(d)
+ case _ =>
+ v match {
+ case NumericValue(d) =>
+ b.setType(Measurement.Type.DOUBLE)
+ b.setDoubleVal(d)
+ case BooleanConst(bv) =>
+ b.setType(Measurement.Type.BOOL)
+ b.setBoolVal(bv)
+ case _ =>
+ throw new EvalException("Cannot use value as output: " + v)
+ }
+ }
+
+ b
+ }
+}
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/QualityInputStrategy.scala b/calculation/src/main/scala/io/greenbus/calc/lib/QualityInputStrategy.scala
new file mode 100755
index 0000000..1f7aa19
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/QualityInputStrategy.scala
@@ -0,0 +1,70 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib
+
+import io.greenbus.client.service.proto.Calculations.InputQuality
+import io.greenbus.client.service.proto.Measurements.{ Quality, Measurement }
+
+trait QualityInputStrategy {
+ def checkInputs[A](inputs: Map[A, MeasContainer]): Option[Map[A, MeasContainer]]
+}
+
+object QualityInputStrategy {
+
+ def build(config: InputQuality.Strategy): QualityInputStrategy = config match {
+ case InputQuality.Strategy.ACCEPT_ALL => AcceptAll
+ case InputQuality.Strategy.ONLY_WHEN_ALL_OK => WhenAllOk
+ case InputQuality.Strategy.REMOVE_BAD_AND_CALC => FilterOutBad
+ case _ => throw new Exception("Unknown quality input strategy")
+ }
+
+ private def isGoodQuality(m: Measurement): Boolean = m.getQuality.getValidity == Quality.Validity.GOOD
+
+ object AcceptAll extends QualityInputStrategy {
+ def checkInputs[A](inputs: Map[A, MeasContainer]): Option[Map[A, MeasContainer]] = {
+ Some(inputs)
+ }
+ }
+
+ object WhenAllOk extends QualityInputStrategy {
+ def checkInputs[A](inputs: Map[A, MeasContainer]): Option[Map[A, MeasContainer]] = {
+
+ val allGood = inputs.values.forall {
+ case SingleMeas(m) => isGoodQuality(m)
+ case MeasRange(seq) => seq.forall(isGoodQuality)
+ }
+
+ if (allGood) Some(inputs) else None
+ }
+ }
+
+ object FilterOutBad extends QualityInputStrategy {
+ def checkInputs[A](inputs: Map[A, MeasContainer]): Option[Map[A, MeasContainer]] = {
+
+ val filtMap = inputs.flatMap {
+ case kv @ (_, SingleMeas(m)) => if (isGoodQuality(m)) Some(kv) else None
+ case kv @ (_, MeasRange(seq)) =>
+ val filtered = seq.filter(isGoodQuality)
+ if (filtered.nonEmpty) Some(kv) else None
+ }
+
+ Some(filtMap)
+ }
+ }
+}
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/QualityOutputStrategy.scala b/calculation/src/main/scala/io/greenbus/calc/lib/QualityOutputStrategy.scala
new file mode 100644
index 0000000..78711da
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/QualityOutputStrategy.scala
@@ -0,0 +1,103 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib
+
+import io.greenbus.client.service.proto.Calculations.OutputQuality
+import io.greenbus.client.service.proto.Measurements.{ DetailQual, Quality, Measurement }
+
+trait QualityOutputStrategy {
+ def getQuality(inputs: Map[String, Seq[Measurement]]): Quality
+}
+
+object QualityOutputStrategy {
+ def build(config: OutputQuality.Strategy) = config match {
+ case OutputQuality.Strategy.ALWAYS_OK => AlwaysOk
+ case OutputQuality.Strategy.WORST_QUALITY => WorstQuality
+ case _ => throw new Exception("Unknown quality output strategy")
+ }
+
+ object AlwaysOk extends QualityOutputStrategy {
+ def getQuality(inputs: Map[String, Seq[Measurement]]): Quality = {
+ Quality.newBuilder().setValidity(Quality.Validity.GOOD).setSource(Quality.Source.PROCESS).build()
+ }
+ }
+
+ object WorstQuality extends QualityOutputStrategy {
+ def getQuality(inputs: Map[String, Seq[Measurement]]): Quality = {
+ if (inputs.nonEmpty) {
+ inputs.values.flatten.map(_.getQuality).reduceLeft((l, r) => merge(l, r))
+ } else {
+ Quality.newBuilder().setValidity(Quality.Validity.GOOD).setSource(Quality.Source.PROCESS).build()
+ }
+ }
+
+ private def merge(l: Quality, r: Quality): Quality = {
+ val ldetail = if (l.hasDetailQual) l.getDetailQual else DetailQual.newBuilder().build()
+ val rdetail = if (r.hasDetailQual) r.getDetailQual else DetailQual.newBuilder().build()
+
+ Quality.newBuilder()
+ .setDetailQual(merge(ldetail, rdetail))
+ .setSource(merge(l.getSource, r.getSource))
+ .setValidity(merge(l.getValidity, r.getValidity))
+ .setTest(mergeTest(l.getTest, r.getTest))
+ .setOperatorBlocked(mergeBlocked(l.getOperatorBlocked, r.getOperatorBlocked))
+ .build()
+ }
+
+ private def rank(v: Quality.Validity) = v match {
+ case Quality.Validity.GOOD => 1
+ case Quality.Validity.INVALID => 2
+ case Quality.Validity.QUESTIONABLE => 3
+ }
+
+ private def merge(left: Quality.Validity, right: Quality.Validity) = {
+ if (rank(left) >= rank(right)) left else right
+ }
+
+ private def merge(left: Quality.Source, right: Quality.Source) = {
+ if (left == Quality.Source.SUBSTITUTED || right == Quality.Source.SUBSTITUTED) {
+ Quality.Source.SUBSTITUTED
+ } else {
+ Quality.Source.PROCESS
+ }
+ }
+
+ private def mergeTest(left: Boolean, right: Boolean) = {
+ left || right
+ }
+
+ private def mergeBlocked(left: Boolean, right: Boolean) = {
+ left || right
+ }
+
+ private def merge(left: DetailQual, right: DetailQual) = {
+ DetailQual.newBuilder()
+ .setBadReference(left.getBadReference || right.getBadReference)
+ .setOverflow(left.getOverflow || right.getOverflow)
+ .setOutOfRange(left.getOutOfRange || right.getOutOfRange)
+ .setOscillatory(left.getOscillatory || right.getOscillatory)
+ .setFailure(left.getFailure || right.getFailure)
+ .setOldData(left.getOldData || right.getOldData)
+ .setInconsistent(left.getInconsistent || right.getInconsistent)
+ .setInaccurate(left.getInaccurate || right.getInaccurate)
+ .build()
+ }
+
+ }
+}
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/TimeSource.scala b/calculation/src/main/scala/io/greenbus/calc/lib/TimeSource.scala
new file mode 100755
index 0000000..6635baa
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/TimeSource.scala
@@ -0,0 +1,27 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib
+
+trait TimeSource {
+ def now: Long
+}
+
+object SystemTimeSource extends TimeSource {
+ def now = System.currentTimeMillis()
+}
\ No newline at end of file
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/TimeStrategy.scala b/calculation/src/main/scala/io/greenbus/calc/lib/TimeStrategy.scala
new file mode 100644
index 0000000..74c283f
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/TimeStrategy.scala
@@ -0,0 +1,35 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib
+
+import io.greenbus.client.service.proto.Measurements.Measurement
+
+object TimeStrategy {
+
+ def getTime(inputs: Map[String, Seq[Measurement]]): Long = {
+ val time = inputs.values.flatten.map(_.getTime).foldLeft(0L) {
+ case (l, r) => if (l >= r) l else r
+ }
+ if (time != 0) {
+ time
+ } else {
+ System.currentTimeMillis()
+ }
+ }
+}
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/eval/BasicOperationSource.scala b/calculation/src/main/scala/io/greenbus/calc/lib/eval/BasicOperationSource.scala
new file mode 100755
index 0000000..bfce406
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/eval/BasicOperationSource.scala
@@ -0,0 +1,25 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib.eval
+
+class BasicOperationSource(ops: List[(List[String], () => Operation)]) extends OperationSource {
+ private val map: Map[String, () => Operation] = ops.flatMap { case (names, opFun) => names.map((_, opFun)) }.toMap
+
+ def forName(name: String): Option[Operation] = map.get(name).map { _() }
+}
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/eval/BasicOperations.scala b/calculation/src/main/scala/io/greenbus/calc/lib/eval/BasicOperations.scala
new file mode 100755
index 0000000..eb6f994
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/eval/BasicOperations.scala
@@ -0,0 +1,208 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib.eval
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.calc.lib.eval.OperationPatterns._
+
+object BasicOperations {
+
+ def getSource = {
+ new BasicOperationSource(List(
+ (List("SUM", "+"), () => new Sum),
+ (List("-"), () => new Subtract),
+ (List("PRODUCT", "*"), () => new Product),
+ (List("QUOTIENT", "/"), () => new Divide),
+ (List("POWER", "^"), () => new Power),
+ (List("AVERAGE"), () => new Average),
+ (List("MAX"), () => new Max),
+ (List("MIN"), () => new Min),
+ (List("ABS"), () => new Abs),
+ (List("GT"), () => new Greater),
+ (List("GTE"), () => new Gte),
+ (List("LT"), () => new Less),
+ (List("LTE"), () => new Lte),
+ (List("EQ"), () => new Eq),
+ (List("SQRT"), () => new SquareRoot),
+ (List("COUNT"), () => new Count),
+ (List("AND"), () => new And),
+ (List("OR"), () => new Or),
+ (List("NOT"), () => new Not),
+ (List("IF"), () => new If),
+ (List("LATEST"), () => new Latest),
+ (List("INTEGRATE"), () => new Integrate)))
+ }
+
+ class Sum extends MultiNumericOperation {
+ def eval(args: List[Double]): Double = {
+ args.foldLeft(0.0) { _ + _ }
+ }
+ }
+
+ class Subtract extends PairNumericOperation {
+ def eval(l: Double, r: Double): Double = { l - r }
+ }
+
+ class Product extends MultiNumericOperation {
+ def eval(args: List[Double]): Double = {
+ args.reduceLeft(_ * _)
+ }
+ }
+
+ class Divide extends PairNumericOperation {
+ def eval(l: Double, r: Double): Double = { l / r }
+ }
+
+ class Power extends PairNumericOperation {
+ def eval(l: Double, r: Double): Double = { math.pow(l, r) }
+ }
+
+ class Average extends MultiNumericOperation {
+ def eval(args: List[Double]): Double = {
+ args.foldLeft(0.0) { _ + _ } / args.size
+ }
+ }
+
+ class Max extends MultiNumericOperation {
+ def eval(args: List[Double]): Double = {
+ args.foldLeft(Option.empty[Double]) { (result, value) =>
+ result match {
+ case Some(last) => Some(if (last > value) last else value)
+ case None => Some(value)
+ }
+ }.get
+ }
+ }
+
+ class Min extends MultiNumericOperation {
+ def eval(args: List[Double]): Double = {
+ args.foldLeft(Option.empty[Double]) { (result, value) =>
+ result match {
+ case Some(last) => Some(if (last < value) last else value)
+ case None => Some(value)
+ }
+ }.get
+ }
+ }
+
+ class Abs extends SingleNumericOperation {
+ def eval(v: Double): Double = math.abs(v)
+ }
+
+ class Greater extends ConditionalPairNumericOperation {
+ def eval(l: Double, r: Double) = l > r
+ }
+
+ class Gte extends ConditionalPairNumericOperation {
+ def eval(l: Double, r: Double) = l >= r
+ }
+
+ class Less extends ConditionalPairNumericOperation {
+ def eval(l: Double, r: Double) = l < r
+ }
+
+ class Lte extends ConditionalPairNumericOperation {
+ def eval(l: Double, r: Double) = l <= r
+ }
+
+ class Eq extends ConditionalPairNumericOperation {
+ def eval(l: Double, r: Double) = l == r
+ }
+
+ class SquareRoot extends SingleNumericOperation {
+ def eval(v: Double): Double = math.sqrt(v)
+ }
+
+ class Not extends SingleBooleanOperation {
+ def eval(arg: Boolean) = !arg
+ }
+
+ class And extends BooleanReduceOperation {
+ def eval(args: List[Boolean]) = {
+ args.foldLeft(true) { case (out, v) => out && v }
+ }
+ }
+
+ class Or extends BooleanReduceOperation {
+ def eval(args: List[Boolean]) = {
+ args.foldLeft(false) { case (out, v) => out || v }
+ }
+ }
+
+ class Count extends BooleanFoldOperation {
+ def eval(args: List[Boolean]) = {
+ args.foldLeft(0) { case (sum, v) => sum + (if (v) 1 else 0) }
+ }
+ }
+
+ class If extends AbstractOperation {
+ def apply(args: List[OperationValue]): OperationValue = {
+ args match {
+ case List(condition, consequent, alternative) => {
+ condition match {
+ case BooleanValue(conditionResult) => {
+ if (conditionResult) consequent else alternative
+ }
+ case _ => throw new EvalException("If statement condition must be a boolean expression.")
+ }
+ }
+ case _ => throw new EvalException("If statement must take three arguments: IF(condition, value if true, value if false)")
+ }
+ }
+ }
+
+ class Integrate extends AccumulatedNumericOperation with Logging {
+ def eval(initialValue: AccumulatedValue, args: List[NumericMeas]) = {
+ args.foldLeft(initialValue.copy(value = 0)) {
+ case (state, meas) =>
+ state.lastMeas match {
+ case Some(NumericMeas(v, t)) =>
+
+ val time = meas.time - t
+ if (time < 0) {
+ logger.warn("Measurements out of order. new: " + meas.time + " previous: " + t + " delta: " + time)
+ state
+ } else {
+ val area = if (time > 0) ((meas.doubleValue + v) * time) / 2
+ else 0
+ state.copy(lastMeas = Some(meas), value = state.value + area)
+ }
+ case None =>
+ state.copy(lastMeas = Some(meas))
+ }
+ }
+ }
+ }
+
+ class Latest extends AbstractOperation {
+ def apply(args: List[OperationValue]): OperationValue = {
+ val measurements = args.flatMap {
+ case m: MeasuredValue => Some(m)
+ case _ => None
+ }
+
+ if (measurements.nonEmpty) {
+ measurements.maxBy(_.time)
+ } else {
+ throw new EvalException("Latest function must have measurements as inputs")
+ }
+ }
+
+ }
+}
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/eval/EvalException.scala b/calculation/src/main/scala/io/greenbus/calc/lib/eval/EvalException.scala
new file mode 100644
index 0000000..fe20e3c
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/eval/EvalException.scala
@@ -0,0 +1,21 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib.eval
+
+class EvalException(s: String) extends Exception(s)
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/eval/Expression.scala b/calculation/src/main/scala/io/greenbus/calc/lib/eval/Expression.scala
new file mode 100755
index 0000000..5738f95
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/eval/Expression.scala
@@ -0,0 +1,27 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib.eval
+
+trait Expression {
+ def prepare(ops: OperationSource): PreparedExpression
+}
+
+trait PreparedExpression {
+ def evaluate(inputs: VariableSource): OperationValue
+}
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/eval/Formula.scala b/calculation/src/main/scala/io/greenbus/calc/lib/eval/Formula.scala
new file mode 100755
index 0000000..b816f82
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/eval/Formula.scala
@@ -0,0 +1,48 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib.eval
+
+trait Formula {
+ def evaluate(inputs: VariableSource): OperationValue
+}
+
+class AccumulatedFormula(var lastValue: OperationValue, formula: Formula) extends Formula {
+
+ def evaluate(inputs: VariableSource) = {
+ val incrementalValue = formula.evaluate(inputs)
+ val result = OperationValue.combine(lastValue, incrementalValue)
+ lastValue = result
+ result
+ }
+}
+
+object Formula {
+ def apply(expr: Expression, opSource: OperationSource) = {
+ new BasicFormulaEvaluator(expr, opSource)
+ }
+
+ class BasicFormulaEvaluator(expr: Expression, opSource: OperationSource) extends Formula {
+
+ private val prepared = expr.prepare(opSource)
+
+ def evaluate(inputs: VariableSource): OperationValue = {
+ prepared.evaluate(inputs)
+ }
+ }
+}
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/eval/Operation.scala b/calculation/src/main/scala/io/greenbus/calc/lib/eval/Operation.scala
new file mode 100755
index 0000000..5dbc666
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/eval/Operation.scala
@@ -0,0 +1,42 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib.eval
+/**
+ * Copyright 2011 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+trait Operation {
+
+ def apply(args: List[OperationValue]): OperationValue
+}
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/eval/OperationInterpreter.scala b/calculation/src/main/scala/io/greenbus/calc/lib/eval/OperationInterpreter.scala
new file mode 100755
index 0000000..0dd87d9
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/eval/OperationInterpreter.scala
@@ -0,0 +1,79 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib.eval
+
+object OperationInterpreter {
+
+ private def getOrElse(ops: OperationSource, name: String): Operation = {
+ ops.forName(name).getOrElse { throw new EvalException("Operation not found: " + name) }
+ }
+
+ case class Fun(fun: String, args: List[Expression]) extends Expression {
+ def prepare(ops: OperationSource): PreparedExpression = {
+ PreparedFun(getOrElse(ops, fun), args.map { _.prepare(ops) })
+ }
+ }
+
+ case class PreparedFun(operation: Operation, args: List[PreparedExpression]) extends PreparedExpression {
+ def evaluate(inputs: VariableSource) = {
+ operation.apply(args.map(_.evaluate(inputs)).flatMap(_.toSeq))
+ }
+ }
+
+ case class Infix(op: String, left: Expression, right: Expression) extends Expression {
+ def prepare(ops: OperationSource): PreparedExpression = {
+ PreparedInfix(getOrElse(ops, op), left.prepare(ops), right.prepare(ops))
+ }
+ }
+
+ case class PreparedInfix(operation: Operation, left: PreparedExpression, right: PreparedExpression) extends PreparedExpression {
+ def evaluate(inputs: VariableSource) = {
+ operation.apply(List(left.evaluate(inputs), right.evaluate(inputs)).flatMap(_.toSeq))
+ }
+ }
+
+ trait SimpleExpr extends Expression with PreparedExpression {
+ def prepare(ops: OperationSource) = this
+ }
+
+ case class ConstDouble(v: Double) extends SimpleExpr {
+ def evaluate(inputs: VariableSource): OperationValue = {
+ NumericConst(v)
+ }
+ }
+
+ case class ConstLong(v: Long) extends SimpleExpr {
+ def evaluate(inputs: VariableSource): OperationValue = {
+ LongConst(v)
+ }
+ }
+
+ case class ConstBoolean(v: Boolean) extends SimpleExpr {
+ def evaluate(inputs: VariableSource): OperationValue = {
+ BooleanConst(v)
+ }
+ }
+
+ case class Var(name: String) extends SimpleExpr {
+ def evaluate(inputs: VariableSource): OperationValue = {
+ inputs.forName(name)
+ }
+ }
+
+}
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/eval/OperationParser.scala b/calculation/src/main/scala/io/greenbus/calc/lib/eval/OperationParser.scala
new file mode 100755
index 0000000..c19717a
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/eval/OperationParser.scala
@@ -0,0 +1,86 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib.eval
+
+import util.parsing.combinator.JavaTokenParsers
+
+/**
+ * expr :: = mult { "+" mult | "-" mult }
+ * mult :: = exp { "*" fac | "/" fac | "%" fac }
+ * exp ::= leaf { "^" leaf }
+ * leaf ::= const | fun | var | "(" expr ")"
+ * fun ::= ident "(" csv ")"
+ * csv ::= expr { "," expr }
+ *
+ */
+object OperationParser extends JavaTokenParsers {
+
+ import OperationInterpreter._
+
+ def expr: Parser[Expression] = mult ~ rep("+" ~ mult ^^ part | "-" ~ mult ^^ part) ^^ multiInfix
+
+ def mult: Parser[Expression] = exp ~ rep("*" ~ exp ^^ part | "/" ~ exp ^^ part) ^^ multiInfix
+
+ def exp: Parser[Expression] = leaf ~ rep("^" ~ leaf ^^ part) ^^ multiInfix
+
+ def leaf: Parser[Expression] = constants | fun | variable | "(" ~> expr <~ ")"
+
+ def fun: Parser[Expression] = ident ~ ("(" ~> csv <~ ")") ^^ {
+ case f ~ args => Fun(f, args)
+ }
+
+ def csv: Parser[List[Expression]] = expr ~ rep("," ~> expr) ^^ {
+ case head ~ tail => head :: tail
+ }
+
+ def variable: Parser[Expression] = ident ^^ {
+ Var(_)
+ }
+
+ def constants: Parser[Expression] = doubleConst ||| longConst ||| boolConst
+
+ def longConst: Parser[Expression] = wholeNumber ^^ { x =>
+ ConstLong(x.toLong)
+ }
+ def doubleConst: Parser[Expression] = (decimalNumber | floatingPointNumber) ^^ { x =>
+ ConstDouble(x.toDouble)
+ }
+ def boolConst: Parser[Expression] = """true|false""".r ^^ { x =>
+ ConstBoolean(x.toBoolean)
+ }
+
+ case class Part(op: String, right: Expression)
+
+ def part: ~[String, Expression] => Part = {
+ case op ~ r => Part(op, r)
+ }
+
+ def multiInfix: ~[Expression, List[Part]] => Expression = {
+ case n ~ Nil => n
+ case l ~ reps => reps.foldLeft(l) {
+ case (l, part) => Infix(part.op, l, part.right)
+ }
+ }
+
+ def parseFormula(formula: String): Expression = {
+ parseAll(expr, formula) getOrElse {
+ throw new Exception("Bad Parse: " + formula)
+ }
+ }
+}
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/eval/OperationPatterns.scala b/calculation/src/main/scala/io/greenbus/calc/lib/eval/OperationPatterns.scala
new file mode 100755
index 0000000..a7bd777
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/eval/OperationPatterns.scala
@@ -0,0 +1,139 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib.eval
+
+object OperationPatterns {
+ trait AbstractOperation extends Operation {
+ protected val name = this.getClass.getSimpleName
+ }
+
+ trait MultiNumericOperation extends AbstractOperation {
+ def apply(args: List[OperationValue]): OperationValue = {
+ if (args.size > 0) {
+ val nums = args.map {
+ case NumericValue(v) => v
+ case _ => throw new EvalException("Operation " + name + " only takes numeric values")
+ }
+ NumericConst(eval(nums))
+ } else {
+ throw new EvalException("Operation " + name + " requires one or more value")
+ }
+ }
+
+ def eval(args: List[Double]): Double
+ }
+
+ trait PairNumericOperation extends AbstractOperation {
+ def apply(args: List[OperationValue]): OperationValue = {
+ args match {
+ case List(NumericValue(l), NumericValue(r)) => NumericConst(eval(l, r))
+ case _ => throw new EvalException("Operation " + name + " requires exactly two numeric values")
+ }
+ }
+
+ def eval(l: Double, r: Double): Double
+ }
+
+ trait ConditionalPairNumericOperation extends AbstractOperation {
+ def apply(args: List[OperationValue]): OperationValue = {
+ args match {
+ case List(NumericValue(l), NumericValue(r)) => BooleanConst(eval(l, r))
+ case _ => throw new EvalException("Operation " + name + " requires exactly two numeric values")
+ }
+ }
+
+ def eval(l: Double, r: Double): Boolean
+ }
+
+ trait SingleNumericOperation extends AbstractOperation {
+ def apply(args: List[OperationValue]): OperationValue = {
+ args match {
+ case List(NumericValue(v)) => NumericConst(eval(v))
+ case _ => throw new EvalException("Operation " + name + " requires one numeric value")
+ }
+ }
+
+ def eval(v: Double): Double
+ }
+
+ abstract class BooleanFoldOperation extends AbstractOperation {
+ def apply(args: List[OperationValue]): OperationValue = {
+ if (args.size > 0) {
+ val vals = args.map {
+ case BooleanValue(v) => v
+ case _ => throw new EvalException("Operation " + name + " only takes boolean values")
+ }
+ LongConst(eval(vals))
+ } else {
+ throw new EvalException("Operation " + name + " requires one or more value")
+ }
+ }
+
+ def eval(args: List[Boolean]): Int
+ }
+
+ abstract class SingleBooleanOperation extends AbstractOperation {
+ def apply(args: List[OperationValue]): OperationValue = {
+ args match {
+ case List(BooleanValue(v)) => BooleanConst(eval(v))
+ case _ => throw new EvalException("Operation " + name + " requires one boolean value")
+ }
+ }
+
+ def eval(arg: Boolean): Boolean
+ }
+
+ abstract class BooleanReduceOperation extends AbstractOperation {
+ def apply(args: List[OperationValue]): OperationValue = {
+ if (args.size > 0) {
+ val nums = args.map {
+ case BooleanValue(v) => v
+ case _ => throw new EvalException("Operation " + name + " only takes boolean values")
+ }
+ BooleanConst(eval(nums))
+ } else {
+ throw new EvalException("Operation " + name + " requires one or more value")
+ }
+ }
+
+ def eval(args: List[Boolean]): Boolean
+ }
+
+ trait AccumulatedNumericOperation extends AbstractOperation {
+
+ case class AccumulatedValue(value: Double, lastMeas: Option[NumericMeas])
+
+ var accumulatedValue = AccumulatedValue(0, None)
+
+ def apply(args: List[OperationValue]): OperationValue = {
+ if (args.size > 0) {
+ val nums = args.map {
+ case NumericMeas(v, t) => NumericMeas(v, t)
+ case _ => throw new EvalException("Operation " + name + " only takes numeric values")
+ }
+ accumulatedValue = eval(accumulatedValue, nums)
+ NumericConst(accumulatedValue.value)
+ } else {
+ NumericConst(accumulatedValue.value)
+ }
+ }
+
+ def eval(initialValue: AccumulatedValue, args: List[NumericMeas]): AccumulatedValue
+ }
+}
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/eval/OperationSource.scala b/calculation/src/main/scala/io/greenbus/calc/lib/eval/OperationSource.scala
new file mode 100644
index 0000000..993877c
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/eval/OperationSource.scala
@@ -0,0 +1,41 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib.eval
+/**
+ * Copyright 2011 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+trait OperationSource {
+ def forName(name: String): Option[Operation]
+}
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/eval/OperationValue.scala b/calculation/src/main/scala/io/greenbus/calc/lib/eval/OperationValue.scala
new file mode 100755
index 0000000..e21ac6d
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/eval/OperationValue.scala
@@ -0,0 +1,92 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib.eval
+/**
+ * Copyright 2011 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+sealed trait OperationValue {
+ def toSeq: Seq[OperationValue] = List(this)
+}
+object OperationValue {
+ def combine(a1: OperationValue, a2: OperationValue): OperationValue = {
+ (a1, a2) match {
+ case (NumericValue(v1), NumericValue(v2)) => NumericConst(v1 + v2)
+ case _ => throw new EvalException("Cannot combine " + a1 + " and " + a2)
+ }
+ }
+}
+
+sealed trait MeasuredValue {
+ val time: Long
+}
+
+case class ValueRange(list: Seq[OperationValue]) extends OperationValue {
+ override def toSeq: Seq[OperationValue] = list
+}
+
+trait NumericValue extends OperationValue {
+ def doubleValue: Double
+}
+object NumericValue {
+ def unapply(v: NumericValue): Option[Double] = Some(v.doubleValue)
+}
+case class NumericConst(doubleValue: Double) extends NumericValue
+case class NumericMeas(doubleValue: Double, time: Long) extends NumericValue with MeasuredValue
+
+// we can have both classes defined but can only match on one at a time.
+// weird bug where you can't match on both types in same match block
+// https://issues.scala-lang.org/browse/SI-5081
+// https://issues.scala-lang.org/browse/SI-4832
+trait LongValue extends NumericValue {
+ def longValue: Long
+ def doubleValue = longValue.toDouble
+}
+object LongValue {
+ def unapply(v: LongValue): Option[Long] = Some(v.longValue)
+}
+
+case class LongConst(longValue: Long) extends LongValue
+case class LongMeas(longValue: Long, time: Long) extends LongValue with MeasuredValue
+
+trait BooleanValue extends OperationValue {
+ def value: Boolean
+}
+object BooleanValue {
+ def unapply(v: BooleanValue): Option[Boolean] = Some(v.value)
+}
+
+case class BooleanConst(value: Boolean) extends BooleanValue
+case class BooleanMeas(value: Boolean, time: Long) extends BooleanValue with MeasuredValue
+
diff --git a/calculation/src/main/scala/io/greenbus/calc/lib/eval/VariableSource.scala b/calculation/src/main/scala/io/greenbus/calc/lib/eval/VariableSource.scala
new file mode 100644
index 0000000..bfe550a
--- /dev/null
+++ b/calculation/src/main/scala/io/greenbus/calc/lib/eval/VariableSource.scala
@@ -0,0 +1,41 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib.eval
+/**
+ * Copyright 2011 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+trait VariableSource {
+ def forName(name: String): OperationValue
+}
diff --git a/calculation/src/test/scala/io/greenbus/calc/lib/CalcLibTestHelpers.scala b/calculation/src/test/scala/io/greenbus/calc/lib/CalcLibTestHelpers.scala
new file mode 100755
index 0000000..26e12c9
--- /dev/null
+++ b/calculation/src/test/scala/io/greenbus/calc/lib/CalcLibTestHelpers.scala
@@ -0,0 +1,39 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib
+
+import io.greenbus.client.service.proto.Measurements.{ Quality, Measurement }
+
+object CalcLibTestHelpers {
+
+ def makeTraceMeas(v: Int, time: Long = 0) = {
+ Measurement.newBuilder()
+ .setType(Measurement.Type.INT)
+ .setIntVal(v)
+ .setQuality(Quality.newBuilder)
+ .setTime(time)
+ .build
+ }
+
+ class MockTimeSource(var time: Long = 0) extends TimeSource {
+
+ def now = time
+ }
+}
+
diff --git a/calculation/src/test/scala/io/greenbus/calc/lib/InputBucketTest.scala b/calculation/src/test/scala/io/greenbus/calc/lib/InputBucketTest.scala
new file mode 100755
index 0000000..744b983
--- /dev/null
+++ b/calculation/src/test/scala/io/greenbus/calc/lib/InputBucketTest.scala
@@ -0,0 +1,146 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib
+
+import org.scalatest.FunSuite
+import org.scalatest.matchers.ShouldMatchers
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import io.greenbus.client.service.proto.Measurements.{ Quality, Measurement }
+import scala.collection.immutable.Queue
+
+@RunWith(classOf[JUnitRunner])
+class InputBucketTest extends FunSuite with ShouldMatchers {
+ import InputBucket._
+
+ import CalcLibTestHelpers._
+
+ class MutableBucketFacade(protected var bucket: InputBucket) {
+ def onReceived(m: Measurement) {
+ bucket = bucket.added(m)
+ }
+ def getSnapshot: Option[MeasContainer] = {
+ val (contOpt, buck) = bucket.snapshot()
+ bucket = buck
+ contOpt
+ }
+ }
+
+ test("SinceLastBucket") {
+ val buck = new MutableBucketFacade(new SingleLatestBucket(None))
+ buck.getSnapshot should equal(None)
+
+ val first = makeTraceMeas(1)
+ buck.onReceived(first)
+ buck.getSnapshot should equal(Some(SingleMeas(first)))
+
+ val second = makeTraceMeas(2)
+ buck.onReceived(second)
+ buck.getSnapshot should equal(Some(SingleMeas(second)))
+ }
+
+ test("Limit Bucket for last two values") {
+ val buck = new MutableBucketFacade(new LimitRangeBucket(Queue.empty[Measurement], 2, 2))
+ buck.getSnapshot should equal(None)
+
+ val first = makeTraceMeas(1)
+ buck.onReceived(first)
+ buck.getSnapshot should equal(None)
+
+ val second = makeTraceMeas(2)
+ buck.onReceived(second)
+ buck.getSnapshot should equal(Some(MeasRange(List(first, second))))
+
+ val third = makeTraceMeas(3)
+ buck.onReceived(third)
+ buck.getSnapshot should equal(Some(MeasRange(List(second, third))))
+ }
+
+ test("Limit Bucket with upto 100 values") {
+ val buck = new MutableBucketFacade(new LimitRangeBucket(Queue.empty[Measurement], 100))
+ buck.getSnapshot should equal(None)
+
+ val values = (0 to 199).map { i => makeTraceMeas(i) }
+
+ (0 to 99).foreach { i =>
+ buck.onReceived(values(i))
+ buck.getSnapshot should equal(Some(MeasRange(values.slice(0, i + 1))))
+ }
+ (100 to 199).foreach { i =>
+ buck.onReceived(values(i))
+ buck.getSnapshot should equal(Some(MeasRange(values.slice(i - 99, i + 1))))
+ }
+ }
+
+ test("Time Bucket with upto 100 values") {
+ val timeSource = new MockTimeSource(0)
+ val buck = new MutableBucketFacade(new FromRangeBucket(Queue.empty[Measurement], timeSource, -1000, 100))
+ buck.getSnapshot should equal(None)
+
+ val values = (0 to 199).map { i => makeTraceMeas(i, 0) }
+
+ (0 to 99).foreach { i =>
+ buck.onReceived(values(i))
+ buck.getSnapshot should equal(Some(MeasRange(values.slice(0, i + 1))))
+ }
+ (100 to 199).foreach { i =>
+ buck.onReceived(values(i))
+ buck.getSnapshot should equal(Some(MeasRange(values.slice(i - 99, i + 1))))
+ }
+ }
+
+ test("Time Bucket with expiring measurements") {
+ val timeSource = new MockTimeSource(0)
+ val buck = new MutableBucketFacade(new FromRangeBucket(Queue.empty[Measurement], timeSource, -100, 10000))
+ buck.getSnapshot should equal(None)
+
+ val values = (0 to 199).map { i => makeTraceMeas(i, i) }
+
+ (0 to 99).foreach { i =>
+ timeSource.time = i
+ buck.onReceived(values(i))
+ buck.getSnapshot should equal(Some(MeasRange(values.slice(0, i + 1))))
+ }
+ (100 to 199).foreach { i =>
+ timeSource.time = i
+ buck.onReceived(values(i))
+ buck.getSnapshot should equal(Some(MeasRange(values.slice(i - 99, i + 1))))
+ }
+
+ timeSource.time = 250
+ buck.getSnapshot should equal(Some(MeasRange(values.slice(151, 200))))
+
+ timeSource.time = 298
+ buck.getSnapshot should equal(Some(MeasRange(values.slice(199, 200))))
+
+ timeSource.time = 299
+ buck.getSnapshot should equal(None)
+ }
+
+ test("No Storage Bucket") {
+ val buck = new MutableBucketFacade(new NoStorageBucket(Queue.empty[Measurement], 10000))
+ buck.getSnapshot should equal(None)
+
+ val first = makeTraceMeas(1)
+ buck.onReceived(first)
+ buck.getSnapshot should equal(Some(MeasRange(List(first))))
+ buck.getSnapshot should equal(None)
+ }
+}
+
diff --git a/calculation/src/test/scala/io/greenbus/calc/lib/eval/OperationIntegrationTest.scala b/calculation/src/test/scala/io/greenbus/calc/lib/eval/OperationIntegrationTest.scala
new file mode 100755
index 0000000..a1f7391
--- /dev/null
+++ b/calculation/src/test/scala/io/greenbus/calc/lib/eval/OperationIntegrationTest.scala
@@ -0,0 +1,299 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib.eval
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.FunSuite
+import org.scalatest.matchers.ShouldMatchers
+
+@RunWith(classOf[JUnitRunner])
+class OperationIntegrationTest extends FunSuite with ShouldMatchers {
+
+ def parseFormula(f: String): Formula = {
+ Formula(OperationParser.parseFormula(f), BasicOperations.getSource)
+ }
+
+ test("Constant value") {
+ val f = "8.443"
+ val expr = parseFormula(f)
+ val result = NumericConst(8.443)
+
+ expr.evaluate(new ValueMap(Map())) should equal(result)
+ }
+
+ test("Simple math") {
+ val f = "5 * 2"
+ val expr = parseFormula(f)
+ val result = NumericConst(10.0)
+
+ expr.evaluate(new ValueMap(Map())) should equal(result)
+ }
+
+ test("Average") {
+ val f = "B + AVERAGE(A)"
+ val expr = parseFormula(f)
+ val result = NumericConst(11.5)
+
+ val doubleValues = Map("B" -> 1.5, "A" -> ValueRange(List(NumericConst(5.0), NumericConst(10.0), NumericConst(15.0))))
+ expr.evaluate(new ValueMap(doubleValues)) should equal(result)
+
+ // show that math works when we pass in Longs and cast them up to doubles
+ val longValues = Map("B" -> 1.5, "A" -> ValueRange(List(LongConst(5), LongConst(10), LongConst(15))))
+ expr.evaluate(new ValueMap(longValues)) should equal(result)
+ }
+
+ test("Numeric MAX") {
+
+ val tests = List(
+ (5.0, List(5.0)),
+ (10.0, List(10.0, 5.0)),
+ (-10.0, List(-10.0, -50.0)),
+ (50.0, List(50.0, 49.9, 49.8)))
+
+ testNumeric("MAX(A)", tests)
+ }
+
+ test("Numeric MIN") {
+
+ val tests = List(
+ (5.0, List(5.0)),
+ (5.0, List(10.0, 5.0)),
+ (-50.0, List(-10.0, -50.0)),
+ (49.8, List(50.0, 49.9, 49.8)))
+
+ testNumeric("MIN(A)", tests)
+ }
+
+ test("Numeric ABS") {
+
+ val tests = List(
+ (5.0, List(5.0)),
+ (5.0, List(-5.0)))
+
+ testNumeric("ABS(A)", tests)
+ }
+
+ test("Boolean AND") {
+
+ val tests = List(
+ ("AND(true)", BooleanConst(true)),
+ ("AND(false)", BooleanConst(false)),
+ ("AND(true,true)", BooleanConst(true)),
+ ("AND(true,false)", BooleanConst(false)),
+ ("AND(false,false)", BooleanConst(false)))
+
+ testWithoutValues(tests)
+ }
+
+ test("Boolean OR") {
+
+ val tests = List(
+ ("OR(true)", BooleanConst(true)),
+ ("OR(false)", BooleanConst(false)),
+ ("OR(true,true)", BooleanConst(true)),
+ ("OR(true,false)", BooleanConst(true)),
+ ("OR(false,false)", BooleanConst(false)))
+
+ testWithoutValues(tests)
+ }
+
+ test("Boolean NOT") {
+
+ val tests = List(
+ ("NOT(true)", BooleanConst(false)),
+ ("NOT(false)", BooleanConst(true)))
+
+ testWithoutValues(tests)
+
+ val errorTests = List(
+ ("NOT(true,true)", "requires one"))
+
+ testErrorsWithoutValues(errorTests)
+ }
+
+ test("Boolean COUNT") {
+
+ val tests = List(
+ ("COUNT(true)", LongConst(1)),
+ ("COUNT(false)", LongConst(0)),
+ ("COUNT(true,true)", LongConst(2)),
+ ("COUNT(true,false)", LongConst(1)),
+ ("COUNT(false,false)", LongConst(0)))
+
+ testWithoutValues(tests)
+ }
+
+ test("Boolean GT") {
+
+ val tests = List(
+ ("GT(0,0)", BooleanConst(false)),
+ ("GT(1,0)", BooleanConst(true)),
+ ("GT(5.5,5.4)", BooleanConst(true)),
+ ("GT(-19,-25)", BooleanConst(true)),
+ ("GT(-25,-20)", BooleanConst(false)),
+ ("GT(4,7)", BooleanConst(false)))
+
+ testWithoutValues(tests)
+ }
+
+ test("Boolean GTE") {
+
+ val tests = List(
+ ("GTE(0,0)", BooleanConst(true)),
+ ("GTE(1,0)", BooleanConst(true)),
+ ("GTE(5.5,5.4)", BooleanConst(true)),
+ ("GTE(-19,-25)", BooleanConst(true)),
+ ("GTE(-25,-20)", BooleanConst(false)),
+ ("GTE(4,7)", BooleanConst(false)))
+
+ testWithoutValues(tests)
+ }
+
+ test("Boolean LT") {
+
+ val tests = List(
+ ("LT(0,0)", BooleanConst(false)),
+ ("LT(1,0)", BooleanConst(false)),
+ ("LT(5.5,5.4)", BooleanConst(false)),
+ ("LT(-19,-25)", BooleanConst(false)),
+ ("LT(-25,-20)", BooleanConst(true)),
+ ("LT(4,7)", BooleanConst(true)))
+
+ testWithoutValues(tests)
+ }
+
+ test("Boolean LTE") {
+
+ val tests = List(
+ ("LTE(0,0)", BooleanConst(true)),
+ ("LTE(1,0)", BooleanConst(false)),
+ ("LTE(5.5,5.4)", BooleanConst(false)),
+ ("LTE(-19,-25)", BooleanConst(false)),
+ ("LTE(-25,-20)", BooleanConst(true)),
+ ("LTE(4,7)", BooleanConst(true)))
+
+ testWithoutValues(tests)
+ }
+
+ test("Boolean EQ") {
+
+ val tests = List(
+ ("EQ(0,0)", BooleanConst(true)),
+ ("EQ(-1,-1)", BooleanConst(true)),
+ ("EQ(5.5,5.5)", BooleanConst(true)),
+ ("EQ(5.5,5.4)", BooleanConst(false)),
+ ("EQ(-19,-25)", BooleanConst(false)))
+
+ testWithoutValues(tests)
+ }
+
+ test("Boolean IF") {
+
+ val tests = List(
+ ("IF(true,3,4)", LongConst(3)),
+ ("IF(false,3,4)", LongConst(4)))
+
+ testWithoutValues(tests)
+
+ val errorTests = List(
+ ("IF(1.0,3,4)", "condition must be a boolean expression"),
+ ("IF(3,4)", ""),
+ ("IF(true,4)", ""),
+ ("IF(true)", ""))
+
+ testErrorsWithoutValues(errorTests)
+ }
+
+ test("Numeric INTEGRATE") {
+
+ val f = "INTEGRATE(A)"
+
+ val tests = List(
+ (20 * 5.0, List((5.0, 0), (5.0, 10), (5.0, 20))),
+ (0.0, List((0.0, 0), (0.0, 10), (0.0, 20))),
+ (100.0, List((0.0, 0), (5.0, 10), (10.0, 20))),
+ (100.0, List((10.0, 0), (5.0, 10), (0.0, 20))),
+ (300.0, List((10.0, 0), (10.0, 10), (20.0, 10), (20.0, 20))),
+ (5.0 * 10, List((5.0, 10), (300.0, 8), (5.0, 20)))) // Ignore out of order
+
+ tests.foreach {
+ case (output, inputs) =>
+ val values = Map("A" -> ValueRange(inputs.map { v => NumericMeas(v._1, v._2) }))
+ val result = NumericConst(output)
+ val expr = parseFormula(f)
+ expr.evaluate(new ValueMap(values)) should equal(result)
+ }
+ }
+
+ test("Numeric INTEGRATE (Accumulated)") {
+
+ val f = "INTEGRATE(A)"
+ val expr = new AccumulatedFormula(NumericConst(0), parseFormula(f))
+
+ val tests = List(
+ (10.0, List((5.0, 0), (5.0, 1), (5.0, 2))),
+ (30.0, List((10.0, 2), (10.0, 3), (10.0, 4))),
+ (30.0, List((20.0, 4))),
+ (50.0, List((20.0, 5))))
+
+ tests.foreach {
+ case (output, inputs) =>
+ val values = Map("A" -> ValueRange(inputs.map { v => NumericMeas(v._1, v._2) }))
+ val result = NumericConst(output)
+ expr.evaluate(new ValueMap(values)) should equal(result)
+ }
+ }
+
+ private def testErrorsWithoutValues(errorTests: List[(String, String)]) {
+ errorTests.foreach {
+ case (f, errString) =>
+ val expr = parseFormula(f)
+ intercept[EvalException] {
+ expr.evaluate(new ValueMap(Map()))
+ }.getMessage should include(errString)
+ }
+ }
+
+ private def testWithValues(map: ValueMap, tests: List[(String, OperationValue)]) {
+ tests.foreach {
+ case (f, result) =>
+ val expr = parseFormula(f)
+ expr.evaluate(map) should equal(result)
+ }
+ }
+
+ private def testWithoutValues(tests: List[(String, OperationValue)]) {
+ tests.foreach {
+ case (f, result) =>
+ val expr = parseFormula(f)
+ expr.evaluate(new ValueMap(Map())) should equal(result)
+ }
+ }
+
+ private def testNumeric(f: String, tests: List[(Double, List[Double])]) {
+ tests.foreach {
+ case (output, inputs) =>
+ val values = Map("A" -> ValueRange(inputs.map { NumericConst(_) }))
+ val result = NumericConst(output)
+ val expr = parseFormula(f)
+ expr.evaluate(new ValueMap(values)) should equal(result)
+ }
+ }
+}
diff --git a/calculation/src/test/scala/io/greenbus/calc/lib/eval/OperationInterpreterTest.scala b/calculation/src/test/scala/io/greenbus/calc/lib/eval/OperationInterpreterTest.scala
new file mode 100755
index 0000000..a730b0d
--- /dev/null
+++ b/calculation/src/test/scala/io/greenbus/calc/lib/eval/OperationInterpreterTest.scala
@@ -0,0 +1,78 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib.eval
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.FunSuite
+import org.scalatest.matchers.ShouldMatchers
+
+@RunWith(classOf[JUnitRunner])
+class OperationInterpreterTest extends FunSuite with ShouldMatchers {
+ import OperationInterpreter._
+
+ class OpMap(map: Map[String, Operation]) extends OperationSource {
+ def forName(name: String): Option[Operation] = map.get(name)
+ }
+
+ class Op(n: String, f: List[OperationValue] => OperationValue) extends Operation {
+ def names = List(n)
+ def apply(inputs: List[OperationValue]): OperationValue = f(inputs)
+ }
+
+ test("FunTest") {
+
+ val exp = Fun("SUM", List(Var("A"), Var("B"), ConstDouble(5.0)))
+
+ val ins = Map("A" -> NumericConst(2.0), "B" -> NumericConst(3.0))
+
+ def check(inputs: List[OperationValue]): OperationValue = inputs match {
+ case List(NumericConst(2.0), NumericConst(3.0), NumericConst(5.0)) => NumericConst(10.0)
+ case _ => throw new Exception("not matching")
+ }
+
+ val ops = new OpMap(Map("SUM" -> new Op("SUM", check)))
+
+ exp.prepare(ops).evaluate(new ValueMap(ins)) should equal(NumericConst(10.0))
+ }
+
+ test("Nesting") {
+
+ val exp = Fun("SUM", List(Var("A"), Var("B"), Infix("*", Var("C"), Var("D"))))
+
+ val ins = Map(
+ "A" -> NumericConst(2.0),
+ "B" -> NumericConst(3.0),
+ "C" -> NumericConst(5.0),
+ "D" -> NumericConst(7.0))
+
+ def sum(inputs: List[OperationValue]): OperationValue = inputs match {
+ case List(NumericConst(2.0), NumericConst(3.0), NumericConst(35.0)) => NumericConst(40.0)
+ case _ => throw new Exception("wrong")
+ }
+ def mult: List[OperationValue] => OperationValue = {
+ case List(NumericConst(5.0), NumericConst(7.0)) => NumericConst(35.0)
+ case _ => throw new Exception("wrong")
+ }
+
+ val ops = new OpMap(Map("SUM" -> new Op("SUM", sum), "*" -> new Op("*", mult)))
+
+ exp.prepare(ops).evaluate(new ValueMap(ins)) should equal(NumericConst(40.0))
+ }
+}
diff --git a/calculation/src/test/scala/io/greenbus/calc/lib/eval/OperationParserTest.scala b/calculation/src/test/scala/io/greenbus/calc/lib/eval/OperationParserTest.scala
new file mode 100755
index 0000000..5192609
--- /dev/null
+++ b/calculation/src/test/scala/io/greenbus/calc/lib/eval/OperationParserTest.scala
@@ -0,0 +1,57 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib.eval
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.FunSuite
+import org.scalatest.matchers.ShouldMatchers
+
+@RunWith(classOf[JUnitRunner])
+class OperationParserTest extends FunSuite with ShouldMatchers {
+ import OperationInterpreter._
+
+ test("Recursive Nesting") {
+
+ val f = "5.5 + A * AVG(B, 5 + C, SUM(D, 2.2, 3))"
+
+ val tree = Infix("+", ConstDouble(5.5), Infix("*", Var("A"), Fun("AVG", List(Var("B"), Infix("+", ConstLong(5), Var("C")), Fun("SUM", List(Var("D"), ConstDouble(2.2), ConstLong(3)))))))
+
+ OperationParser.parseFormula(f) should equal(tree)
+ }
+
+ test("Precedence") {
+
+ val f = "5 + 3 / 4 + 7.7"
+
+ val tree = Infix("+", Infix("+", ConstLong(5), Infix("/", ConstLong(3), ConstLong(4))), ConstDouble(7.7))
+
+ OperationParser.parseFormula(f) should equal(tree)
+ }
+
+ test("Literal Handling") {
+
+ val f = "(5 + 5.5) + (true + false)"
+
+ val tree = Infix("+", Infix("+", ConstLong(5), ConstDouble(5.5)), Infix("+", ConstBoolean(true), ConstBoolean(false)))
+
+ OperationParser.parseFormula(f) should equal(tree)
+ }
+
+}
diff --git a/calculation/src/test/scala/io/greenbus/calc/lib/eval/ValueMap.scala b/calculation/src/test/scala/io/greenbus/calc/lib/eval/ValueMap.scala
new file mode 100644
index 0000000..0d6845d
--- /dev/null
+++ b/calculation/src/test/scala/io/greenbus/calc/lib/eval/ValueMap.scala
@@ -0,0 +1,30 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.calc.lib.eval
+
+class ValueMap(input: Map[String, Any]) extends VariableSource {
+ private val map: Map[String, OperationValue] = input.mapValues {
+ case v: OperationValue => v
+ case d: Double => NumericConst(d)
+ case b: Boolean => BooleanConst(b)
+ case _ => throw new Exception("Invalid op value")
+ }
+
+ def forName(name: String): OperationValue = map(name)
+}
\ No newline at end of file
diff --git a/cli/pom.xml b/cli/pom.xml
new file mode 100755
index 0000000..0ee6344
--- /dev/null
+++ b/cli/pom.xml
@@ -0,0 +1,108 @@
+
+ 4.0.0
+
+
+ io.greenbus
+ greenbus-scala-base
+ 3.0.0
+ ../scala-base
+
+
+ greenbus-cli
+ jar
+
+
+ 2.3.1
+ ${karaf.version}
+ 1.4.0
+ GreenBus
+ GreenBus Platform
+ Dedicated to enabling the smart grid.
+
+
+
+
+ AGPLv3
+ http://www.gnu.org/licenses/agpl-3.0.txt
+
+
+
+
+
+
+ ${project.basedir}/src/main/resources
+ true
+
+ **/*
+
+
+
+
+
+ com.mycila.maven-license-plugin
+ maven-license-plugin
+
+
+
+
+
+ maven-assembly-plugin
+
+
+
+ io.greenbus.cli.CliMain
+
+
+
+ jar-with-dependencies
+
+
+
+
+ make-assembly
+ package
+
+ single
+
+
+
+
+
+
+
+
+
+ io.greenbus
+ greenbus-client
+ 3.0.0
+
+
+ io.greenbus
+ greenbus-util
+ 3.0.0
+
+
+ io.greenbus.msg
+ greenbus-msg-qpid
+ 1.0.0
+
+
+ commons-cli
+ commons-cli
+ 1.2
+
+
+ jline
+ jline
+ 2.11
+
+
+ org.fusesource.jansi
+ jansi
+ 1.11
+
+
+
+
+
diff --git a/cli/src/main/resources/io.greenbus.cli.branding/branding.properties b/cli/src/main/resources/io.greenbus.cli.branding/branding.properties
new file mode 100644
index 0000000..cda6ac9
--- /dev/null
+++ b/cli/src/main/resources/io.greenbus.cli.branding/branding.properties
@@ -0,0 +1,21 @@
+#
+# Copyright 2011-2016 Green Energy Corp.
+#
+# Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+# contributor license agreements. See the NOTICE file distributed with this
+# work for additional information regarding copyright ownership. Green Energy
+# Corp licenses this file to you under the GNU Affero General Public License
+# Version 3.0 (the "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.gnu.org/licenses/agpl.html
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+#
+
+
+welcome=\r\n \u001B[32m\u001B[1m _____ ____\u001B[0m\r\n \u001B[32m\u001B[1m / ____| | _ \\ \u001B[0m\r\n \u001B[32m\u001B[1m | | __ _ __ ___ ___ _ __ | |_) |_ _ ___\u001B[0m\r\n \u001B[32m\u001B[1m | | |_ | '__/ _ \\/ _ \\ '_ \\| _ <| | | / __|\u001B[0m\r\n \u001B[32m\u001B[1m | |__| | | | __/ __/ | | | |_) | |_| \\__ \\\u001B[0m\r\n \u001B[32m\u001B[1m \\_____|_| \\___|\\___|_| |_|____/ \\__,_|___/\u001B[0m\r\n\r\n\u001B[1m${app.fullname}\u001B[0m (${project.version})\r\n${app.blurb}\r\n\r\nGreen Energy Corp (\u001B[1mwww.greenenergycorp.com\u001B[0m)\r\n\r\nHit '\u001B[1m\u001B[0m' for a list of available commands\r\nand '\u001B[1mhelp [cmd]\u001B[0m' for help on a specific command.\r\nHit '\u001B[1m\u001B[0m' to close console.\r\n
diff --git a/cli/src/main/scala/io/greenbus/cli/CliContext.scala b/cli/src/main/scala/io/greenbus/cli/CliContext.scala
new file mode 100644
index 0000000..684e225
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/CliContext.scala
@@ -0,0 +1,68 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli
+
+import io.greenbus.msg.Session
+import jline.console.ConsoleReader
+import io.greenbus.client.ServiceConnection
+
+trait CliContext {
+ def reader: ConsoleReader
+ def session: Session
+ def agent: String
+}
+
+trait ManagementCliContext extends CliContext {
+
+ def setSession(sessOpt: Option[Session])
+ def setToken(token: Option[String])
+ def setAgent(agentOpt: Option[String])
+
+ def connection: ServiceConnection
+ def tokenOption: Option[String]
+ def agentOption: Option[String]
+}
+
+object CliContext {
+ def apply(reader: ConsoleReader, connection: ServiceConnection): ManagementCliContext = new DefaultCliContext(reader, connection)
+
+ private class DefaultCliContext(val reader: ConsoleReader, val connection: ServiceConnection) extends ManagementCliContext {
+ private var sess = Option.empty[Session]
+ private var agentName = Option.empty[String]
+ private var tokenString = Option.empty[String]
+
+ def session: Session = sess.getOrElse(throw new IllegalArgumentException("Not logged in."))
+ def agent: String = agentName.getOrElse(throw new IllegalArgumentException("Not logged in."))
+
+ def setSession(sessOpt: Option[Session]) {
+ sess = sessOpt
+ }
+ def setToken(token: Option[String]) {
+ tokenString = token
+ }
+ def setAgent(agentOpt: Option[String]) {
+ agentName = agentOpt
+ }
+
+ def token = tokenString
+
+ def agentOption = agentName
+ def tokenOption = tokenString
+ }
+}
diff --git a/cli/src/main/scala/io/greenbus/cli/CliMain.scala b/cli/src/main/scala/io/greenbus/cli/CliMain.scala
new file mode 100644
index 0000000..244818d
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/CliMain.scala
@@ -0,0 +1,342 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.msg.amqp.util.LoadingException
+import jline.console.ConsoleReader
+import io.greenbus.msg.amqp.AmqpSettings
+import io.greenbus.msg.qpid.QpidBroker
+import io.greenbus.client.{ ServiceHeaders, ServiceConnection }
+import scala.concurrent.duration._
+import scala.concurrent.Await
+import io.greenbus.cli.commands._
+import jline.console.completer.{ NullCompleter, StringsCompleter, ArgumentCompleter }
+import scala.collection.JavaConversions._
+import io.greenbus.util.{ ConfigurationException, UserSettings }
+import java.util.Properties
+import org.fusesource.jansi.AnsiConsole
+import io.greenbus.client.exception.ServiceException
+import io.greenbus.client.version.Version
+import java.util.concurrent.TimeoutException
+import java.io.IOException
+import io.greenbus.msg.SessionUnusableException
+
+object CliMain extends Logging {
+
+ val commandList = List(
+ () => new LoginCommand,
+ () => new LogoutCommand,
+ () => new EntityViewCommand,
+ () => new EntityListCommand,
+ () => new EntityTypeCommand,
+ () => new EntityChildrenCommand,
+ () => new KvListCommand,
+ () => new KvViewCommand,
+ () => new KvSetCommand,
+ () => new KvDeleteCommand,
+ () => new PointListCommand,
+ () => new CommandListCommand,
+ () => new CommandIssueCommand,
+ () => new LockListCommand,
+ () => new LockSelectCommand,
+ () => new LockBlockCommand,
+ () => new LockDeleteCommand,
+ () => new EndpointListCommand,
+ () => new EndpointSubscribeCommand,
+ () => new EndpointEnableCommand,
+ () => new EndpointDisableCommand,
+ () => new AgentListCommand,
+ () => new AgentViewCommand,
+ () => new AgentCreateCommand,
+ () => new AgentModifyCommand,
+ () => new AgentPasswordCommand,
+ () => new AgentPermissionsListCommand,
+ () => new AgentPermissionsViewCommand,
+ () => new MeasListCommand,
+ () => new MeasHistoryCommand,
+ () => new MeasDownloadCommand,
+ () => new MeasSubscribeCommand,
+ () => new MeasBlockCommand,
+ () => new MeasUnblockCommand,
+ () => new MeasReplaceCommand,
+ () => new EventConfigListCommmand,
+ () => new EventConfigViewCommmand,
+ () => new EventConfigPutCommmand,
+ () => new EventConfigDeleteCommmand,
+ () => new EventPostCommmand,
+ () => new EventListCommmand,
+ () => new EventSubscribeCommmand,
+ () => new EventViewCommmand,
+ () => new AlarmListCommand,
+ () => new AlarmSubscribeCommmand,
+ () => new AlarmSilenceCommand,
+ () => new AlarmAckCommand,
+ () => new AlarmRemoveCommand,
+ () => new CalculationListCommand)
+
+ val map: Map[String, () => Command[_ >: ManagementCliContext <: CliContext]] = commandList.map { fac =>
+ (fac().commandName, fac)
+ }.toMap
+
+ def main(args: Array[String]): Unit = {
+ try {
+ if (args.length > 0) {
+ run(execute(args, _, _, _))
+ } else {
+ run(runCli)
+ }
+ } catch {
+ case ex: LoadingException =>
+ System.err.println(ex.getMessage)
+ case ex: IllegalStateException =>
+ System.err.println(ex.getMessage)
+ case ex: Throwable =>
+ logger.error("Unhandled exception: " + ex)
+ System.err.println("Error: " + ex.getMessage)
+ }
+ }
+
+ def run(fun: (ServiceConnection, String, Option[UserSettings]) => Unit) {
+ if (System.getProperty("jline.terminal") == null) {
+ System.setProperty("jline.terminal", "jline.UnsupportedTerminal")
+ }
+
+ val baseDir = Option(System.getProperty("io.greenbus.config.base")).getOrElse("")
+ val amqpConfigPath = Option(System.getProperty("io.greenbus.config.amqp")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.msg.amqp.cfg")
+ val userConfigPath = Option(System.getProperty("io.greenbus.config.user")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.user.cfg")
+
+ val settings = AmqpSettings.load(amqpConfigPath)
+
+ val userSettingsOpt = try {
+ Some(UserSettings.load(userConfigPath))
+ } catch {
+ case ex: ConfigurationException => None
+ }
+
+ val conn = try {
+ ServiceConnection.connect(settings, QpidBroker, 10000)
+ } catch {
+ case ex: Throwable =>
+ logger.error("Broker connection error: " + ex)
+ throw new IllegalStateException(ex.getMessage)
+ }
+
+ try {
+ fun(conn, settings.host, userSettingsOpt)
+
+ } finally {
+ conn.disconnect()
+ }
+
+ }
+
+ def execute(args: Array[String], conn: ServiceConnection, host: String, userOpt: Option[UserSettings]): Unit = {
+ val reader = new ConsoleReader(null, System.in, AnsiConsole.out(), null, "UTF-8")
+ var context = CliContext(reader, conn)
+
+ val userSettings = userOpt.getOrElse(throw new IllegalStateException("Must provide user settings to run single commands."))
+
+ try {
+ val session = Await.result(conn.login(userSettings.user, userSettings.password), 5000.milliseconds)
+ conn.session
+ context.setSession(Some(session))
+ context.setAgent(Some(userSettings.user))
+ context.setToken(session.headers.get(ServiceHeaders.tokenHeader()))
+ } catch {
+ case ex: Throwable =>
+ throw new IllegalStateException("Login failed.")
+ }
+
+ try {
+ val commandName = args(0)
+
+ commandName match {
+ case "version" =>
+ println(Version.clientVersion)
+ case "help" =>
+ if (args.length == 1) {
+ println("Usage: help ")
+ } else {
+ val helpCmdName = args(1)
+
+ helpCmdName match {
+ case "version" => println("Prints build version of client library.")
+ case _ =>
+ map.get(helpCmdName) match {
+ case None => println("Unknown command")
+ case Some(cmdFun) =>
+ val cmd = cmdFun()
+ cmd.printHelpString()
+ }
+ }
+ }
+ case _ =>
+ map.get(commandName) match {
+ case None => println("Unknown command")
+ case Some(cmdFun) =>
+ val cmd = cmdFun()
+ println("")
+ cmd.run(args.drop(1), context)
+ }
+ }
+ println("")
+ } catch {
+ case sue: SessionUnusableException =>
+ println("Session failure: " + sue.getMessage)
+ println("")
+
+ // The broker killing sessions is a detail we hide from the user
+ context.setSession(None)
+ val session = conn.session
+ context.tokenOption.foreach(session.addHeader(ServiceHeaders.tokenHeader(), _))
+ context.setSession(Some(session))
+
+ case ioe: IOException =>
+ println("Communication failure: " + ioe.getMessage)
+ println("")
+ case rse: ServiceException =>
+ println("Request failure: " + rse.getStatus + " - " + rse.getMessage)
+ println("")
+ case te: TimeoutException =>
+ println("Request timeout")
+ println("")
+ case ex: IllegalArgumentException =>
+ println(ex.getMessage)
+ println("")
+ }
+ }
+
+ def runCli(conn: ServiceConnection, host: String, userOpt: Option[UserSettings]) {
+
+ val allCmds = (map.keys.toSeq ++ Seq("login", "logout", "version")).sorted
+
+ val reader = new ConsoleReader(null, System.in, AnsiConsole.out(), null, "UTF-8")
+
+ reader.addCompleter(new ArgumentCompleter(
+ new StringsCompleter(allCmds ++ Seq("help")),
+ new NullCompleter))
+
+ reader.addCompleter(new ArgumentCompleter(
+ new StringsCompleter(Seq("help")),
+ new StringsCompleter(allCmds),
+ new NullCompleter))
+
+ loadTitle(reader)
+
+ var keepGoing = true
+ var context = CliContext(reader, conn)
+
+ userOpt.foreach { userSettings =>
+ reader.println(s"Attempting auto-login for user ${userSettings.user}.")
+
+ try {
+ val session = Await.result(conn.login(userSettings.user, userSettings.password), 5000.milliseconds)
+ conn.session
+ context.setSession(Some(session))
+ context.setAgent(Some(userSettings.user))
+ context.setToken(session.headers.get(ServiceHeaders.tokenHeader()))
+ } catch {
+ case ex: Throwable =>
+ reader.println("Login failed.")
+ }
+ reader.println("")
+ }
+
+ while (keepGoing) {
+ val agentPrompt = context.agentOption.getOrElse("")
+ val prompt = s"\u001B[1m${agentPrompt}\u001B[0m@$host> "
+
+ val lineRead = Option(reader.readLine(prompt)).map(_.trim)
+
+ lineRead match {
+ case None => keepGoing = false
+ case Some("") =>
+ case Some(line) =>
+ try {
+
+ val (cmdArr, rest) = SearchingTokenizer.tokenize(line).toArray.splitAt(1)
+ val commandName = cmdArr(0)
+
+ commandName match {
+ case "version" =>
+ println(Version.clientVersion)
+ case "help" =>
+ if (rest.length == 0) {
+ println("Usage: help ")
+ } else {
+ val helpCmdName = rest(0)
+
+ helpCmdName match {
+ case "version" => println("Prints build version of client library.")
+ case _ =>
+ map.get(helpCmdName) match {
+ case None => println("Unknown command")
+ case Some(cmdFun) =>
+ val cmd = cmdFun()
+ cmd.printHelpString()
+ }
+ }
+ }
+ case _ =>
+ map.get(commandName) match {
+ case None => println("Unknown command")
+ case Some(cmdFun) =>
+ val cmd = cmdFun()
+ println("")
+ cmd.run(rest, context)
+ }
+ }
+ println("")
+ } catch {
+ case sue: SessionUnusableException =>
+ println("Session failure: " + sue.getMessage)
+ println("")
+
+ // The broker killing sessions is a detail we hide from the user
+ context.setSession(None)
+ val session = conn.session
+ context.tokenOption.foreach(session.addHeader(ServiceHeaders.tokenHeader(), _))
+ context.setSession(Some(session))
+
+ case ioe: IOException =>
+ println("Communication failure: " + ioe.getMessage)
+ println("")
+ case rse: ServiceException =>
+ println("Request failure: " + rse.getStatus + " - " + rse.getMessage)
+ println("")
+ case te: TimeoutException =>
+ println("Request timeout")
+ println("")
+ case ex: IllegalArgumentException =>
+ println(ex.getMessage)
+ println("")
+ }
+ }
+ }
+ }
+
+ def loadTitle(reader: ConsoleReader) {
+ val stream = this.getClass.getClassLoader.getResourceAsStream("io.greenbus.cli.branding/branding.properties")
+ val props = new Properties
+ props.load(stream)
+ Option(props.getProperty("welcome")) foreach { s => reader.println(s) }
+ }
+
+}
diff --git a/cli/src/main/scala/io/greenbus/cli/Command.scala b/cli/src/main/scala/io/greenbus/cli/Command.scala
new file mode 100644
index 0000000..1d919d2
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/Command.scala
@@ -0,0 +1,305 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli
+
+import scala.collection.mutable.ListBuffer
+import org.apache.commons.cli.{ CommandLine, Options }
+import org.apache.commons.cli
+import java.io.PrintStream
+
+trait RequiredInputRef[A] {
+ def value: A
+}
+trait OptionalInputRef[A] {
+ def value: Option[A]
+}
+trait SeqInputRef[A] {
+ def value: Seq[A]
+}
+trait SwitchInputRef {
+ def value: Boolean
+}
+
+sealed trait SettableInputRef
+trait SingleRef extends SettableInputRef {
+ def set(in: String)
+}
+trait SeqRef extends SettableInputRef {
+ def set(in: Seq[String])
+}
+trait SwitchRef extends SettableInputRef {
+ def set(v: Boolean)
+}
+
+class RequiredRefContainer[A](parser: String => A) extends RequiredInputRef[A] with SingleRef {
+ protected var v = Option.empty[A]
+ def value: A = v.get
+ def set(in: String) {
+ v = Some(parser(in))
+ }
+}
+class OptionalRefContainer[A](parser: String => A) extends OptionalInputRef[A] with SingleRef {
+ protected var v = Option.empty[A]
+ def value: Option[A] = v
+ def set(in: String) {
+ v = Some(parser(in))
+ }
+}
+class SeqRefContainer[A](parser: String => A) extends SeqInputRef[A] with SeqRef {
+ protected var v = Seq.empty[A]
+ def value: Seq[A] = v
+ def set(in: Seq[String]) {
+ v = in map parser
+ }
+}
+class SwitchRefContainer extends SwitchInputRef with SwitchRef {
+ protected var v = false
+ def value: Boolean = v
+ def set(v: Boolean) {
+ this.v = v
+ }
+}
+
+sealed trait PresenceType
+case object SingleRequired extends PresenceType
+case object SingleOptional extends PresenceType
+case object Sequential extends PresenceType
+case object Switch extends PresenceType
+
+trait ArgumentDesc {
+ val name: String
+ val displayName: String
+ val typ: PresenceType
+}
+
+trait OptionDesc {
+ val shortName: Option[String]
+ val longName: Option[String]
+ val desc: String
+ val typ: PresenceType
+}
+
+case class RegisteredArg(ref: SingleRef, name: String, displayName: String, typ: PresenceType) extends ArgumentDesc
+case class RegisteredRepeatedArg(ref: SeqRef, name: String, displayName: String, typ: PresenceType) extends ArgumentDesc
+case class RegisteredOption(ref: SettableInputRef, shortName: Option[String], longName: Option[String], desc: String, typ: PresenceType) extends OptionDesc
+
+object Command {
+
+ def optionString(regOp: OptionDesc): String = {
+ regOp.shortName.map("-" + _ + " ").getOrElse("") + regOp.longName.map("--" + _).getOrElse("") + (if (regOp.typ == Switch) "" else " ")
+ }
+}
+
+trait Command[ContextType] {
+
+ val commandName: String
+ val description: String
+
+ private val argsRequired = new ListBuffer[RegisteredArg]
+ private val argsOptional = new ListBuffer[RegisteredArg]
+ private var argsRepeated = Option.empty[RegisteredRepeatedArg]
+
+ private val options = new ListBuffer[RegisteredOption]
+ private val optionMap = scala.collection.mutable.Map.empty[String, SettableInputRef]
+
+ def argumentDescriptions: Seq[ArgumentDesc] = argsRequired.toVector ++ argsOptional.toVector ++ argsRepeated.toVector
+ def optionDescriptions: Seq[OptionDesc] = options.toVector
+
+ private def addArg[A](ref: RequiredInputRef[A] with SingleRef, name: String, desc: String): RequiredInputRef[A] = {
+ if (argsOptional.nonEmpty) {
+ throw new IllegalArgumentException("Cannot add a required argument after an optional argument")
+ }
+ if (argsRepeated.nonEmpty) {
+ throw new IllegalArgumentException("Cannot add a required argument after a repeated argument")
+ }
+ argsRequired += RegisteredArg(ref, name, desc, SingleRequired)
+ ref
+ }
+ private def addArgOpt[A](ref: OptionalInputRef[A] with SingleRef, name: String, desc: String): OptionalInputRef[A] = {
+ if (argsRepeated.nonEmpty) {
+ throw new IllegalArgumentException("Cannot add an optional argument after a repeated argument")
+ }
+ argsOptional += RegisteredArg(ref, name, desc, SingleOptional)
+ ref
+ }
+ private def addArgRepeated[A](ref: SeqInputRef[A] with SeqRef, name: String, desc: String): SeqInputRef[A] = {
+ if (argsOptional.nonEmpty) {
+ throw new IllegalArgumentException("Cannot add a repeated argument after an optional argument")
+ }
+ if (argsRepeated.nonEmpty) {
+ throw new IllegalArgumentException("Cannot only have one repeated argument")
+ }
+ argsRepeated = Some(RegisteredRepeatedArg(ref, name, desc, Sequential))
+ ref
+ }
+
+ private def addAnyOption[A <: SettableInputRef](ref: A, short: Option[String], long: Option[String], desc: String, typ: PresenceType): A = {
+ if (short.isEmpty && long.isEmpty) {
+ throw new IllegalArgumentException(s"Must include either a short or long option form for option")
+ }
+ options += RegisteredOption(ref, short, long, desc, typ)
+ short.foreach { s => optionMap += (s -> ref) }
+ long.foreach { s => optionMap += (s -> ref) }
+ ref
+ }
+
+ private def addOption[A](ref: OptionalInputRef[A] with SettableInputRef, short: Option[String], long: Option[String], desc: String): OptionalInputRef[A] = {
+ addAnyOption(ref, short, long, desc, SingleOptional)
+ }
+
+ private def addOptionRepeated[A](ref: SeqInputRef[A] with SettableInputRef, short: Option[String], long: Option[String], desc: String): SeqInputRef[A] = {
+ addAnyOption(ref, short, long, desc, Sequential)
+ }
+
+ private def addOptionSwitch[A](ref: SwitchInputRef with SettableInputRef, short: Option[String], long: Option[String], desc: String): SwitchInputRef = {
+ addAnyOption(ref, short, long, desc, Switch)
+ }
+
+ protected class Registry[A](parser: String => A) {
+ def arg(name: String, desc: String): RequiredInputRef[A] = addArg(new RequiredRefContainer[A](parser), name, desc)
+ def argOptional(name: String, desc: String): OptionalInputRef[A] = addArgOpt(new OptionalRefContainer[A](parser), name, desc)
+ def argRepeated(name: String, desc: String): SeqInputRef[A] = addArgRepeated(new SeqRefContainer[A](parser), name, desc)
+
+ def option(short: Option[String], long: Option[String], desc: String): OptionalInputRef[A] = addOption(new OptionalRefContainer[A](parser), short, long, desc)
+ def optionRepeated(short: Option[String], long: Option[String], desc: String): SeqInputRef[A] = addOptionRepeated(new SeqRefContainer[A](parser), short, long, desc)
+ }
+
+ def optionSwitch(short: Option[String], long: Option[String], desc: String): SwitchInputRef = addOptionSwitch(new SwitchRefContainer, short, long, desc)
+
+ protected val strings = new Registry[String](v => v)
+ protected val ints = new Registry[Int](_.toInt)
+ protected val doubles = new Registry[Double](_.toDouble)
+
+ def printHelpString(stream: PrintStream = System.out) {
+ import stream._
+ println("DESCRIPTION")
+ println(s"\t$commandName")
+ println("")
+ println(s"\t$description")
+ println("")
+ println("SYNTAX")
+ println(s"\t$syntax")
+ println("")
+ if (argsRequired.nonEmpty || argsOptional.nonEmpty || argsRepeated.nonEmpty) {
+ println("ARGUMENTS")
+ (argsRequired ++ argsOptional).foreach { regArg =>
+ println("\t" + regArg.name)
+ println("\t\t" + regArg.displayName)
+ println("")
+ }
+ argsRepeated.foreach { regArg =>
+ println("\t" + regArg.name)
+ println("\t\t" + regArg.displayName)
+ println("")
+ }
+ } else {
+ println("")
+ }
+ if (options.nonEmpty) {
+ println("OPTIONS")
+ options.foreach { regOp =>
+ val optStr = Command.optionString(regOp)
+ println("\t" + optStr)
+ println("\t\t" + regOp.desc)
+ println("")
+ }
+ if (options.isEmpty) {
+ println("")
+ }
+ }
+ }
+
+ def syntax: String = {
+ val optString = if (options.nonEmpty) "[options] " else ""
+
+ commandName + " " +
+ optString +
+ argsRequired.toList.map("<" + _.name + ">").mkString(" ") + " " +
+ argsRepeated.map("[<" + _.name + "> ...]").getOrElse("") +
+ argsOptional.map("[<" + _.name + ">]").mkString(" ")
+ }
+
+ def buildOptions: Options = {
+ val opts = new Options
+
+ options.foreach { regOpt =>
+
+ val shortName = regOpt.shortName.getOrElse(null)
+ val longName = regOpt.longName.getOrElse(null)
+
+ regOpt.ref match {
+ case ref: SingleRef => opts.addOption(new cli.Option(shortName, longName, true, regOpt.desc))
+ case ref: SeqRef => opts.addOption(new cli.Option(shortName, longName, true, regOpt.desc))
+ case ref: SwitchRef => opts.addOption(new cli.Option(shortName, longName, false, regOpt.desc))
+ }
+ }
+
+ opts
+ }
+
+ private def interpretLine(line: CommandLine) {
+
+ options.foreach { regOpt =>
+ val key = (regOpt.shortName orElse regOpt.longName).getOrElse { throw new IllegalStateException("Must have short or long name for option") }
+ if (line.hasOption(key)) {
+ regOpt.ref match {
+ case ref: SingleRef => ref.set(line.getOptionValue(key))
+ case ref: SeqRef => ref.set(line.getOptionValues(key).toSeq)
+ case ref: SwitchRef => ref.set(true)
+ }
+ }
+ }
+
+ val argsList = line.getArgs.toList
+
+ if (argsList.size < argsRequired.size) {
+ throw new IllegalArgumentException("Missing arguments: " + argsRequired.drop(argsList.size).map("<" + _.name + ">").mkString(" "))
+ }
+ val (reqdArgs, extraArgs) = argsList.splitAt(argsRequired.size)
+
+ argsRequired.map(_.ref).zip(reqdArgs).foreach {
+ case (ref: SingleRef, v: String) => ref.set(v)
+ }
+ if (argsOptional.nonEmpty) {
+ argsOptional.map(_.ref).zip(extraArgs).foreach {
+ case (ref: SingleRef, v: String) => ref.set(v)
+ }
+ } else if (argsRepeated.nonEmpty) {
+ argsRepeated.get.ref.set(extraArgs)
+ }
+ }
+
+ def run(args: Array[String], context: ContextType) {
+ val cmdOptions = buildOptions
+ val parser = new cli.BasicParser
+
+ val line = try {
+ parser.parse(cmdOptions, args)
+ } catch {
+ case ex: Throwable =>
+ throw new IllegalArgumentException(ex.getMessage)
+ }
+
+ interpretLine(line)
+
+ execute(context)
+ }
+
+ protected def execute(context: ContextType)
+}
diff --git a/cli/src/main/scala/io/greenbus/cli/LineTokenizer.scala b/cli/src/main/scala/io/greenbus/cli/LineTokenizer.scala
new file mode 100644
index 0000000..1ab0b13
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/LineTokenizer.scala
@@ -0,0 +1,91 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli
+
+import scala.annotation.tailrec
+
+object LineTokenizer {
+
+ val tokenRegex = """".*"\s+|".*"$|\S+\s+|\S+$""".r
+
+ def tokenize(line: String): Seq[String] = {
+
+ @tailrec
+ def findAll(s: String, tokens: List[String]): List[String] = {
+ val in = s.trim
+ tokenRegex.findPrefixOf(in) match {
+ case None => tokens.reverse
+ case Some(rawTok) =>
+ val trimmed = rawTok.trim
+ val tok = if (trimmed.size >= 2 && trimmed.head == '\"' && trimmed.last == '\"') {
+ trimmed.drop(1).dropRight(1)
+ } else {
+ trimmed
+ }
+
+ findAll(in.drop(rawTok.size), tok :: tokens)
+ }
+ }
+
+ findAll(line, Nil)
+ }
+}
+
+object SearchingTokenizer {
+
+ def isQuote(c: Char): Boolean = c == '\"'
+
+ def tokenize(line: String): Seq[String] = {
+
+ def untilSpaceSearch(s: String, tokens: List[String]): List[String] = {
+ val found = s.indexWhere(c => c.isWhitespace)
+ if (found < 0) {
+ (s :: tokens).reverse
+ } else {
+ val tok = s.take(found)
+ inBetweenSearch(s.drop(found + 1), tok :: tokens)
+ }
+ }
+
+ def untilQuoteSearch(s: String, tokens: List[String]): List[String] = {
+ val found = s.indexWhere(c => c == '\"')
+ if (found < 0) {
+ throw new IllegalArgumentException("Unterminated quote in input")
+ } else {
+ val tok = s.take(found)
+ inBetweenSearch(s.drop(found + 1), tok :: tokens)
+ }
+ }
+
+ def inBetweenSearch(s: String, tokens: List[String]): List[String] = {
+ val found = s.indexWhere(c => !c.isWhitespace)
+ if (found < 0) {
+ tokens.reverse
+ } else {
+ if (s.charAt(found) == '\"') {
+ untilQuoteSearch(s.drop(found + 1), tokens)
+ } else {
+ untilSpaceSearch(s.drop(found), tokens)
+ }
+ }
+ }
+
+ inBetweenSearch(line, Nil)
+ }
+}
diff --git a/cli/src/main/scala/io/greenbus/cli/commands/AgentCommands.scala b/cli/src/main/scala/io/greenbus/cli/commands/AgentCommands.scala
new file mode 100644
index 0000000..c822188
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/commands/AgentCommands.scala
@@ -0,0 +1,269 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli.commands
+
+import io.greenbus.cli.view._
+import io.greenbus.cli.{ CliContext, Command }
+import io.greenbus.client.service.AuthService
+import io.greenbus.client.service.proto.Auth.{ Agent, PermissionSet }
+import io.greenbus.client.service.proto.AuthRequests._
+import io.greenbus.client.service.proto.Model.ModelID
+import io.greenbus.client.service.proto.ModelRequests.EntityPagingParams
+
+import scala.collection.JavaConversions._
+import scala.concurrent.Await
+import scala.concurrent.duration._
+
+class AgentListCommand extends Command[CliContext] {
+
+ val commandName = "agent:list"
+ val description = "List all agents"
+
+ protected def execute(context: CliContext) {
+ val authClient = AuthService.client(context.session)
+
+ def results(last: Option[String], pageSize: Int) = {
+
+ val pageB = EntityPagingParams.newBuilder().setPageSize(pageSize)
+ last.foreach(pageB.setLastName)
+
+ authClient.agentQuery(AgentQuery.newBuilder().setPagingParams(pageB.build()).build())
+ }
+
+ def pageBoundary(results: Seq[Agent]): String = results.last.getName
+
+ Paging.page(context.reader, results, pageBoundary, AgentView.printTable, AgentView.printRows)
+ }
+}
+
+object AgentCommon {
+
+ def handleAgentResult(authClient: AuthService, results: Seq[Agent], otherwise: Option[String]) = {
+
+ results.headOption match {
+ case Some(agent) =>
+ val permNames = agent.getPermissionSetsList.toSeq
+
+ val permissionSets = if (permNames.nonEmpty) {
+ val permKeySet = PermissionSetKeySet.newBuilder().addAllNames(permNames).build()
+ Await.result(authClient.getPermissionSets(permKeySet), 5000.milliseconds)
+ } else {
+ Seq()
+ }
+
+ AgentView.viewAgent(agent, permissionSets)
+
+ case None =>
+ otherwise.foreach(println)
+ }
+ }
+}
+
+class AgentCreateCommand extends Command[CliContext] {
+
+ val commandName = "agent:create"
+ val description = "Create an agent"
+
+ val name = strings.arg("user name", "Agent name to create.")
+
+ val passOpt = strings.option(None, Some("password"), "Supply password instead of prompting, useful for scripting. WARNING: Password will be visible in command history.")
+
+ val permissions = strings.optionRepeated(Some("p"), Some("permission-set"), "Attach permission set to agent")
+
+ protected def execute(context: CliContext) {
+ val authClient = AuthService.client(context.session)
+
+ val password = passOpt.value.getOrElse {
+
+ def prompt(retries: Int): String = {
+ if (retries == 0) {
+ throw new IllegalArgumentException("Failed to provide a password")
+ } else {
+
+ println("Enter password: ")
+ val first = readLine()
+ println("Re-enter password: ")
+ val second = readLine()
+ if (first == second) {
+ first
+ } else {
+ println("Passwords did not match.\n")
+ prompt(retries - 1)
+ }
+ }
+ }
+
+ prompt(3)
+ }
+
+ println()
+ val template = AgentTemplate.newBuilder()
+ .setName(name.value)
+ .setPassword(password)
+ .addAllPermissionSets(permissions.value)
+ .build()
+
+ val agentResult = Await.result(authClient.putAgents(Seq(template)), 10000.milliseconds)
+
+ AgentCommon.handleAgentResult(authClient, agentResult, None)
+ }
+}
+
+class AgentModifyCommand extends Command[CliContext] {
+
+ val commandName = "agent:modify"
+ val description = "Modify an agent's permission sets"
+
+ val name = strings.arg("user name", "Agent name to modify.")
+
+ val addPerms = strings.optionRepeated(Some("a"), Some("add"), "Attach permission set to agent")
+ val remPerms = strings.optionRepeated(Some("r"), Some("remove"), "Attach permission set to agent")
+
+ protected def execute(context: CliContext) {
+ val authClient = AuthService.client(context.session)
+
+ val agentGetResult = Await.result(authClient.getAgents(AgentKeySet.newBuilder().addNames(name.value).build()), 5000.milliseconds)
+
+ agentGetResult.headOption match {
+ case None => println("Agent not found.")
+ case Some(agent) =>
+ val currentPermSet = agent.getPermissionSetsList.toSet
+ val withoutRemoves = currentPermSet diff remPerms.value.toSet
+ val withAdds = withoutRemoves ++ addPerms.value
+
+ val template = AgentTemplate.newBuilder()
+ .setUuid(agent.getUuid)
+ .setName(name.value)
+ .addAllPermissionSets(withAdds)
+ .build()
+
+ val putResult = Await.result(authClient.putAgents(Seq(template)), 10000.milliseconds)
+ AgentCommon.handleAgentResult(authClient, putResult, None)
+ }
+ }
+}
+
+class AgentPasswordCommand extends Command[CliContext] {
+
+ val commandName = "agent:password"
+ val description = "Modify an agent's password"
+
+ val name = strings.arg("user name", "Agent name to modify.")
+
+ val passOpt = strings.option(None, Some("password"), "Supply password instead of prompting, useful for scripting. WARNING: Password will be visible in command history.")
+
+ protected def execute(context: CliContext) {
+ val authClient = AuthService.client(context.session)
+
+ val password = passOpt.value.getOrElse {
+
+ def prompt(retries: Int): String = {
+ if (retries == 0) {
+ throw new IllegalArgumentException("Failed to provide a password")
+ } else {
+
+ println("Enter password: ")
+ val first = readLine()
+ println("Re-enter password: ")
+ val second = readLine()
+ if (first == second) {
+ first
+ } else {
+ println("Passwords did not match.\n")
+ prompt(retries - 1)
+ }
+ }
+ }
+
+ prompt(3)
+ }
+
+ val agentGetResult = Await.result(authClient.getAgents(AgentKeySet.newBuilder().addNames(name.value).build()), 5000.milliseconds)
+
+ agentGetResult.headOption match {
+ case None => println("Agent not found.")
+ case Some(agent) =>
+ val update = AgentPasswordUpdate.newBuilder()
+ .setUuid(agent.getUuid)
+ .setPassword(password)
+ .build()
+
+ val putResult = Await.result(authClient.putAgentPasswords(Seq(update)), 10000.milliseconds)
+ AgentCommon.handleAgentResult(authClient, putResult, None)
+ }
+ }
+}
+
+class AgentViewCommand extends Command[CliContext] {
+
+ val commandName = "agent:view"
+ val description = "View details of agent"
+
+ val setName = strings.arg("name", "Agent name")
+
+ protected def execute(context: CliContext) {
+ val authClient = AuthService.client(context.session)
+
+ val agentResult = Await.result(authClient.getAgents(AgentKeySet.newBuilder().addNames(setName.value).build()), 5000.milliseconds)
+ AgentCommon.handleAgentResult(authClient, agentResult, Some("Agent not found."))
+ }
+}
+
+class AgentPermissionsListCommand extends Command[CliContext] {
+
+ val commandName = "agent-permissions:list"
+ val description = "List all permission sets"
+
+ protected def execute(context: CliContext) {
+ val authClient = AuthService.client(context.session)
+
+ def results(last: Option[ModelID], pageSize: Int) = {
+ val queryBuilder = PermissionSetQuery.newBuilder().setPageSize(pageSize)
+ last.foreach(queryBuilder.setLastId)
+ val query = queryBuilder.build()
+
+ authClient.permissionSetQuery(query)
+ }
+
+ def pageBoundary(results: Seq[PermissionSet]) = results.last.getId
+
+ Paging.page(context.reader, results, pageBoundary, PermissionSetView.printTable, PermissionSetView.printRows)
+ }
+}
+
+class AgentPermissionsViewCommand extends Command[CliContext] {
+
+ val commandName = "agent-permissions:view"
+ val description = "View permissions in a permission set"
+
+ val setName = strings.arg("name", "Permission set name")
+
+ protected def execute(context: CliContext) {
+ val authClient = AuthService.client(context.session)
+
+ val result = Await.result(authClient.getPermissionSets(PermissionSetKeySet.newBuilder().addNames(setName.value).build()), 5000.milliseconds)
+
+ result.headOption match {
+ case None => println("Permission set not found")
+ case Some(set) =>
+ PermissionSetView.viewPermissionSet(set)
+ }
+
+ }
+}
diff --git a/cli/src/main/scala/io/greenbus/cli/commands/CalculationCommands.scala b/cli/src/main/scala/io/greenbus/cli/commands/CalculationCommands.scala
new file mode 100644
index 0000000..85243f0
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/commands/CalculationCommands.scala
@@ -0,0 +1,95 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli.commands
+
+import io.greenbus.cli.view.CalculationView
+import io.greenbus.cli.{ CliContext, Command }
+import io.greenbus.client.service.ModelService
+import io.greenbus.client.service.proto.Calculations.CalculationDescriptor
+import io.greenbus.client.service.proto.Model.{ Entity, ModelUUID }
+import io.greenbus.client.service.proto.ModelRequests.{ EntityPagingParams, EndpointQuery, EntityKeyPair, EntityRelationshipFlatQuery }
+
+import scala.collection.JavaConversions._
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.duration._
+import scala.concurrent.{ Await, Future }
+
+class CalculationListCommand extends Command[CliContext] {
+
+ val commandName = "calc:list"
+ val description = "List all calculations"
+
+ protected def execute(context: CliContext) {
+ val modelClient = ModelService.client(context.session)
+
+ val calcEndpoints = Await.result(modelClient.endpointQuery(EndpointQuery.newBuilder().addProtocols("calculator").build()), 5000.milliseconds)
+
+ val endpointUuids = calcEndpoints.map(_.getUuid)
+
+ def allQueryResults(last: Option[ModelUUID], pageSize: Int): Future[Seq[(Entity, Option[CalculationDescriptor])]] = {
+
+ val queryB = EntityRelationshipFlatQuery.newBuilder()
+ .addAllStartUuids(endpointUuids)
+ .setRelationship("source")
+ .setDescendantOf(true)
+ .addEndTypes("Point")
+
+ val pageParamsB = EntityPagingParams.newBuilder()
+ .setPageSize(pageSize)
+
+ last.foreach(pageParamsB.setLastUuid)
+
+ val query = queryB.setPagingParams(pageParamsB).build()
+
+ modelClient.relationshipFlatQuery(query).flatMap {
+ case Seq() => Future.successful(Nil)
+ case points =>
+ val uuidKeys = points.map { p =>
+ EntityKeyPair.newBuilder().setUuid(p.getUuid).setKey("calculation").build()
+ }
+
+ modelClient.getEntityKeyValues(uuidKeys).map { kvs =>
+
+ val pointToCalcMap: Map[ModelUUID, CalculationDescriptor] = kvs.flatMap { kv =>
+ val calcDescOpt = if (kv.hasValue && kv.getValue.hasByteArrayValue) {
+ try {
+ Some(CalculationDescriptor.parseFrom(kv.getValue.getByteArrayValue))
+ } catch {
+ case ex: Throwable => None
+ }
+ } else {
+ None
+ }
+ calcDescOpt.map(c => (kv.getUuid, c))
+ }.toMap
+
+ points.map(p => (p, pointToCalcMap.get(p.getUuid)))
+ }
+ }
+ }
+
+ def pageBoundary(results: Seq[(Entity, Option[CalculationDescriptor])]) = results.last._1.getUuid
+
+ if (endpointUuids.nonEmpty) {
+ Paging.page(context.reader, allQueryResults, pageBoundary, CalculationView.printTable, CalculationView.printRows)
+ } else {
+ CalculationView.printTable(Nil)
+ }
+ }
+}
diff --git a/cli/src/main/scala/io/greenbus/cli/commands/CommandCommands.scala b/cli/src/main/scala/io/greenbus/cli/commands/CommandCommands.scala
new file mode 100644
index 0000000..6a5c936
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/commands/CommandCommands.scala
@@ -0,0 +1,298 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli.commands
+
+import io.greenbus.msg.Session
+import io.greenbus.cli.view.CommandLockView
+import io.greenbus.cli.{ CliContext, Command }
+import io.greenbus.client.service.proto.AuthRequests.AgentKeySet
+import io.greenbus.client.service.proto.CommandRequests.{ CommandBlock, CommandLockQuery, CommandSelect }
+import io.greenbus.client.service.proto.Commands.{ CommandLock, CommandRequest }
+import io.greenbus.client.service.proto.Model.ModelID
+import io.greenbus.client.service.proto.ModelRequests.EntityKeySet
+import io.greenbus.client.service.{ AuthService, CommandService, ModelService }
+
+import scala.collection.JavaConversions._
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.duration._
+import scala.concurrent.{ Await, Future }
+
+object CommandIssueCommand {
+
+ def interpretValue(b: CommandRequest.Builder, s: String) {
+
+ def asOpt[A](f: => A): Option[A] = {
+ try { Some(f) } catch { case _: NumberFormatException => None }
+ }
+
+ val parsed = asOpt(s.toInt) orElse
+ asOpt(s.toDouble) getOrElse (s)
+
+ parsed match {
+ case v: Int =>
+ b.setIntVal(v); b.setType(CommandRequest.ValType.INT)
+ case v: Double =>
+ b.setDoubleVal(v); b.setType(CommandRequest.ValType.DOUBLE)
+ case v: String => b.setStringVal(v); b.setType(CommandRequest.ValType.STRING)
+ }
+ }
+}
+
+class CommandIssueCommand extends Command[CliContext] {
+
+ val commandName = "command:issue"
+ val description = "Select commands for execution"
+
+ val commandToIssue = strings.arg("command name", "Command name")
+ val commandValue = strings.argOptional("command value", "Command value")
+
+ protected def execute(context: CliContext) {
+ val modelClient = ModelService.client(context.session)
+ val commandClient = CommandService.client(context.session)
+
+ if (commandToIssue.value.isEmpty) {
+ throw new IllegalArgumentException("Must include command to issue")
+ }
+
+ val cmdFut = modelClient.getCommands(EntityKeySet.newBuilder().addNames(commandToIssue.value).build())
+
+ val cmdResults = Await.result(cmdFut, 5000.milliseconds)
+
+ if (cmdResults.isEmpty) {
+ throw new IllegalArgumentException("Commands not found")
+ }
+
+ val cmdUuid = cmdResults.head.getUuid
+
+ val b = CommandRequest.newBuilder()
+ .setCommandUuid(cmdUuid)
+
+ commandValue.value.foreach(CommandIssueCommand.interpretValue(b, _))
+
+ val cmdReq = b.build()
+
+ // Can fail because the front end isn't there, server will send ServiceException with timeout
+ val result = Await.result(commandClient.issueCommandRequest(cmdReq), 10000.milliseconds)
+
+ val errorStr = if (result.hasErrorMessage) " - " + result.getErrorMessage else ""
+
+ println(result.getStatus.toString + errorStr)
+ }
+}
+
+object LocksCommands {
+
+ def lockToRow(session: Session, cmdLocks: Seq[CommandLock]): Future[Seq[(CommandLock, Seq[String], String)]] = {
+ val modelClient = ModelService.client(session)
+
+ if (cmdLocks.isEmpty) {
+ Future.successful(Nil)
+ } else {
+
+ val agentUuids = cmdLocks.map(_.getAgentUuid).distinct
+ val cmdUuids = cmdLocks.flatMap(_.getCommandUuidsList).distinct
+
+ val keys = EntityKeySet.newBuilder().addAllUuids(agentUuids ++ cmdUuids).build()
+
+ modelClient.get(keys).map { ents =>
+ val uuidToName = ents.map(e => (e.getUuid, e.getName)).toMap
+
+ cmdLocks.map { lock =>
+ val cmdNames = lock.getCommandUuidsList.toList.flatMap(uuidToName.get)
+ val agentName = uuidToName.get(lock.getAgentUuid).getOrElse("unknown")
+ (lock, cmdNames, agentName)
+ }
+ }
+ }
+ }
+}
+
+class LockListCommand extends Command[CliContext] {
+
+ val commandName = "lock:list"
+ val description = "List command locks"
+
+ val commandNames = strings.optionRepeated(Some("c"), Some("command"), "Command names")
+
+ val agentNames = strings.optionRepeated(Some("a"), Some("agent"), "Agent names")
+
+ val isSelect = optionSwitch(Some("s"), Some("select"), "List command selects only")
+
+ val isBlock = optionSwitch(Some("b"), Some("block"), "List command blocks only")
+
+ protected def execute(context: CliContext) {
+ val modelClient = ModelService.client(context.session)
+ val commandClient = CommandService.client(context.session)
+
+ val agentUuids = if (agentNames.value.nonEmpty) {
+ val authClient = AuthService.client(context.session)
+
+ val keys = AgentKeySet.newBuilder().addAllNames(agentNames.value).build()
+ val agents = Await.result(authClient.getAgents(keys), 5000.milliseconds)
+
+ if (agents.isEmpty) {
+ throw new IllegalArgumentException("Agents specified not found")
+ }
+
+ agents.map(_.getUuid)
+ } else {
+ Nil
+ }
+
+ val commandUuids = if (commandNames.value.nonEmpty) {
+ val modelClient = ModelService.client(context.session)
+
+ val keys = EntityKeySet.newBuilder().addAllNames(commandNames.value).build()
+ val commands = Await.result(modelClient.getCommands(keys), 5000.milliseconds)
+
+ if (commands.isEmpty) {
+ throw new IllegalArgumentException("Commands specified not found")
+ }
+
+ commands.map(_.getUuid)
+ } else {
+ Nil
+ }
+
+ if (isSelect.value && isBlock.value) {
+ throw new IllegalArgumentException("Can choose only one of select or block")
+ }
+
+ def getResults(last: Option[ModelID], pageSize: Int) = {
+ val b = CommandLockQuery.newBuilder()
+
+ if (isSelect.value) b.setAccess(CommandLock.AccessMode.ALLOWED)
+ if (isBlock.value) b.setAccess(CommandLock.AccessMode.BLOCKED)
+
+ if (agentUuids.nonEmpty) b.addAllAgentUuids(agentUuids)
+ if (commandUuids.nonEmpty) b.addAllCommandUuids(commandUuids)
+
+ last.foreach(b.setLastId)
+ b.setPageSize(pageSize)
+
+ val lockFut = commandClient.commandLockQuery(b.build())
+
+ lockFut.flatMap { cmdLocks => LocksCommands.lockToRow(context.session, cmdLocks) }
+ }
+
+ def pageBoundary(results: Seq[(CommandLock, Seq[String], String)]) = {
+ results.last._1.getId
+ }
+
+ Paging.page(context.reader, getResults, pageBoundary, CommandLockView.printTable, CommandLockView.printRows)
+ }
+}
+
+class LockSelectCommand extends Command[CliContext] {
+
+ val commandName = "lock:select"
+ val description = "Select commands for execution"
+
+ val duration = ints.option(Some("t"), Some("expiry"), "Timeout duration in milliseconds. Default 30000.")
+
+ val commandNames = strings.argRepeated("command name", "Command names")
+
+ protected def execute(context: CliContext) {
+ val modelClient = ModelService.client(context.session)
+ val commandClient = CommandService.client(context.session)
+
+ val durationMs = duration.value.getOrElse(30000)
+
+ if (commandNames.value.isEmpty) {
+ throw new IllegalArgumentException("Must include at least one command")
+ }
+
+ val cmdNames = commandNames.value
+ val cmdFut = modelClient.getCommands(EntityKeySet.newBuilder().addAllNames(cmdNames).build())
+
+ val cmdResults = Await.result(cmdFut, 5000.milliseconds)
+
+ if (cmdResults.isEmpty) {
+ throw new IllegalArgumentException("Commands not found")
+ }
+
+ val select = CommandSelect.newBuilder()
+ .addAllCommandUuids(cmdResults.map(_.getUuid))
+ .setExpireDuration(durationMs)
+ .build()
+
+ val lock = Await.result(commandClient.selectCommands(select), 5000.milliseconds)
+
+ CommandLockView.printInspect(lock, cmdResults.map(_.getName), context.agent)
+
+ }
+}
+
+class LockBlockCommand extends Command[CliContext] {
+
+ val commandName = "lock:block"
+ val description = "Block commands from being execution"
+
+ val commandNames = strings.argRepeated("command name", "Command names")
+
+ protected def execute(context: CliContext) {
+ val modelClient = ModelService.client(context.session)
+ val commandClient = CommandService.client(context.session)
+
+ if (commandNames.value.isEmpty) {
+ throw new IllegalArgumentException("Must include at least one command")
+ }
+
+ val cmdNames = commandNames.value
+ val cmdFut = modelClient.getCommands(EntityKeySet.newBuilder().addAllNames(cmdNames).build())
+
+ val cmdResults = Await.result(cmdFut, 5000.milliseconds)
+
+ if (cmdResults.isEmpty) {
+ throw new IllegalArgumentException("Commands not found")
+ }
+
+ val block = CommandBlock.newBuilder()
+ .addAllCommandUuids(cmdResults.map(_.getUuid))
+ .build()
+
+ val lock = Await.result(commandClient.blockCommands(block), 5000.milliseconds)
+
+ CommandLockView.printInspect(lock, cmdResults.map(_.getName), context.agent)
+ }
+}
+
+class LockDeleteCommand extends Command[CliContext] {
+
+ val commandName = "lock:delete"
+ val description = "Delete command locks"
+
+ val commandIds = strings.argRepeated("command name", "Command names")
+
+ protected def execute(context: CliContext) {
+ val commandClient = CommandService.client(context.session)
+
+ if (commandIds.value.isEmpty) {
+ throw new IllegalArgumentException("Must include command IDs to delete")
+ }
+
+ val ids = commandIds.value.map(id => ModelID.newBuilder().setValue(id).build())
+
+ val rowFut = commandClient.deleteCommandLocks(ids).flatMap(LocksCommands.lockToRow(context.session, _))
+
+ val rows = Await.result(rowFut, 5000.milliseconds)
+
+ CommandLockView.printTable(rows.toList)
+ }
+}
diff --git a/cli/src/main/scala/io/greenbus/cli/commands/EntityCommands.scala b/cli/src/main/scala/io/greenbus/cli/commands/EntityCommands.scala
new file mode 100644
index 0000000..975bc0e
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/commands/EntityCommands.scala
@@ -0,0 +1,150 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli.commands
+
+import io.greenbus.cli.{ CliContext, Command }
+import io.greenbus.cli.view._
+import scala.concurrent.Await
+import io.greenbus.client.service.proto.ModelRequests._
+import scala.concurrent.duration._
+import io.greenbus.client.service.ModelService
+import io.greenbus.client.service.proto.Model.{ Entity, ModelUUID }
+import scala.collection.JavaConversions._
+
+class EntityListCommand extends Command[CliContext] {
+
+ val commandName = "entity:list"
+ val description = "List all entities"
+
+ protected def execute(context: CliContext) {
+ val client = ModelService.client(context.session)
+
+ def getResults(last: Option[ModelUUID], pageSize: Int) = {
+ val pageBuilder = EntityPagingParams.newBuilder().setPageSize(pageSize)
+ last.foreach(pageBuilder.setLastUuid)
+ client.entityQuery(EntityQuery.newBuilder().setPagingParams(pageBuilder).build())
+ }
+
+ def pageBoundary(results: Seq[Entity]) = {
+ results.last.getUuid
+ }
+
+ Paging.page(context.reader, getResults, pageBoundary, EntityView.printTable, EntityView.printRows)
+ }
+}
+
+class EntityViewCommand extends Command[CliContext] {
+
+ val commandName = "entity:view"
+ val description = "View details of an entity specified by name"
+
+ val name = strings.arg("entity name", "Entity name")
+
+ protected def execute(context: CliContext) {
+ val client = ModelService.client(context.session)
+
+ val result = Await.result(client.get(EntityKeySet.newBuilder().addNames(name.value).build()), 5000.milliseconds)
+
+ result.headOption.map(EntityView.printInspect).getOrElse(println("No entities found."))
+ }
+}
+
+class EntityTypeCommand extends Command[CliContext] {
+
+ val commandName = "entity:type"
+ val description = "List entities that include any of the given types"
+
+ val types = strings.argRepeated("entity type", "Entity type to search for")
+
+ protected def execute(context: CliContext) {
+
+ if (types.value.isEmpty) {
+ throw new IllegalArgumentException("Must include at least one type")
+ }
+
+ val client = ModelService.client(context.session)
+
+ def getResults(last: Option[ModelUUID], pageSize: Int) = {
+ val pageBuilder = EntityPagingParams.newBuilder().setPageSize(pageSize)
+ last.foreach(pageBuilder.setLastUuid)
+
+ val query = EntityQuery.newBuilder
+ .setTypeParams(EntityTypeParams.newBuilder()
+ .addAllIncludeTypes(types.value))
+ .setPagingParams(pageBuilder)
+ .build()
+
+ client.entityQuery(query)
+ }
+
+ def pageBoundary(results: Seq[Entity]) = {
+ results.last.getUuid
+ }
+
+ Paging.page(context.reader, getResults, pageBoundary, EntityView.printTable, EntityView.printRows)
+ }
+}
+
+class EntityChildrenCommand extends Command[CliContext] {
+
+ val commandName = "entity:children"
+ val description = "List children of specified entity "
+
+ val name = strings.arg("entity name", "Entity name")
+
+ val relation = strings.option(Some("r"), Some("relation"), "Relationship to children (default: \"owns\")")
+
+ val childTypes = strings.optionRepeated(Some("t"), Some("child-type"), "Child types to include")
+
+ val depthLimit = ints.option(Some("d"), Some("depth"), "Depth limit for children")
+
+ protected def execute(context: CliContext) {
+
+ val relationship = relation.value.getOrElse("owns")
+
+ val client = ModelService.client(context.session)
+
+ def getResults(last: Option[ModelUUID], pageSize: Int) = {
+ val queryBuilder = EntityRelationshipFlatQuery.newBuilder
+ .addStartNames(name.value)
+ .setDescendantOf(true)
+ .setRelationship(relationship)
+
+ if (childTypes.value.nonEmpty) {
+ queryBuilder.addAllEndTypes(childTypes.value)
+ }
+
+ depthLimit.value.foreach(queryBuilder.setDepthLimit)
+
+ val pageParamsB = EntityPagingParams.newBuilder()
+ .setPageSize(pageSize)
+
+ last.foreach(pageParamsB.setLastUuid)
+
+ val query = queryBuilder.setPagingParams(pageParamsB).build()
+ client.relationshipFlatQuery(query)
+ }
+
+ def pageBoundary(results: Seq[Entity]) = {
+ results.last.getUuid
+ }
+
+ Paging.page(context.reader, getResults, pageBoundary, EntityView.printTable, EntityView.printRows)
+ }
+}
diff --git a/cli/src/main/scala/io/greenbus/cli/commands/EventCommands.scala b/cli/src/main/scala/io/greenbus/cli/commands/EventCommands.scala
new file mode 100644
index 0000000..3d70cd4
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/commands/EventCommands.scala
@@ -0,0 +1,476 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli.commands
+
+import io.greenbus.cli.{ CliContext, Command }
+import io.greenbus.cli.view._
+import scala.concurrent.Await
+import scala.concurrent.duration._
+import io.greenbus.client.service.{ ModelService, EventService }
+import io.greenbus.client.service.proto.EventRequests._
+import io.greenbus.client.service.proto.Events.{ Alarm, EventConfig, Attribute }
+import io.greenbus.client.service.proto.ModelRequests.EntityKeySet
+import io.greenbus.client.service.proto.Model.ModelID
+import scala.collection.JavaConversions._
+import java.util.concurrent.atomic.AtomicReference
+
+class EventConfigListCommmand extends Command[CliContext] {
+
+ val commandName = "event-config:list"
+ val description = "List all event configurations"
+
+ protected def execute(context: CliContext) {
+ val client = EventService.client(context.session)
+
+ def getResults(last: Option[String], pageSize: Int) = {
+ val queryBuilder = EventConfigQuery.newBuilder().setPageSize(pageSize)
+ last.foreach(queryBuilder.setLastEventType)
+ val query = queryBuilder.build()
+ client.eventConfigQuery(query)
+ }
+
+ def pageBoundary(results: Seq[EventConfig]) = {
+ results.last.getEventType
+ }
+
+ Paging.page(context.reader, getResults, pageBoundary, EventConfigView.printTable, EventConfigView.printRows)
+ }
+}
+
+class EventConfigViewCommmand extends Command[CliContext] {
+
+ val commandName = "event-config:view"
+ val description = "View particular event configurations"
+
+ val eventTypes = strings.argRepeated("event type", "Event type")
+
+ protected def execute(context: CliContext) {
+ val client = EventService.client(context.session)
+
+ if (eventTypes.value.isEmpty) {
+ throw new IllegalArgumentException("Must include at least one event type")
+ }
+
+ val results = Await.result(client.getEventConfigs(eventTypes.value), 5000.milliseconds)
+ EventConfigView.printTable(results.toList)
+ }
+}
+
+class EventConfigDeleteCommmand extends Command[CliContext] {
+
+ val commandName = "event-config:delete"
+ val description = "Delete event configurations"
+
+ val eventTypes = strings.argRepeated("event type", "Event type")
+
+ protected def execute(context: CliContext) {
+ val client = EventService.client(context.session)
+
+ if (eventTypes.value.isEmpty) {
+ throw new IllegalArgumentException("Must include at least one event type")
+ }
+
+ val results = Await.result(client.deleteEventConfigs(eventTypes.value), 5000.milliseconds)
+ EventConfigView.printTable(results.toList)
+ }
+}
+
+class EventConfigPutCommmand extends Command[CliContext] {
+
+ val commandName = "event-config:put"
+ val description = "Create/modify event configurations"
+
+ val eventType = strings.arg("event type", "Event type")
+ val resource = strings.arg("resource", "Resource string to be rendered for events")
+
+ val severity = ints.option(Some("s"), Some("severity"), "Severity of processed events")
+
+ val alarm = optionSwitch(Some("a"), Some("alarm"), "Event generates an alarm")
+
+ val log = optionSwitch(Some("l"), Some("log"), "Event is a log entry")
+
+ val silent = optionSwitch(Some("q"), Some("silent"), "Alarm is silent (used with --alarm)")
+
+ protected def execute(context: CliContext) {
+ val client = EventService.client(context.session)
+
+ val designation = (alarm.value, log.value) match {
+ case (true, true) => throw new IllegalArgumentException("Cannot set both --alarm and --log flags.")
+ case (true, false) => EventConfig.Designation.ALARM
+ case (false, true) => EventConfig.Designation.LOG
+ case (false, false) => EventConfig.Designation.EVENT
+ }
+
+ if (!alarm.value && silent.value) {
+ throw new IllegalArgumentException("Cannot use --silent when --alarm not set")
+ }
+
+ val alarmState = if (silent.value) Alarm.State.UNACK_SILENT else Alarm.State.UNACK_AUDIBLE
+
+ val template = EventConfigTemplate.newBuilder()
+ .setEventType(eventType.value)
+ .setResource(resource.value)
+ .setSeverity(severity.value getOrElse 4)
+ .setDesignation(designation)
+ .setAlarmState(alarmState)
+ .build()
+
+ val results = Await.result(client.putEventConfigs(List(template)), 5000.milliseconds)
+ EventConfigView.printTable(results.toList)
+ }
+}
+
+class EventPostCommmand extends Command[CliContext] {
+
+ val commandName = "event:post"
+ val description = "Post an event. Refer to event configs for types and arguments."
+
+ val eventType = strings.arg("event type", "Event type")
+
+ val subsystem = strings.option(None, Some("subsystem"), "Subsystem event applies to")
+
+ val entity = strings.option(None, Some("entity"), "Entity name event applies to")
+
+ val args = strings.optionRepeated(Some("a"), Some("arg"), "Attribute, in form \"key=value\"")
+
+ protected def execute(context: CliContext) {
+ val client = EventService.client(context.session)
+
+ val attrPairs = args.value.map { s =>
+ val parts = s.split("=")
+ parts.toList match {
+ case List(key, value) => (key, value)
+ case other => throw new IllegalArgumentException(s"Couldn't parse argument $s")
+ }
+ }
+
+ val entUuid = entity.value.map { name =>
+ val entClient = ModelService.client(context.session)
+ val results = Await.result(entClient.get(EntityKeySet.newBuilder().addNames(name).build()), 5000.milliseconds)
+ if (results.size == 1) {
+ results.head.getUuid
+ } else {
+ throw new IllegalArgumentException(s"Couldn't find entity with name $name")
+ }
+ }
+
+ val attributes = attrPairs.map {
+ case (key, value) =>
+ Attribute.newBuilder()
+ .setName(key)
+ .setValueString(value)
+ .build()
+ }
+
+ val b = EventTemplate.newBuilder()
+ .setEventType(eventType.value)
+ .addAllArgs(attributes)
+
+ subsystem.value.foreach(b.setSubsystem)
+ entUuid.foreach(b.setEntityUuid)
+
+ val results = Await.result(client.postEvents(List(b.build())), 5000.milliseconds)
+
+ EventView.printTable(results.toList)
+ }
+}
+
+class EventViewCommmand extends Command[CliContext] {
+
+ val commandName = "event:view"
+ val description = "Show specific events by ID."
+
+ val eventIds = strings.argRepeated("event id", "Event IDs to display")
+
+ protected def execute(context: CliContext) {
+ val client = EventService.client(context.session)
+
+ if (eventIds.value.isEmpty) {
+ throw new IllegalArgumentException("Must include at least one ID")
+ }
+
+ val ids = eventIds.value.map(v => ModelID.newBuilder.setValue(v).build())
+
+ val results = Await.result(client.getEvents(ids), 5000.milliseconds)
+
+ EventView.printTable(results.toList)
+ }
+}
+
+class EventListCommmand extends Command[CliContext] {
+
+ val commandName = "event:list"
+ val description = "List events."
+
+ val eventType = strings.optionRepeated(Some("t"), Some("type"), "Event types to include")
+ val severities = ints.optionRepeated(Some("s"), Some("severity"), "Severities to include")
+ val severityOrHigher = ints.option(None, Some("min-severity"), "Filters to event with this severity or higher")
+ val agents = strings.optionRepeated(Some("a"), Some("agent"), "Agent names to include")
+
+ val alarm = optionSwitch(None, Some("alarm"), "Only include events that are alarms")
+ val notAlarm = optionSwitch(None, Some("not-alarm"), "Only include events that are not alarms")
+
+ val limit = ints.option(None, Some("limit"), "Maximum number of events to return")
+
+ val nonLatest = optionSwitch(Some("b"), Some("beginning"), "Fill page window from the beginning of the result set")
+
+ protected def execute(context: CliContext) {
+ val client = EventService.client(context.session)
+
+ val b = EventQueryParams.newBuilder()
+
+ eventType.value.foreach(b.addEventType)
+ severities.value.foreach(b.addSeverity)
+ severityOrHigher.value.foreach(b.setSeverityOrHigher)
+ agents.value.foreach(b.addAgent)
+
+ val qb = EventQuery.newBuilder().setQueryParams(b)
+ limit.value.foreach(qb.setPageSize)
+
+ val isAlarm = (alarm.value, notAlarm.value) match {
+ case (true, true) => throw new IllegalArgumentException("Must use only one of --alarm or --not-alarm")
+ case (true, false) => Some(true)
+ case (false, true) => Some(false)
+ case (false, false) => None
+ }
+
+ isAlarm.foreach(b.setIsAlarm)
+
+ b.setLatest(!nonLatest.value)
+
+ val results = Await.result(client.eventQuery(qb.build()), 5000.milliseconds)
+
+ EventView.printTable(results.toList)
+ }
+}
+
+class EventSubscribeCommmand extends Command[CliContext] {
+
+ val commandName = "event:subscribe"
+ val description = "Subscribe to events."
+
+ val eventType = strings.optionRepeated(Some("t"), Some("type"), "Event types to include")
+ val severities = ints.optionRepeated(Some("s"), Some("severity"), "Severities to include")
+ val agents = strings.optionRepeated(Some("a"), Some("agent"), "Agent names to include")
+
+ val limit = ints.option(None, Some("limit"), "Maximum number of events to return")
+
+ protected def execute(context: CliContext) {
+ val client = EventService.client(context.session)
+
+ val b = EventSubscriptionQuery.newBuilder()
+
+ eventType.value.foreach(b.addEventType)
+ severities.value.foreach(b.addSeverity)
+ agents.value.foreach(b.addAgent)
+
+ limit.value.foreach(b.setLimit)
+
+ val (results, sub) = Await.result(client.subscribeToEvents(b.build()), 5000.milliseconds)
+
+ val widths = new AtomicReference[Seq[Int]](Nil)
+
+ val origWidths = EventView.printTable(results.toList)
+
+ widths.set(origWidths)
+
+ sub.start { eventNotification =>
+ widths.set(EventView.printRows(List(eventNotification.getValue), widths.get()))
+ }
+
+ context.reader.readCharacter()
+ sub.cancel()
+ }
+}
+
+class AlarmListCommand extends Command[CliContext] {
+
+ val commandName = "alarm:list"
+ val description = "List alarms."
+
+ val alarmStates = strings.optionRepeated(None, Some("alarm-state"), "Alarm states to include: " + List(Alarm.State.UNACK_SILENT, Alarm.State.UNACK_AUDIBLE, Alarm.State.ACKNOWLEDGED, Alarm.State.REMOVED).mkString(", "))
+
+ val eventType = strings.optionRepeated(Some("t"), Some("type"), "Event types to include")
+ val severities = ints.optionRepeated(Some("s"), Some("severity"), "Severities to include")
+ val severityOrHigher = ints.option(None, Some("min-severity"), "Filters to event with this severity or higher")
+ val agents = strings.optionRepeated(Some("a"), Some("agent"), "Agent names to include")
+
+ val limit = ints.option(None, Some("limit"), "Maximum number of events to return")
+
+ val nonLatest = optionSwitch(Some("b"), Some("beginning"), "Fill page window from the beginning of the result set")
+
+ protected def execute(context: CliContext) {
+ val client = EventService.client(context.session)
+
+ val states = try {
+ alarmStates.value.map { str =>
+ Alarm.State.valueOf(str)
+ }
+ } catch {
+ case ex: Throwable =>
+ throw new IllegalArgumentException("Couldn't parse alarm state")
+ }
+
+ val b = EventQueryParams.newBuilder()
+ eventType.value.foreach(b.addEventType)
+ severities.value.foreach(b.addSeverity)
+ severityOrHigher.value.foreach(b.setSeverityOrHigher)
+ agents.value.foreach(b.addAgent)
+ b.setLatest(!nonLatest.value)
+
+ val ab = AlarmQuery.newBuilder()
+ states.foreach(ab.addAlarmStates)
+ limit.value.foreach(ab.setPageSize)
+ ab.setEventQueryParams(b.build())
+
+ val query = ab.build()
+
+ val results = Await.result(client.alarmQuery(query), 5000.milliseconds)
+
+ AlarmView.printTable(results.toList)
+ }
+}
+
+class AlarmSubscribeCommmand extends Command[CliContext] {
+
+ val commandName = "alarm:subscribe"
+ val description = "Subscribe to alarms."
+
+ val alarmStates = strings.optionRepeated(None, Some("alarm-state"), "Alarm states to include: " + List(Alarm.State.UNACK_SILENT, Alarm.State.UNACK_AUDIBLE, Alarm.State.ACKNOWLEDGED, Alarm.State.REMOVED).mkString(", "))
+
+ val eventType = strings.optionRepeated(Some("t"), Some("type"), "Event types to include")
+ val severities = ints.optionRepeated(Some("s"), Some("severity"), "Severities to include")
+ val agents = strings.optionRepeated(Some("a"), Some("agent"), "Agent names to include")
+
+ val limit = ints.option(None, Some("limit"), "Maximum number of events to return")
+
+ protected def execute(context: CliContext) {
+ val client = EventService.client(context.session)
+
+ val states = try {
+ alarmStates.value.map { str =>
+ Alarm.State.valueOf(str)
+ }
+ } catch {
+ case ex: Throwable =>
+ throw new IllegalArgumentException("Couldn't parse alarm state")
+ }
+
+ val b = EventSubscriptionQuery.newBuilder()
+
+ eventType.value.foreach(b.addEventType)
+ severities.value.foreach(b.addSeverity)
+ agents.value.foreach(b.addAgent)
+
+ limit.value.foreach(b.setLimit)
+
+ val alarmBuilder = AlarmSubscriptionQuery.newBuilder().setEventQuery(b.build())
+
+ states.foreach(alarmBuilder.addAlarmStates)
+
+ val (results, sub) = Await.result(client.subscribeToAlarms(alarmBuilder.build()), 5000.milliseconds)
+
+ val widths = new AtomicReference[Seq[Int]](Nil)
+
+ val origWidths = AlarmView.printTable(results.toList)
+
+ widths.set(origWidths)
+
+ sub.start { alarmNotification =>
+ widths.set(AlarmView.printRows(List(alarmNotification.getValue), widths.get()))
+ }
+
+ context.reader.readCharacter()
+ sub.cancel()
+ }
+}
+
+class AlarmSilenceCommand extends Command[CliContext] {
+
+ val commandName = "alarm:silence"
+ val description = "Silence events specified by ID."
+
+ val alarmIds = strings.argRepeated("alarm id", "Alarm IDs")
+
+ protected def execute(context: CliContext) {
+ val client = EventService.client(context.session)
+
+ val updates = alarmIds.value.map { id =>
+ AlarmStateUpdate.newBuilder()
+ .setAlarmId(ModelID.newBuilder().setValue(id).build())
+ .setAlarmState(Alarm.State.UNACK_SILENT)
+ .build()
+ }
+
+ val results = Await.result(client.putAlarmState(updates), 5000.milliseconds)
+
+ AlarmView.printTable(results.toList)
+ }
+}
+
+class AlarmAckCommand extends Command[CliContext] {
+
+ val commandName = "alarm:ack"
+ val description = "Acknowledge events specified by ID."
+
+ val alarmIds = strings.argRepeated("alarm id", "Alarm IDs")
+
+ protected def execute(context: CliContext) {
+ val client = EventService.client(context.session)
+
+ if (alarmIds.value.isEmpty) {
+ throw new IllegalArgumentException("Must include at least one id")
+ }
+
+ val updates = alarmIds.value.map { id =>
+ AlarmStateUpdate.newBuilder()
+ .setAlarmId(ModelID.newBuilder().setValue(id).build())
+ .setAlarmState(Alarm.State.ACKNOWLEDGED)
+ .build()
+ }
+
+ val results = Await.result(client.putAlarmState(updates), 5000.milliseconds)
+
+ AlarmView.printTable(results.toList)
+ }
+}
+
+class AlarmRemoveCommand extends Command[CliContext] {
+
+ val commandName = "alarm:remove"
+ val description = "Remove events specified by ID."
+
+ val alarmIds = strings.argRepeated("alarm id", "Alarm IDs")
+
+ protected def execute(context: CliContext) {
+ val client = EventService.client(context.session)
+
+ val updates = alarmIds.value.map { id =>
+ AlarmStateUpdate.newBuilder()
+ .setAlarmId(ModelID.newBuilder().setValue(id).build())
+ .setAlarmState(Alarm.State.REMOVED)
+ .build()
+ }
+
+ val results = Await.result(client.putAlarmState(updates), 5000.milliseconds)
+
+ AlarmView.printTable(results.toList)
+ }
+}
+
diff --git a/cli/src/main/scala/io/greenbus/cli/commands/FrontEndCommands.scala b/cli/src/main/scala/io/greenbus/cli/commands/FrontEndCommands.scala
new file mode 100644
index 0000000..eed94ca
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/commands/FrontEndCommands.scala
@@ -0,0 +1,364 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli.commands
+
+import java.util.concurrent.atomic.AtomicReference
+
+import io.greenbus.cli.view._
+import io.greenbus.cli.{ CliContext, Command }
+import io.greenbus.client.service.proto.FrontEnd.FrontEndConnectionStatus
+import io.greenbus.client.service.{ FrontEndService, ModelService }
+import io.greenbus.client.service.proto.Model.{ Endpoint, Point, ModelUUID, Command => ProtoCommand }
+import io.greenbus.client.service.proto.ModelRequests.{ EntityEdgeQuery, EntityKeySet, EntityPagingParams, _ }
+import io.greenbus.msg.Session
+
+import scala.collection.JavaConversions._
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.duration._
+import scala.concurrent.{ Await, Future }
+
+object EndpointListCommand {
+
+ def endpointCombined(session: Session, query: EndpointQuery): Future[Seq[(Endpoint, Option[(FrontEndConnectionStatus.Status, Long)])]] = {
+ val modelClient = ModelService.client(session)
+ val frontEndClient = FrontEndService.client(session)
+
+ val endFut = modelClient.endpointQuery(query)
+
+ endFut.flatMap { endpoints =>
+ val uuids = endpoints.map(_.getUuid).distinct
+
+ frontEndClient.getFrontEndConnectionStatuses(EntityKeySet.newBuilder().addAllUuids(uuids).build()).map { statuses =>
+ val statusMap = statuses.map(s => (s.getEndpointUuid, (s.getState, s.getUpdateTime))).toMap
+ endpoints.map { end =>
+ (end, statusMap.get(end.getUuid))
+ }
+ }
+ }
+ }
+
+}
+class EndpointListCommand extends Command[CliContext] {
+
+ val commandName = "endpoint:list"
+ val description = "List all endpoints"
+
+ val protocols = strings.optionRepeated(Some("p"), Some("protocol"), "Endpoint protocols to query for")
+
+ protected def execute(context: CliContext) {
+ val modelClient = ModelService.client(context.session)
+ val frontEndClient = FrontEndService.client(context.session)
+
+ def allQueryResults(last: Option[ModelUUID], pageSize: Int): Future[Seq[(Endpoint, Option[(FrontEndConnectionStatus.Status, Long)])]] = {
+
+ val pageBuilder = EntityPagingParams.newBuilder().setPageSize(pageSize)
+ last.foreach(pageBuilder.setLastUuid)
+
+ val b = EndpointQuery.newBuilder().setPagingParams(pageBuilder)
+
+ if (protocols.value.nonEmpty) {
+ b.addAllProtocols(protocols.value)
+ }
+
+ EndpointListCommand.endpointCombined(context.session, b.build())
+ }
+
+ def pageBoundary(results: Seq[(Endpoint, Option[(FrontEndConnectionStatus.Status, Long)])]) = results.last._1.getUuid
+
+ Paging.page(context.reader, allQueryResults, pageBoundary, EndpointStatusView.printTable, EndpointStatusView.printRows)
+ }
+}
+
+class EndpointSubscribeCommand extends Command[CliContext] {
+
+ val commandName = "endpoint:subscribe"
+ val description = "Subscribe to endpoints"
+
+ val protocols = strings.optionRepeated(Some("p"), Some("protocol"), "Endpoint protocols to query for")
+ val allUpdates = optionSwitch(Some("a"), Some("all"), "Shows all subscription updates, even those with no status change")
+
+ private case class Cache(widths: Seq[Int], endpoints: Map[ModelUUID, Endpoint], statuses: Map[ModelUUID, (FrontEndConnectionStatus.Status, Long)])
+
+ protected def execute(context: CliContext) {
+ val modelClient = ModelService.client(context.session)
+ val frontEndClient = FrontEndService.client(context.session)
+
+ val b = EndpointSubscriptionQuery.newBuilder()
+
+ if (protocols.value.nonEmpty) {
+ b.addAllProtocols(protocols.value)
+ }
+
+ val query = b.build()
+
+ val immediateEndpoints = {
+ val regQuery = EndpointQuery.newBuilder()
+ if (protocols.value.nonEmpty) b.addAllProtocols(protocols.value)
+ Await.result(EndpointListCommand.endpointCombined(context.session, regQuery.build()), 5000.milliseconds)
+ }
+
+ val (_, sub) = Await.result(modelClient.subscribeToEndpoints(query), 5000.milliseconds)
+ val (_, statusSub) = Await.result(frontEndClient.subscribeToFrontEndConnectionStatuses(query), 5000.milliseconds)
+
+ val endpointMap = immediateEndpoints.map(tup => (tup._1.getUuid, tup._1)).toMap
+ val statusMap = immediateEndpoints.flatMap(tup => tup._2.map(stat => (tup._1.getUuid, stat))).toMap
+
+ val originals = immediateEndpoints.map(tup => (tup._1, tup._2))
+ val origWidths = EndpointStatusView.printTable(originals.toList)
+
+ val cacheRef = new AtomicReference[Cache](Cache(origWidths, endpointMap, statusMap))
+
+ sub.start { endpointEvent =>
+ val cache = cacheRef.get()
+ val update = (endpointEvent.getValue, cache.statuses.get(endpointEvent.getValue.getUuid))
+ val nextWidths = EndpointStatusView.printRows(List(update), cache.widths)
+ cacheRef.set(
+ cache.copy(
+ widths = nextWidths,
+ endpoints = cache.endpoints.updated(endpointEvent.getValue.getUuid, endpointEvent.getValue)))
+ }
+
+ statusSub.start { statusEvent =>
+ val stateTup = (statusEvent.getValue.getState, statusEvent.getValue.getUpdateTime)
+ val cache = cacheRef.get()
+ endpointMap.get(statusEvent.getValue.getEndpointUuid) match {
+ case Some(endpoint) =>
+ val currentStatusOpt: Option[(FrontEndConnectionStatus.Status, Long)] = cache.statuses.get(endpoint.getUuid)
+ if (!currentStatusOpt.exists(tup => tup._1 == statusEvent.getValue.getState) || allUpdates.value) {
+ val update = (endpoint, Some(stateTup))
+ val nextWidths = EndpointStatusView.printRows(List(update), cache.widths)
+ cacheRef.set(
+ cache.copy(
+ widths = nextWidths,
+ statuses = cache.statuses.updated(statusEvent.getValue.getEndpointUuid, stateTup)))
+ } else {
+ cacheRef.set(cache.copy(statuses = cache.statuses.updated(statusEvent.getValue.getEndpointUuid, stateTup)))
+ }
+
+ case None =>
+ cacheRef.set(cache.copy(statuses = cache.statuses.updated(statusEvent.getValue.getEndpointUuid, stateTup)))
+ }
+ }
+
+ try {
+ context.reader.readCharacter()
+ } finally {
+ sub.cancel()
+ statusSub.cancel()
+ }
+ }
+}
+
+class EndpointEnableCommand extends Command[CliContext] {
+
+ val commandName = "endpoint:enable"
+ val description = "Enable endpoint"
+
+ val names = strings.argRepeated("endpoint name", "Endpoint names")
+
+ protected def execute(context: CliContext) {
+ val modelClient = ModelService.client(context.session)
+
+ if (names.value.isEmpty) {
+ throw new IllegalArgumentException("Must include at least one argument")
+ }
+
+ val keyQuery = EntityKeySet.newBuilder().addAllNames(names.value).build()
+
+ val endpoints = Await.result(modelClient.getEndpoints(keyQuery), 5000.milliseconds)
+
+ val nameToUuidMap = endpoints.map(end => (end.getName, end.getUuid)).toMap
+
+ val updates = names.value.flatMap { name =>
+ nameToUuidMap.get(name).map { uuid =>
+ EndpointDisabledUpdate.newBuilder()
+ .setEndpointUuid(uuid)
+ .setDisabled(false)
+ .build()
+ }
+ }
+
+ val results = Await.result(modelClient.putEndpointDisabled(updates), 5000.milliseconds)
+
+ EndpointView.printTable(results.toList)
+ }
+}
+
+class EndpointDisableCommand extends Command[CliContext] {
+
+ val commandName = "endpoint:disable"
+ val description = "Disable endpoint"
+
+ val names = strings.argRepeated("endpoint name", "Endpoint names")
+
+ protected def execute(context: CliContext) {
+ val modelClient = ModelService.client(context.session)
+
+ if (names.value.isEmpty) {
+ throw new IllegalArgumentException("Must include at least one argument")
+ }
+
+ val keyQuery = EntityKeySet.newBuilder().addAllNames(names.value).build()
+
+ val endpoints = Await.result(modelClient.getEndpoints(keyQuery), 5000.milliseconds)
+
+ val nameToUuidMap = endpoints.map(end => (end.getName, end.getUuid)).toMap
+
+ val updates = names.value.flatMap { name =>
+ nameToUuidMap.get(name).map { uuid =>
+ EndpointDisabledUpdate.newBuilder()
+ .setEndpointUuid(uuid)
+ .setDisabled(true)
+ .build()
+ }
+ }
+
+ val results = Await.result(modelClient.putEndpointDisabled(updates), 5000.milliseconds)
+
+ EndpointView.printTable(results.toList)
+ }
+}
+
+class PointListCommand extends Command[CliContext] {
+
+ val commandName = "point:list"
+ val description = "List all points"
+
+ val showCommands = optionSwitch(Some("c"), Some("show-commands"), "Show commands associated with points")
+
+ protected def execute(context: CliContext) {
+ val modelClient = ModelService.client(context.session)
+
+ def vanillaResults(last: Option[ModelUUID], pageSize: Int) = {
+
+ val pageBuilder = EntityPagingParams.newBuilder().setPageSize(pageSize)
+ last.foreach(pageBuilder.setLastUuid)
+
+ val query = PointQuery.newBuilder().setPagingParams(pageBuilder).build()
+ modelClient.pointQuery(query)
+ }
+
+ def resultsWithCommands(last: Option[ModelUUID], pageSize: Int): Future[Seq[(Point, Seq[String])]] = {
+
+ val pageBuilder = EntityPagingParams.newBuilder().setPageSize(pageSize)
+ last.foreach(pageBuilder.setLastUuid)
+
+ val pointQuery = PointQuery.newBuilder().setPagingParams(pageBuilder).build()
+ val pointsFut = modelClient.pointQuery(pointQuery)
+
+ pointsFut.flatMap {
+ case Seq() => Future.successful(Nil)
+ case points =>
+
+ val query = EntityEdgeQuery.newBuilder()
+ .addAllParentUuids(points.map(_.getUuid))
+ .addRelationships("feedback")
+ .build()
+
+ modelClient.edgeQuery(query).flatMap { edges =>
+ val grouped = edges.map(e => (e.getParent, e.getChild)).groupBy(_._1).mapValues(_.map(_._2))
+ val commandIds = edges.map(_.getChild).distinct
+
+ modelClient.getCommands(EntityKeySet.newBuilder().addAllUuids(commandIds).build()).map { commands =>
+ val commandMap = commands.map(c => (c.getUuid, c.getName)).toMap
+ points.map { p =>
+ val cmdIds: Seq[ModelUUID] = grouped.get(p.getUuid).getOrElse(Nil)
+ val cmdNames: Seq[String] = cmdIds.flatMap(id => commandMap.get(id))
+ (p, cmdNames)
+ }
+ }
+ }
+ }
+ }
+
+ def pageBoundary(results: Seq[Point]) = results.last.getUuid
+ def pageBoundaryWithCommands(results: Seq[(Point, Seq[String])]) = results.last._1.getUuid
+
+ if (showCommands.value) {
+ Paging.page(context.reader, resultsWithCommands, pageBoundaryWithCommands, PointWithCommandsView.printTable, PointWithCommandsView.printRows)
+ } else {
+ Paging.page(context.reader, vanillaResults, pageBoundary, PointView.printTable, PointView.printRows)
+ }
+ }
+}
+
+class CommandListCommand extends Command[CliContext] {
+
+ val commandName = "command:list"
+ val description = "List all commands"
+
+ val showCommands = optionSwitch(Some("p"), Some("show-points"), "Show points associated with commands")
+
+ protected def execute(context: CliContext) {
+ val modelClient = ModelService.client(context.session)
+
+ def vanillaResults(last: Option[ModelUUID], pageSize: Int) = {
+
+ val pageBuilder = EntityPagingParams.newBuilder().setPageSize(pageSize)
+ last.foreach(pageBuilder.setLastUuid)
+
+ val query = CommandQuery.newBuilder().setPagingParams(pageBuilder).build()
+ modelClient.commandQuery(query)
+ }
+
+ def resultsWithPoints(last: Option[ModelUUID], pageSize: Int): Future[Seq[(ProtoCommand, Seq[String])]] = {
+
+ val pageBuilder = EntityPagingParams.newBuilder().setPageSize(pageSize)
+ last.foreach(pageBuilder.setLastUuid)
+
+ val query = CommandQuery.newBuilder().setPagingParams(pageBuilder).build()
+
+ modelClient.commandQuery(query).flatMap {
+ case Seq() => Future.successful(Nil)
+ case commands =>
+
+ val query = EntityEdgeQuery.newBuilder()
+ .addAllChildUuids(commands.map(_.getUuid))
+ .addRelationships("feedback")
+ .build()
+
+ modelClient.edgeQuery(query).flatMap {
+ case Seq() => Future.successful(Nil)
+ case edges =>
+ val grouped = edges.map(e => (e.getChild, e.getParent)).groupBy(_._1).mapValues(_.map(_._2))
+ val pointIds = edges.map(_.getParent).distinct
+
+ modelClient.getPoints(EntityKeySet.newBuilder().addAllUuids(pointIds).build()).map { points =>
+ val pointMap = points.map(c => (c.getUuid, c.getName)).toMap
+ commands.map { c =>
+ val pointIds: Seq[ModelUUID] = grouped.get(c.getUuid).getOrElse(Nil)
+ val pointNames: Seq[String] = pointIds.flatMap(id => pointMap.get(id))
+ (c, pointNames)
+ }
+ }
+ }
+ }
+ }
+
+ def pageBoundary(results: Seq[ProtoCommand]) = results.last.getUuid
+ def pageBoundaryWithCommands(results: Seq[(ProtoCommand, Seq[String])]) = results.last._1.getUuid
+
+ if (showCommands.value) {
+ Paging.page(context.reader, resultsWithPoints, pageBoundaryWithCommands, CommandWithPointsView.printTable, CommandWithPointsView.printRows)
+ } else {
+ Paging.page(context.reader, vanillaResults, pageBoundary, CommandView.printTable, CommandView.printRows)
+ }
+ }
+}
+
diff --git a/cli/src/main/scala/io/greenbus/cli/commands/KeyValueCommands.scala b/cli/src/main/scala/io/greenbus/cli/commands/KeyValueCommands.scala
new file mode 100644
index 0000000..646b2d2
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/commands/KeyValueCommands.scala
@@ -0,0 +1,182 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli.commands
+
+import io.greenbus.cli.view.KvView
+import io.greenbus.cli.{ CliContext, Command }
+import io.greenbus.client.service.ModelService
+import io.greenbus.client.service.proto.Model.{ EntityKeyValue, StoredValue }
+import io.greenbus.client.service.proto.ModelRequests.{ EntityKeyPair, EntityKeySet }
+
+import scala.concurrent.Await
+import scala.concurrent.duration._
+
+class KvListCommand extends Command[CliContext] {
+
+ val commandName = "kv:list"
+ val description = "List keys of key-values for entity specified by name"
+
+ val name = strings.arg("entity name", "Entity name")
+
+ protected def execute(context: CliContext) {
+ val client = ModelService.client(context.session)
+
+ val result = Await.result(client.get(EntityKeySet.newBuilder().addNames(name.value).build()), 5000.milliseconds)
+
+ result.headOption match {
+ case None => println(s"Entity with name '${name.value}' not found.")
+ case Some(ent) => {
+ val keyResults = Await.result(client.getEntityKeys(Seq(ent.getUuid)), 5000.milliseconds)
+
+ if (keyResults.nonEmpty) {
+ val kvResults = Await.result(client.getEntityKeyValues(keyResults), 5000.milliseconds)
+
+ val nameAndKv = kvResults.map(r => (name.value, r)).sortBy(_._2.getKey)
+ KvView.printTable(nameAndKv)
+ }
+ }
+ }
+ }
+}
+
+class KvViewCommand extends Command[CliContext] {
+
+ val commandName = "kv:view"
+ val description = "View key-value for entity"
+
+ val name = strings.arg("entity name", "Entity name")
+ val key = strings.arg("key", "Key")
+
+ protected def execute(context: CliContext) {
+ val client = ModelService.client(context.session)
+
+ val result = Await.result(client.get(EntityKeySet.newBuilder().addNames(name.value).build()), 5000.milliseconds)
+
+ result.headOption match {
+ case None => println(s"Entity with name '${name.value}' not found.")
+ case Some(ent) => {
+ val keyPair = EntityKeyPair.newBuilder().setUuid(ent.getUuid).setKey(key.value).build()
+ val kvpResults = Await.result(client.getEntityKeyValues(Seq(keyPair)), 5000.milliseconds)
+
+ kvpResults.headOption match {
+ case None => println(s"Key not found.")
+ case Some(kvp) => {
+ KvView.printInspect(kvp)
+ }
+ }
+ }
+ }
+ }
+}
+
+class KvSetCommand extends Command[CliContext] {
+
+ val commandName = "kv:set"
+ val description = "Set key-value for entity"
+
+ val name = strings.arg("entity name", "Entity name")
+ val key = strings.arg("key", "Key")
+
+ val boolVal = ints.option(Some("b"), Some("bool-val"), "Boolean value [0, 1]")
+
+ val intVal = ints.option(Some("i"), Some("int-val"), "Integer value")
+
+ val floatVal = doubles.option(Some("f"), Some("float-val"), "Floating point value")
+
+ val stringVal = strings.option(Some("s"), Some("string-val"), "String value")
+
+ protected def execute(context: CliContext) {
+
+ List(boolVal.value, intVal.value, floatVal.value, stringVal.value).flatten.size match {
+ case 0 => throw new IllegalArgumentException("Must include at least one value option")
+ case 1 =>
+ case n => throw new IllegalArgumentException("Must include only one value option")
+ }
+
+ def storedValue = {
+ val b = StoredValue.newBuilder()
+ if (boolVal.value.nonEmpty) {
+ b.setBoolValue(boolVal.value != Some(0))
+ } else if (intVal.value.nonEmpty) {
+ b.setInt64Value(intVal.value.get)
+ } else if (floatVal.value.nonEmpty) {
+ b.setDoubleValue(floatVal.value.get)
+ } else if (stringVal.value.nonEmpty) {
+ b.setStringValue(stringVal.value.get)
+ } else {
+ throw new IllegalArgumentException("Must include one value option")
+ }
+ b.build()
+ }
+
+ val client = ModelService.client(context.session)
+
+ val result = Await.result(client.get(EntityKeySet.newBuilder().addNames(name.value).build()), 5000.milliseconds)
+
+ result.headOption match {
+ case None => println(s"Entity with name '${name.value}' not found.")
+ case Some(ent) => {
+ val kv = EntityKeyValue.newBuilder()
+ .setUuid(ent.getUuid)
+ .setKey(key.value)
+ .setValue(storedValue)
+ .build()
+
+ val kvpResults = Await.result(client.putEntityKeyValues(Seq(kv)), 5000.milliseconds)
+
+ kvpResults.headOption match {
+ case None => println(s"Key not put.")
+ case Some(kvp) => {
+ KvView.printInspect(kvp)
+ }
+ }
+ }
+ }
+ }
+}
+
+class KvDeleteCommand extends Command[CliContext] {
+
+ val commandName = "kv:delete"
+ val description = "Delete key-value for entity specified by name"
+
+ val name = strings.arg("entity name", "Entity name")
+ val key = strings.arg("key", "Key")
+
+ protected def execute(context: CliContext) {
+ val client = ModelService.client(context.session)
+
+ val result = Await.result(client.get(EntityKeySet.newBuilder().addNames(name.value).build()), 5000.milliseconds)
+
+ result.headOption match {
+ case None => println(s"Entity with name '${name.value}' not found.")
+ case Some(ent) => {
+ val keyPair = EntityKeyPair.newBuilder().setUuid(ent.getUuid).setKey(key.value).build()
+ val kvpResults = Await.result(client.deleteEntityKeyValues(Seq(keyPair)), 5000.milliseconds)
+
+ kvpResults.headOption match {
+ case None => println(s"Key not found.")
+ case Some(kvp) => {
+ KvView.printInspect(kvp)
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/cli/src/main/scala/io/greenbus/cli/commands/LoginCommands.scala b/cli/src/main/scala/io/greenbus/cli/commands/LoginCommands.scala
new file mode 100644
index 0000000..1edd99f
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/commands/LoginCommands.scala
@@ -0,0 +1,70 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli.commands
+
+import io.greenbus.cli.{ ManagementCliContext, Command }
+import scala.concurrent.Await
+import scala.concurrent.duration._
+import io.greenbus.client.service.LoginService
+import io.greenbus.client.service.proto.LoginRequests.LogoutRequest
+import io.greenbus.client.ServiceHeaders
+
+class LoginCommand extends Command[ManagementCliContext] {
+
+ val commandName = "login"
+ val description = "Obtain an auth token from the services"
+
+ val name = strings.arg("user name", "Agent name to login as.")
+
+ val passOpt = strings.option(Some("p"), Some("password"), "Supply password instead of prompting, useful for scripting. WARNING: Password will be visible in command history.")
+
+ protected def execute(context: ManagementCliContext) {
+
+ val password = passOpt.value.getOrElse {
+ println("Enter password: ")
+ readLine()
+ }
+
+ val session = Await.result(context.connection.login(name.value, password), 5000.milliseconds)
+
+ context.setSession(Some(session))
+ context.setAgent(Some(name.value))
+ context.setToken(session.headers.get(ServiceHeaders.tokenHeader()))
+ }
+}
+
+class LogoutCommand extends Command[ManagementCliContext] {
+
+ val commandName = "logout"
+ val description = "Invalidate auth token and remove it"
+
+ protected def execute(context: ManagementCliContext) {
+
+ val sess = context.session
+ val token = sess.headers.get(ServiceHeaders.tokenHeader()).getOrElse {
+ throw new IllegalArgumentException("No auth token in session")
+ }
+
+ val client = LoginService.client(sess)
+ val success = Await.result(client.logout(LogoutRequest.newBuilder().setToken(token).build()), 5000.milliseconds)
+
+ context.setSession(None)
+ context.setAgent(None)
+ }
+}
\ No newline at end of file
diff --git a/cli/src/main/scala/io/greenbus/cli/commands/MeasListCommand.scala b/cli/src/main/scala/io/greenbus/cli/commands/MeasListCommand.scala
new file mode 100644
index 0000000..ba7a1c3
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/commands/MeasListCommand.scala
@@ -0,0 +1,290 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli.commands
+
+import java.io.{ PrintWriter, FileOutputStream }
+import java.text.SimpleDateFormat
+import java.util.concurrent.atomic.AtomicReference
+
+import io.greenbus.cli.view._
+import io.greenbus.cli.{ CliContext, Command }
+import io.greenbus.client.service.proto.MeasurementRequests.MeasurementHistoryQuery
+import io.greenbus.client.service.proto.Measurements.Measurement
+import io.greenbus.client.service.proto.Model.{ Point, ModelUUID }
+import io.greenbus.client.service.proto.ModelRequests.{ EntityKeySet, EntityPagingParams, PointQuery }
+import io.greenbus.client.service.{ MeasurementService, ModelService }
+
+import scala.collection.JavaConversions._
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.duration._
+import scala.concurrent.{ Await, Future }
+
+class MeasListCommand extends Command[CliContext] {
+
+ val commandName = "meas:list"
+ val description = "List current measurement values"
+
+ protected def execute(context: CliContext) {
+ val modelClient = ModelService.client(context.session)
+ val measClient = MeasurementService.client(context.session)
+
+ def results(last: Option[ModelUUID], pageSize: Int) = {
+
+ val pageBuilder = EntityPagingParams.newBuilder().setPageSize(pageSize)
+ last.foreach(pageBuilder.setLastUuid)
+
+ val query = PointQuery.newBuilder().setPagingParams(pageBuilder).build()
+
+ modelClient.pointQuery(query).flatMap {
+ case Seq() => Future.successful(Nil)
+ case points =>
+ val uuids = points.map(_.getUuid)
+ measClient.getCurrentValues(uuids).map { pointMeas =>
+ val pointMap = pointMeas.map(p => (p.getPointUuid, p)).toMap
+ points.map { point =>
+ val measOpt = pointMap.get(point.getUuid)
+ (point, measOpt.map(_.getValue))
+ }
+ }
+ }
+
+ }
+
+ def pageBoundary(results: Seq[(Point, Option[Measurement])]) = results.last._1.getUuid
+
+ Paging.page(context.reader, results, pageBoundary, MeasWithEmptiesView.printTable, MeasWithEmptiesView.printRows)
+ }
+}
+
+class MeasHistoryCommand extends Command[CliContext] {
+
+ val commandName = "meas:history"
+ val description = "Get history for measurement"
+
+ val measName = strings.arg("name", "Name of point to retrieve history for")
+
+ protected def execute(context: CliContext) {
+ val modelClient = ModelService.client(context.session)
+ val measClient = MeasurementService.client(context.session)
+
+ val pointResults = Await.result(modelClient.get(EntityKeySet.newBuilder().addNames(measName.value).build()), 5000.milliseconds)
+
+ val point = pointResults.headOption.getOrElse {
+ throw new IllegalArgumentException("Point does not exist")
+ }
+
+ def results(last: Option[Long], pageSize: Int) = {
+ val queryBuilder = MeasurementHistoryQuery.newBuilder()
+ .setPointUuid(point.getUuid)
+ .setLatest(true)
+ .setLimit(pageSize)
+
+ last.foreach(queryBuilder.setTimeTo)
+
+ val query = queryBuilder.build()
+
+ measClient.getHistory(query).map(_.getValueList.toSeq.reverse)
+ }
+
+ def pageBoundary(results: Seq[Measurement]) = results.last.getTime
+
+ Paging.page(context.reader, results, pageBoundary, MeasurementHistoryView.printTable, MeasurementHistoryView.printRows)
+ }
+}
+
+class MeasSubscribeCommand extends Command[CliContext] {
+
+ val commandName = "meas:subscribe"
+ val description = "Subscribe to measurement update values"
+
+ val allFlag = optionSwitch(Some("a"), Some("all"), "Subscribe to all updates")
+
+ val measNames = strings.argRepeated("name", "Name of point to retrieve history for")
+
+ protected def execute(context: CliContext) {
+ val modelClient = ModelService.client(context.session)
+ val measClient = MeasurementService.client(context.session)
+
+ val sub = if (allFlag.value) {
+
+ if (measNames.value.nonEmpty) {
+ throw new IllegalArgumentException("Must not include measurement names when subscribing to all")
+ }
+
+ val (_, sub) = Await.result(measClient.getCurrentValuesAndSubscribe(Seq()), 5000.milliseconds)
+
+ val widths = new AtomicReference(Seq.empty[Int])
+
+ sub.start { measNotification =>
+ widths.set(SimpleMeasurementView.printRows(List((measNotification.getPointName, measNotification.getValue, "--")), widths.get()))
+ }
+
+ sub
+
+ } else {
+
+ if (measNames.value.isEmpty) {
+ throw new IllegalArgumentException("Must include at least one measurement name")
+ }
+
+ val pointResults = Await.result(modelClient.getPoints(EntityKeySet.newBuilder().addAllNames(measNames.value).build()), 5000.milliseconds)
+
+ if (pointResults.isEmpty) {
+ throw new IllegalArgumentException("Points did not exist")
+ }
+
+ val uuidToPointMap = pointResults.map(p => p.getUuid -> p).toMap
+
+ val (results, sub) = Await.result(measClient.getCurrentValuesAndSubscribe(pointResults.map(_.getUuid)), 5000.milliseconds)
+
+ val origWidths = SimpleMeasurementView.printTable(results.flatMap(r => uuidToPointMap.get(r.getPointUuid).map(p => (p.getName, r.getValue, p.getUnit))).toList)
+
+ val widths = new AtomicReference(origWidths)
+
+ sub.start { measNotification =>
+ val pointOpt = uuidToPointMap.get(measNotification.getPointUuid)
+ pointOpt.foreach { point =>
+ widths.set(SimpleMeasurementView.printRows(List((measNotification.getPointName, measNotification.getValue, point.getUnit)), widths.get()))
+ }
+ }
+
+ sub
+ }
+
+ context.reader.readCharacter()
+ sub.cancel()
+ }
+}
+
+class MeasDownloadCommand extends Command[CliContext] {
+
+ val commandName = "meas:download"
+ val description = "Download measurement history to a file"
+
+ val measName = strings.arg("name", "Name of point to retrieve history for")
+
+ val fileName = strings.option(Some("o"), Some("output"), "Filename to write output to")
+
+ val startTimeOption = strings.option(Some("s"), Some("start"), "Start time, UTC, in format \"yyyy-MM-dd HH:mm\"")
+ val endTimeOption = strings.option(Some("e"), Some("end"), "End time, UTC, in format \"yyyy-MM-dd HH:mm\"")
+
+ val offsetHours = ints.option(None, Some("hours-offset"), "Duration time, in hours, offset from the end time")
+ val offsetMinutes = ints.option(None, Some("minutes-offset"), "Duration time, in minutes, offset from the end time")
+
+ val delimiterOption = strings.option(Some("d"), None, "Delimiter character (e.g. tab-separated, comma-separated). Tab-separated by default.")
+
+ val rawTimeOption = optionSwitch(None, Some("raw-time"), "Write time in milliseconds since 1970 UTC")
+ val dateFormatOption = strings.option(None, Some("date-format"), "Specifies format for time. See documentation for the SimpleDataFormat Java class for the syntax.")
+
+ protected def execute(context: CliContext) {
+ val modelClient = ModelService.client(context.session)
+ val measClient = MeasurementService.client(context.session)
+
+ val pointResults = Await.result(modelClient.get(EntityKeySet.newBuilder().addNames(measName.value).build()), 5000.milliseconds)
+
+ val point = pointResults.headOption.getOrElse {
+ throw new IllegalArgumentException(s"Point ${measName.value} not found.")
+ }
+
+ val endTimeMillis = endTimeOption.value match {
+ case None => System.currentTimeMillis()
+ case Some(endTime) =>
+ try {
+ val date = new SimpleDateFormat("yyyy-MM-dd HH:mm").parse(endTimeOption.value.get)
+ date.getTime
+ } catch {
+ case ex: Throwable =>
+ throw new IllegalArgumentException("Could not parse date format for end time.")
+ }
+ }
+
+ val startTimeMillis = if (startTimeOption.value.nonEmpty) {
+ try {
+ val date = new SimpleDateFormat("yyyy-MM-dd HH:mm").parse(startTimeOption.value.get)
+ date.getTime
+ } catch {
+ case ex: Throwable =>
+ throw new IllegalArgumentException("Could not parse date format for start time.")
+ }
+ } else if (offsetHours.value.nonEmpty || offsetMinutes.value.nonEmpty) {
+ endTimeMillis -
+ offsetHours.value.map(h => h * 60 * 60 * 1000).getOrElse(0) -
+ offsetMinutes.value.map(min => min * 60 * 1000).getOrElse(0)
+ } else {
+ throw new IllegalArgumentException("Must set a start time or a time offset.")
+ }
+
+ val delimiter = delimiterOption.value.getOrElse("\t")
+
+ val dateFormatter = dateFormatOption.value.map(s => new SimpleDateFormat(s))
+
+ def timeFormat(m: Measurement): String = {
+ if (rawTimeOption.value) {
+ m.getTime.toString
+ } else if (dateFormatOption.value.nonEmpty) {
+ dateFormatter.map(f => f.format(m.getTime)).getOrElse(throw new IllegalArgumentException("Couldn't build date formatter"))
+ } else {
+ MeasViewCommon.timeString(m)
+ }
+ }
+
+ def toMeasLine(m: Measurement): String = {
+ val time = timeFormat(m)
+ Seq(time, MeasViewCommon.value(m), MeasViewCommon.shortQuality(m), MeasViewCommon.longQuality(m)).mkString(delimiter)
+ }
+
+ val pageSize = 1000
+
+ def historyBuilder(startTime: Long): MeasurementHistoryQuery = {
+ MeasurementHistoryQuery.newBuilder()
+ .setPointUuid(point.getUuid)
+ .setLatest(false)
+ .setTimeFrom(startTime)
+ .setTimeTo(endTimeMillis)
+ .setLimit(pageSize)
+ .build()
+ }
+
+ val filepath = fileName.value.getOrElse(throw new IllegalArgumentException("Must include output filename."))
+ val file = new FileOutputStream(filepath)
+ val pw = new PrintWriter(file)
+
+ try {
+
+ var latestStartTime = startTimeMillis
+ var done = false
+ while (!done) {
+ val query = historyBuilder(latestStartTime)
+ val results = Await.result(measClient.getHistory(query), 10000.milliseconds)
+
+ results.getValueList.foreach { m => pw.println(toMeasLine(m)) }
+
+ if (results.getValueList.size() < pageSize) {
+ done = true
+ } else {
+ latestStartTime = results.getValueList.last.getTime
+ }
+ }
+ } finally {
+ pw.flush()
+ pw.close()
+ }
+ }
+}
+
diff --git a/cli/src/main/scala/io/greenbus/cli/commands/Paging.scala b/cli/src/main/scala/io/greenbus/cli/commands/Paging.scala
new file mode 100644
index 0000000..9771c1a
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/commands/Paging.scala
@@ -0,0 +1,58 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli.commands
+
+import jline.console.ConsoleReader
+import scala.concurrent.{ Await, Future }
+import scala.annotation.tailrec
+import scala.concurrent.duration._
+
+object Paging {
+
+ def page[A, B](reader: ConsoleReader, getResults: (Option[B], Int) => Future[Seq[A]], pageBoundary: Seq[A] => B, displayFirst: (Seq[A], Seq[Int]) => Seq[Int], displaySubsequent: (Seq[A], Seq[Int]) => Seq[Int]) {
+
+ @tailrec
+ def getAndPrint(last: Option[B], minColWidths: Seq[Int]) {
+ val terminalHeight = reader.getTerminal.getHeight
+ val pageSize = if (last.isEmpty) terminalHeight - 3 else terminalHeight
+
+ val pageResults = Await.result(getResults(last, pageSize), 5000.milliseconds)
+
+ if (pageResults.nonEmpty) {
+
+ val columnWidths = if (last.isEmpty) {
+ displayFirst(pageResults.toList, minColWidths)
+ } else {
+ displaySubsequent(pageResults.toList, minColWidths)
+ }
+
+ if (pageResults.size == pageSize) {
+ val inputInt = reader.readCharacter()
+ val input = inputInt.asInstanceOf[Char]
+ if (!(input == 'q')) {
+ getAndPrint(Some(pageBoundary(pageResults)), columnWidths)
+ }
+ }
+
+ }
+ }
+
+ getAndPrint(None, Nil)
+ }
+}
diff --git a/cli/src/main/scala/io/greenbus/cli/commands/ProcessingCommands.scala b/cli/src/main/scala/io/greenbus/cli/commands/ProcessingCommands.scala
new file mode 100644
index 0000000..5a3e3b8
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/commands/ProcessingCommands.scala
@@ -0,0 +1,174 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli.commands
+
+import io.greenbus.cli.{ CliContext, Command }
+import io.greenbus.client.service.proto.Measurements.{ Quality, Measurement }
+import io.greenbus.client.service.proto.ModelRequests.EntityKeySet
+import io.greenbus.client.service.proto.Processing.MeasOverride
+import io.greenbus.client.service.{ MeasurementService, ModelService, ProcessingService }
+
+import scala.concurrent.Await
+import scala.concurrent.duration._
+
+class MeasBlockCommand extends Command[CliContext] {
+
+ val commandName = "meas:block"
+ val description = "Block measurement from being updated"
+
+ val measName = strings.arg("measurement name", "Measurement name")
+
+ protected def execute(context: CliContext) {
+
+ val modelClient = ModelService.client(context.session)
+ val pointFut = modelClient.getPoints(EntityKeySet.newBuilder().addNames(measName.value).build())
+ val pointResults = Await.result(pointFut, 5000.milliseconds)
+
+ if (pointResults.isEmpty) {
+ throw new IllegalArgumentException(s"Point '${measName.value}' not found")
+ }
+
+ val pointUuid = pointResults.head.getUuid
+
+ val procClient = ProcessingService.client(context.session)
+
+ val overs = MeasOverride.newBuilder()
+ .setPointUuid(pointUuid)
+ .build()
+
+ Await.result(procClient.putOverrides(Seq(overs)), 5000.milliseconds)
+
+ println("Success")
+ }
+}
+
+class MeasUnblockCommand extends Command[CliContext] {
+
+ val commandName = "meas:unblock"
+ val description = "Unblock measurement"
+
+ val measName = strings.arg("measurement name", "Measurement name")
+
+ protected def execute(context: CliContext) {
+
+ val modelClient = ModelService.client(context.session)
+ val pointFut = modelClient.getPoints(EntityKeySet.newBuilder().addNames(measName.value).build())
+ val pointResults = Await.result(pointFut, 5000.milliseconds)
+
+ if (pointResults.isEmpty) {
+ throw new IllegalArgumentException(s"Point '${measName.value}' not found")
+ }
+
+ val pointUuid = pointResults.head.getUuid
+
+ val procClient = ProcessingService.client(context.session)
+
+ Await.result(procClient.deleteOverrides(Seq(pointUuid)), 5000.milliseconds)
+
+ println("Success")
+
+ }
+}
+
+class MeasReplaceCommand extends Command[CliContext] {
+
+ val commandName = "meas:replace"
+ val description = "Block measurement and replace with manual value"
+
+ val measName = strings.arg("measurement name", "Measurement name")
+
+ val boolVal = ints.option(Some("b"), Some("bool-val"), "Boolean value [0, 1]")
+
+ val intVal = ints.option(Some("i"), Some("int-val"), "Integer value")
+
+ val floatVal = doubles.option(Some("f"), Some("float-val"), "Floating point value")
+
+ val stringVal = strings.option(Some("s"), Some("string-val"), "String value")
+
+ val isTest = optionSwitch(Some("t"), Some("test"), "Replace is a test value")
+
+ protected def execute(context: CliContext) {
+
+ List(boolVal.value, intVal.value, floatVal.value, stringVal.value).flatten.size match {
+ case 0 => throw new IllegalArgumentException("Must include at least one value option")
+ case 1 =>
+ case n => throw new IllegalArgumentException("Must include only one value option")
+ }
+
+ def reinitializeBuilder(b: Measurement.Builder) {
+ if (boolVal.value.nonEmpty) {
+ if (boolVal.value.get == 0) {
+ b.setBoolVal(false).setType(Measurement.Type.BOOL)
+ } else if (boolVal.value.get == 1) {
+ b.setBoolVal(true).setType(Measurement.Type.BOOL)
+ } else {
+ throw new IllegalArgumentException("Boolean values must be 0 or 1")
+ }
+ } else if (intVal.value.nonEmpty) {
+ b.setIntVal(intVal.value.get).setType(Measurement.Type.INT)
+ } else if (floatVal.value.nonEmpty) {
+ b.setDoubleVal(floatVal.value.get).setType(Measurement.Type.DOUBLE)
+ } else if (stringVal.value.nonEmpty) {
+ b.setStringVal(stringVal.value.get).setType(Measurement.Type.STRING)
+ } else {
+ throw new IllegalArgumentException("Must include at least one value option")
+ }
+ }
+
+ val modelClient = ModelService.client(context.session)
+ val pointFut = modelClient.getPoints(EntityKeySet.newBuilder().addNames(measName.value).build())
+ val pointResults = Await.result(pointFut, 5000.milliseconds)
+
+ if (pointResults.isEmpty) {
+ throw new IllegalArgumentException(s"Point '${measName.value}' not found")
+ }
+
+ val pointUuid = pointResults.head.getUuid
+
+ val measClient = MeasurementService.client(context.session)
+
+ val measFut = measClient.getCurrentValues(Seq(pointUuid))
+
+ val measResults = Await.result(measFut, 5000.milliseconds)
+
+ val measBuilder = if (measResults.isEmpty) {
+ Measurement.newBuilder().setQuality(Quality.newBuilder().build()).setTime(System.currentTimeMillis())
+ } else {
+ measResults.head.getValue.toBuilder
+ }
+
+ reinitializeBuilder(measBuilder)
+
+ val procClient = ProcessingService.client(context.session)
+
+ val overBuilder = MeasOverride.newBuilder()
+ .setPointUuid(pointUuid)
+ .setMeasurement(measBuilder.build())
+
+ if (isTest.value) {
+ overBuilder.setTestValue(true)
+ }
+
+ val overs = overBuilder.build()
+
+ Await.result(procClient.putOverrides(Seq(overs)), 5000.milliseconds)
+
+ println("Success")
+ }
+}
diff --git a/cli/src/main/scala/io/greenbus/cli/view/AgentView.scala b/cli/src/main/scala/io/greenbus/cli/view/AgentView.scala
new file mode 100755
index 0000000..65e80ee
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/view/AgentView.scala
@@ -0,0 +1,98 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli.view
+
+import io.greenbus.client.service.proto.Auth.{ Permission, EntitySelector, PermissionSet, Agent }
+import scala.collection.JavaConversions._
+
+object AgentView extends TableView[Agent] {
+ def header: List[String] = "Name" :: "Permission Sets" :: Nil
+
+ def row(obj: Agent): List[String] = {
+ obj.getName ::
+ obj.getPermissionSetsList.mkString(", ") ::
+ Nil
+ }
+
+ def viewAgent(agent: Agent, perms: Seq[PermissionSet]) = {
+ val agentRows = Seq(
+ Seq("UUID: ", agent.getUuid.getValue),
+ Seq("Name: ", agent.getName))
+
+ Table.renderRows(agentRows)
+ println()
+
+ val permRows = perms.flatMap(p => p.getPermissionsList.map(PermissionSetView.permissionRowWithSet(_, p.getName)))
+ Table.printTable(PermissionSetView.permissionHeaderWithSet, permRows)
+ }
+}
+
+object PermissionSetView extends TableView[PermissionSet] {
+ def header: List[String] = "Name" :: "Allows" :: "Denies" :: Nil
+
+ def row(obj: PermissionSet): List[String] = {
+ val (allows, denies) = obj.getPermissionsList.toSeq.partition(_.getAllow)
+
+ obj.getName ::
+ allows.size.toString ::
+ denies.size.toString ::
+ Nil
+ }
+
+ def viewPermissionSet(a: PermissionSet) = {
+
+ val permissions = a.getPermissionsList.toList
+
+ Table.printTable(permissionHeader, permissions.map(permissionRow))
+ }
+
+ def permissionHeader = {
+ "Allow" :: "Actions" :: "Resources" :: "Selectors" :: Nil
+ }
+
+ def permissionRow(a: Permission): List[String] = {
+ a.getAllow.toString ::
+ a.getActionsList.toList.mkString(",") ::
+ a.getResourcesList.toList.mkString(",") ::
+ a.getSelectorsList.toList.map(selectorString).mkString(",") ::
+ Nil
+ }
+
+ def permissionHeaderWithSet = {
+ "Allow" :: "Actions" :: "Resources" :: "Selectors" :: "Permission set" :: Nil
+ }
+
+ def permissionRowWithSet(a: Permission, setName: String): List[String] = {
+ a.getAllow.toString ::
+ a.getActionsList.toList.mkString(",") ::
+ a.getResourcesList.toList.mkString(",") ::
+ a.getSelectorsList.toList.map(selectorString).mkString(",") ::
+ setName ::
+ Nil
+ }
+
+ def selectorString(a: EntitySelector): String = {
+ val args = a.getArgumentsList.toList
+ val argString = if (args.isEmpty) ""
+ else args.mkString("(", ",", ")")
+ a.getStyle + argString
+ }
+
+}
+
diff --git a/cli/src/main/scala/io/greenbus/cli/view/CalculationView.scala b/cli/src/main/scala/io/greenbus/cli/view/CalculationView.scala
new file mode 100644
index 0000000..d6b7220
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/view/CalculationView.scala
@@ -0,0 +1,34 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli.view
+
+import io.greenbus.client.service.proto.Calculations.CalculationDescriptor
+import io.greenbus.client.service.proto.Model.Entity
+
+object CalculationView extends TableView[(Entity, Option[CalculationDescriptor])] {
+ def header: List[String] = "Point Name" :: "Formula" :: "Unit" :: Nil
+
+ def row(obj: (Entity, Option[CalculationDescriptor])): List[String] = {
+ val (ent, calcOpt) = obj
+ ent.getName ::
+ calcOpt.map(_.getFormula).getOrElse("-") ::
+ "unit" ::
+ Nil
+ }
+}
\ No newline at end of file
diff --git a/cli/src/main/scala/io/greenbus/cli/view/CommandView.scala b/cli/src/main/scala/io/greenbus/cli/view/CommandView.scala
new file mode 100644
index 0000000..8f127b0
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/view/CommandView.scala
@@ -0,0 +1,56 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli.view
+
+import io.greenbus.client.service.proto.Commands.CommandLock
+import java.util.Date
+
+object CommandLockView extends TableView[(CommandLock, Seq[String], String)] {
+
+ def header: List[String] = "Id" :: "Mode" :: "User" :: "Commands" :: "Expire Time" :: Nil
+
+ def row(obj: (CommandLock, Seq[String], String)): List[String] = {
+ val (lock, cmds, agent) = obj
+
+ lock.getId.getValue ::
+ lock.getAccess.toString ::
+ agent ::
+ cmds.mkString(", ") ::
+ lockTimeString(lock) ::
+ Nil
+ }
+
+ def printInspect(item: (CommandLock, Seq[String], String)) = {
+ val (lock, cmds, agent) = item
+
+ val rows: List[List[String]] = ("ID:" :: lock.getId.getValue :: Nil) ::
+ ("Mode:" :: lock.getAccess.toString :: Nil) ::
+ ("User:" :: agent :: Nil) ::
+ ("Expires:" :: lockTimeString(lock) :: Nil) ::
+ ("Commands:" :: cmds.head :: Nil) :: Nil
+
+ val cmdRows = cmds.tail.map(cmd => "" :: cmd :: Nil)
+
+ Table.renderRows(rows ::: cmdRows.toList, " ")
+ }
+
+ private def lockTimeString(lock: CommandLock): String = {
+ if (lock.hasExpireTime) new Date(lock.getExpireTime).toString else ""
+ }
+}
diff --git a/cli/src/main/scala/io/greenbus/cli/view/EntityView.scala b/cli/src/main/scala/io/greenbus/cli/view/EntityView.scala
new file mode 100755
index 0000000..6b45574
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/view/EntityView.scala
@@ -0,0 +1,42 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli.view
+
+import io.greenbus.client.service.proto.Model.Entity
+import scala.collection.JavaConversions._
+
+object EntityView extends TableView[Entity] {
+ def header: List[String] = "Name" :: "Types" :: Nil
+
+ def row(obj: Entity): List[String] = {
+ obj.getName ::
+ "(" + obj.getTypesList.toVector.sorted.mkString(", ") + ")" ::
+ Nil
+ }
+
+ def printInspect(ent: Entity) = {
+ val lines =
+ ("id" :: ent.getUuid.getValue :: Nil) ::
+ ("name" :: ent.getName :: Nil) ::
+ ("types" :: "(" + ent.getTypesList.toVector.sorted.mkString(", ") + ")" :: Nil) ::
+ Nil
+
+ Table.renderRows(lines, " | ")
+ }
+}
diff --git a/cli/src/main/scala/io/greenbus/cli/view/EventView.scala b/cli/src/main/scala/io/greenbus/cli/view/EventView.scala
new file mode 100644
index 0000000..bdee962
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/view/EventView.scala
@@ -0,0 +1,67 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli.view
+
+import io.greenbus.client.service.proto.Events.{ Event, Alarm, EventConfig }
+
+object EventConfigView extends TableView[EventConfig] {
+ def header: List[String] = "EventType" :: "Dest" :: "Sev" :: "Audible" :: "Resources" :: "BuiltIn" :: Nil
+
+ def row(obj: EventConfig): List[String] = {
+ obj.getEventType ::
+ obj.getDesignation.toString ::
+ obj.getSeverity.toString ::
+ (obj.getAlarmState == Alarm.State.UNACK_AUDIBLE).toString ::
+ obj.getResource ::
+ obj.getBuiltIn.toString :: Nil
+ }
+}
+
+object EventView extends TableView[Event] {
+ def header: List[String] = "ID" :: "EventType" :: "IsAlarm" :: "Time" :: "Sev" :: "Agent" :: "DeviceTime" :: "Subsystem" :: "Message" :: Nil
+
+ def row(obj: Event): List[String] = {
+ obj.getId.getValue ::
+ obj.getEventType ::
+ obj.getAlarm.toString ::
+ new java.util.Date(obj.getTime).toString ::
+ obj.getSeverity.toString ::
+ obj.getAgentName ::
+ (if (obj.hasDeviceTime) new java.util.Date(obj.getDeviceTime).toString else "") ::
+ obj.getSubsystem ::
+ obj.getRendered :: Nil
+ }
+}
+
+object AlarmView extends TableView[Alarm] {
+ def header: List[String] = "ID" :: "State" :: "EventType" :: "Time" :: "Sev" :: "Agent" :: "DeviceTime" :: "Subsystem" :: "Message" :: Nil
+
+ def row(obj: Alarm): List[String] = {
+ obj.getId.getValue ::
+ obj.getState.toString ::
+ obj.getEvent.getEventType ::
+ new java.util.Date(obj.getEvent.getTime).toString ::
+ obj.getEvent.getSeverity.toString ::
+ obj.getEvent.getAgentName ::
+ (if (obj.getEvent.hasDeviceTime) new java.util.Date(obj.getEvent.getDeviceTime).toString else "") ::
+ obj.getEvent.getSubsystem ::
+ obj.getEvent.getRendered :: Nil
+ }
+}
+
diff --git a/cli/src/main/scala/io/greenbus/cli/view/FrontEndView.scala b/cli/src/main/scala/io/greenbus/cli/view/FrontEndView.scala
new file mode 100644
index 0000000..723963e
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/view/FrontEndView.scala
@@ -0,0 +1,102 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli.view
+
+import java.util.Date
+
+import io.greenbus.client.service.proto.FrontEnd.FrontEndConnectionStatus
+import io.greenbus.client.service.proto.Model.{ Command, Endpoint, Point }
+
+import scala.collection.JavaConversions._
+
+object EndpointView extends TableView[Endpoint] {
+ def header: List[String] = "Name" :: "Protocol" :: "Disabled" :: "Types" :: Nil
+
+ def row(obj: Endpoint): List[String] = {
+ obj.getName ::
+ obj.getProtocol ::
+ obj.getDisabled.toString ::
+ "(" + obj.getTypesList.toVector.sorted.mkString(", ") + ")" ::
+ Nil
+ }
+}
+
+object EndpointStatusView extends TableView[(Endpoint, Option[(FrontEndConnectionStatus.Status, Long)])] {
+ def header: List[String] = "Name" :: "Status " :: "Last Status" :: "Protocol" :: "Disabled" :: "Types" :: Nil
+
+ def row(obj: (Endpoint, Option[(FrontEndConnectionStatus.Status, Long)])): List[String] = {
+ val (end, (statusOpt)) = obj
+ end.getName ::
+ statusOpt.map(_._1.toString).getOrElse("--") ::
+ statusOpt.map(t => new Date(t._2).toString).getOrElse("--") ::
+ end.getProtocol ::
+ end.getDisabled.toString ::
+ "(" + end.getTypesList.toVector.sorted.mkString(", ") + ")" ::
+ Nil
+ }
+}
+
+object PointView extends TableView[Point] {
+ def header: List[String] = "Name" :: "Category" :: "Unit" :: "Types" :: Nil
+
+ def row(obj: Point): List[String] = {
+ obj.getName ::
+ obj.getPointCategory.toString ::
+ obj.getUnit ::
+ "(" + obj.getTypesList.toVector.sorted.mkString(", ") + ")" ::
+ Nil
+ }
+}
+object PointWithCommandsView extends TableView[(Point, Seq[String])] {
+ def header: List[String] = "Name" :: "Category" :: "Unit" :: "Commands" :: "Types" :: Nil
+
+ def row(obj: (Point, Seq[String])): List[String] = {
+ obj._1.getName ::
+ obj._1.getPointCategory.toString ::
+ obj._1.getUnit ::
+ obj._2.mkString(", ") ::
+ "(" + obj._1.getTypesList.toVector.sorted.mkString(", ") + ")" ::
+ Nil
+ }
+}
+
+object CommandView extends TableView[Command] {
+ def header: List[String] = "Name" :: "DisplayName" :: "Category" :: "Types" :: Nil
+
+ def row(obj: Command): List[String] = {
+ obj.getName ::
+ obj.getDisplayName ::
+ obj.getCommandCategory.toString ::
+ "(" + obj.getTypesList.toVector.sorted.mkString(", ") + ")" ::
+ Nil
+ }
+}
+
+object CommandWithPointsView extends TableView[(Command, Seq[String])] {
+ def header: List[String] = "Name" :: "DisplayName" :: "Category" :: "Points" :: "Types" :: Nil
+
+ def row(obj: (Command, Seq[String])): List[String] = {
+ obj._1.getName ::
+ obj._1.getDisplayName ::
+ obj._1.getCommandCategory.toString ::
+ obj._2.mkString(", ") ::
+ "(" + obj._1.getTypesList.toVector.sorted.mkString(", ") + ")" ::
+ Nil
+ }
+}
diff --git a/cli/src/main/scala/io/greenbus/cli/view/KvView.scala b/cli/src/main/scala/io/greenbus/cli/view/KvView.scala
new file mode 100644
index 0000000..1f882aa
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/view/KvView.scala
@@ -0,0 +1,66 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli.view
+
+import io.greenbus.client.service.proto.Model.{ EntityKeyValue, StoredValue }
+
+object KvView extends TableView[(String, EntityKeyValue)] {
+
+ def storedValueRow(v: StoredValue): String = {
+ if (v.hasBoolValue) {
+ v.getBoolValue.toString
+ } else if (v.hasInt32Value) {
+ v.getInt32Value.toString
+ } else if (v.hasInt64Value) {
+ v.getInt64Value.toString
+ } else if (v.hasUint32Value) {
+ v.getUint32Value.toString
+ } else if (v.hasUint64Value) {
+ v.getUint64Value.toString
+ } else if (v.hasDoubleValue) {
+ v.getDoubleValue.toString
+ } else if (v.hasStringValue) {
+ "\"" + v.getStringValue + "\""
+ } else if (v.hasByteArrayValue) {
+ val size = v.getByteArrayValue.size()
+ s"[bytes] ($size)"
+ } else {
+ "?"
+ }
+ }
+
+ def header: List[String] = "Name" :: "Key" :: "Value" :: Nil
+
+ def row(obj: (String, EntityKeyValue)): List[String] = {
+ obj._1 ::
+ obj._2.getKey ::
+ storedValueRow(obj._2.getValue) ::
+ Nil
+ }
+
+ def printInspect(v: EntityKeyValue) = {
+ val lines =
+ ("uuid" :: v.getUuid.getValue :: Nil) ::
+ ("key" :: v.getKey :: Nil) ::
+ ("value" :: storedValueRow(v.getValue) :: Nil) ::
+ Nil
+
+ Table.renderRows(lines, " | ")
+ }
+}
diff --git a/cli/src/main/scala/io/greenbus/cli/view/MeasView.scala b/cli/src/main/scala/io/greenbus/cli/view/MeasView.scala
new file mode 100755
index 0000000..c1655d2
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/view/MeasView.scala
@@ -0,0 +1,150 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli.view
+
+import io.greenbus.client.service.proto.Measurements.{ Measurement, PointMeasurementValue, Quality }
+import io.greenbus.client.service.proto.Model.Point
+
+object MeasurementHistoryView extends TableView[Measurement] {
+ import io.greenbus.cli.view.MeasViewCommon._
+ def header: List[String] = "Time" :: "Value" :: "Type" :: "Q" :: "Off" :: Nil
+
+ def row(m: Measurement): List[String] = {
+ val (value, typ) = valueAndType(m)
+ timeString(m) :: value.toString :: typ :: shortQuality(m) :: offset(m) :: Nil
+ }
+
+}
+
+object MeasWithEmptiesView extends TableView[(Point, Option[Measurement])] {
+ import io.greenbus.cli.view.MeasViewCommon._
+
+ def header: List[String] = "Name" :: "Value" :: "Type" :: "Unit" :: "Q" :: "Time" :: "Off" :: Nil
+
+ def row(obj: (Point, Option[Measurement])): List[String] = {
+
+ obj._2 match {
+ case None => obj._1.getName :: "--" :: "--" :: "--" :: "--" :: "--" :: "--" :: Nil
+ case Some(m) =>
+ val (value, typ) = valueAndType(m)
+ obj._1.getName :: value.toString :: typ :: unit(obj._1) :: shortQuality(m) :: timeString(m) :: offset(m) :: Nil
+ }
+ }
+
+}
+
+object SimpleMeasurementView extends TableView[(String, Measurement, String)] {
+ import io.greenbus.cli.view.MeasViewCommon._
+
+ def header: List[String] = "Name" :: "Value" :: "Type" :: "Unit" :: "Q" :: "Time" :: "Off" :: Nil
+
+ def row(obj: (String, Measurement, String)): List[String] = {
+ val (name, m, unit) = obj
+ val (value, typ) = valueAndType(m)
+ name :: value.toString :: typ :: unit :: shortQuality(m) :: timeString(m) :: offset(m) :: Nil
+ }
+
+}
+
+object MeasViewCommon {
+ def valueAndType(m: Measurement): (Any, String) = {
+ if (m.getType == Measurement.Type.BOOL) {
+ val repr = if (m.getBoolVal) "HIGH" else "LOW"
+ (repr, "Binary")
+ } else if (m.getType == Measurement.Type.DOUBLE) {
+ (String.format("%.3f", m.getDoubleVal.asInstanceOf[AnyRef]), "Analog")
+ } else if (m.getType == Measurement.Type.INT) {
+ (m.getIntVal.toString, "Analog")
+ } else if (m.getType == Measurement.Type.STRING) {
+ (m.getStringVal, "String")
+ } else {
+ ("(unknown)", "(unknown)")
+ }
+ }
+
+ def value(m: Measurement): Any = {
+ val (value, typ) = valueAndType(m)
+ value
+ }
+
+ def unit(p: Point) = if (p.hasUnit) p.getUnit else ""
+
+ def shortQuality(m: Measurement) = {
+ val q = m.getQuality
+
+ if (q.getSource == Quality.Source.SUBSTITUTED) {
+ "R"
+ } else if (q.getOperatorBlocked) {
+ "N"
+ } else if (q.getTest) {
+ "T"
+ } else if (q.getDetailQual.getOldData) {
+ "O"
+ } else if (q.getValidity == Quality.Validity.QUESTIONABLE) {
+ "A"
+ } else if (q.getValidity != Quality.Validity.GOOD) {
+ "B"
+ } else {
+ ""
+ }
+ }
+
+ def longQuality(m: Measurement): String = {
+ val q = m.getQuality
+ longQuality(q)
+ }
+
+ def longQuality(q: Quality): String = {
+ val dq = q.getDetailQual
+
+ var list = List.empty[String]
+ if (q.getOperatorBlocked) list ::= "NIS"
+ if (q.getSource == Quality.Source.SUBSTITUTED) list ::= "replaced"
+ if (q.getTest) list ::= "test"
+ if (dq.getOverflow) list ::= "overflow"
+ if (dq.getOutOfRange) list ::= "out of range"
+ if (dq.getBadReference) list ::= "bad reference"
+ if (dq.getOscillatory) list ::= "oscillatory"
+ if (dq.getFailure) list ::= "failure"
+ if (dq.getOldData) list ::= "old"
+ if (dq.getInconsistent) list ::= "inconsistent"
+ if (dq.getInaccurate) list ::= "inaccurate"
+
+ val overall = q.getValidity match {
+ case Quality.Validity.GOOD => "Good"
+ case Quality.Validity.INVALID => "Invalid"
+ case Quality.Validity.QUESTIONABLE => "Questionable"
+ }
+
+ overall + " (" + list.reverse.mkString("; ") + ")"
+ }
+
+ def timeString(m: Measurement): String = {
+ (if (m.hasIsDeviceTime && m.getIsDeviceTime) "~" else "") + new java.util.Date(m.getTime).toString
+ }
+ def systemTimeString(m: Measurement): String = {
+ if (m.hasSystemTime) new java.util.Date(m.getSystemTime).toString
+ else "--"
+ }
+
+ def offset(m: Measurement): String = {
+ if (m.hasSystemTime) (m.getSystemTime - m.getTime).toString
+ else "--"
+ }
+}
\ No newline at end of file
diff --git a/cli/src/main/scala/io/greenbus/cli/view/Table.scala b/cli/src/main/scala/io/greenbus/cli/view/Table.scala
new file mode 100755
index 0000000..30654d4
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/view/Table.scala
@@ -0,0 +1,79 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli.view
+
+import java.io.PrintStream
+
+object Table {
+
+ val colSeparator = " | "
+
+ def normalizeNumCols(rows: Seq[Seq[String]]) = {
+ val max = rows.map(_.length).max
+ rows.map(row => row.padTo(max, ""))
+ }
+
+ def getWidths(rows: Seq[Seq[String]], minWidths: Seq[Int] = List.empty[Int]): Seq[Int] = {
+ rows.foldLeft(minWidths) {
+ case (widths, row) =>
+ val w = if (widths.length < row.length) widths.padTo(row.length, 0) else widths
+ row.map(_.length).zip(w).map { case (a, b) => if (a > b) a else b }
+ }
+ }
+
+ def justifyColumns(rows: Seq[Seq[String]], widths: Seq[Int]) = {
+ rows.map { row =>
+ justifyColumnsInRow(row, widths)
+ }
+ }
+
+ def justifyColumnsInRow(row: Seq[String], widths: Seq[Int]) = {
+ row.zip(widths).map {
+ case (str, width) =>
+ str.padTo(width, " ").mkString
+ }
+ }
+
+ def rowLength(line: Seq[String]) = {
+ line.foldLeft(0)(_ + _.length)
+ }
+
+ def printTable(header: Seq[String], rows: Seq[Seq[String]], minWidths: Seq[Int] = Seq.empty[Int], stream: PrintStream = Console.out): Seq[Int] = {
+ val overallList = normalizeNumCols(Seq(header) ++ rows)
+ val widths = getWidths(overallList, minWidths)
+ val just = justifyColumns(overallList, widths)
+ val headStr = just.head.mkString(" ")
+ //stream.println("Found: " + rows.size)
+ stream.println(headStr)
+ stream.println("".padTo(headStr.length, "-").mkString)
+ just.tail.foreach(line => stream.println(line.mkString(colSeparator)))
+ widths
+ }
+
+ def renderRows(rows: Seq[Seq[String]], sep: String = "", minWidths: Seq[Int] = List.empty[Int], stream: PrintStream = Console.out): Seq[Int] = {
+ val normalizedRows = normalizeNumCols(rows)
+ val widths = getWidths(normalizedRows, minWidths)
+ Table.justifyColumns(normalizedRows, widths).foreach(line => stream.println(line.mkString(sep)))
+ widths
+ }
+
+ def renderTableRow(row: Seq[String], widths: Seq[Int], sep: String = colSeparator, stream: PrintStream = Console.out) = {
+ stream.println(Table.justifyColumnsInRow(row, widths).mkString(sep))
+ }
+}
\ No newline at end of file
diff --git a/cli/src/main/scala/io/greenbus/cli/view/TableView.scala b/cli/src/main/scala/io/greenbus/cli/view/TableView.scala
new file mode 100644
index 0000000..e800f86
--- /dev/null
+++ b/cli/src/main/scala/io/greenbus/cli/view/TableView.scala
@@ -0,0 +1,32 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli.view
+
+trait TableView[A] {
+ def printTable(objs: Seq[A], minColWidths: Seq[Int] = List.empty[Int]): Seq[Int] = {
+ Table.printTable(header, objs map row)
+ }
+
+ def printRows(objs: Seq[A], minColWidths: Seq[Int] = List.empty[Int]): Seq[Int] = {
+ Table.renderRows(objs map row, sep = Table.colSeparator, minWidths = minColWidths)
+ }
+
+ def header: Seq[String]
+ def row(obj: A): Seq[String]
+}
diff --git a/cli/src/test/scala/io/greenbus/cli/Documenter.scala b/cli/src/test/scala/io/greenbus/cli/Documenter.scala
new file mode 100644
index 0000000..82ebefd
--- /dev/null
+++ b/cli/src/test/scala/io/greenbus/cli/Documenter.scala
@@ -0,0 +1,69 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli
+
+object Documenter {
+
+ def main(args: Array[String]): Unit = {
+ val commands = CliMain.commandList.map(f => f())
+
+ val sorted = commands.sortBy(_.commandName)
+
+ val namePrefix = "#### "
+ val usagePrefix = "\t"
+
+ val descriptions = sorted.map { cmd =>
+ val sb = new StringBuilder
+
+ sb.append(namePrefix + cmd.commandName + "\n")
+ sb.append("\n")
+ sb.append(cmd.description + "\n")
+ sb.append("\n")
+ sb.append("Usage:\n")
+ sb.append("\n")
+ sb.append(usagePrefix + cmd.syntax + "\n")
+ sb.append("\n")
+
+ val args = cmd.argumentDescriptions
+
+ if (args.nonEmpty) {
+ sb.append("Arguments:\n")
+ sb.append("\n")
+ args.foreach { arg =>
+ sb.append("`" + arg.name + "`" + " - " + arg.displayName + "\n\n")
+ }
+ }
+
+ val options = cmd.optionDescriptions
+
+ if (options.nonEmpty) {
+ sb.append("Options:\n")
+ sb.append("\n")
+ options.foreach { opt =>
+ sb.append("\t" + Command.optionString(opt) + "\n\n")
+ sb.append(opt.desc + "\n\n")
+ }
+ }
+
+ sb.result()
+ }
+
+ println(descriptions.mkString("\n"))
+ }
+}
diff --git a/cli/src/test/scala/io/greenbus/cli/ParserTest.scala b/cli/src/test/scala/io/greenbus/cli/ParserTest.scala
new file mode 100644
index 0000000..85ef7cb
--- /dev/null
+++ b/cli/src/test/scala/io/greenbus/cli/ParserTest.scala
@@ -0,0 +1,66 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli
+
+import org.junit.runner.RunWith
+import org.scalatest.FunSuite
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.matchers.ShouldMatchers
+
+@RunWith(classOf[JUnitRunner])
+class ParserTest extends FunSuite with ShouldMatchers {
+
+ class TestAll extends Command[Boolean] {
+
+ val commandName = "Test01"
+ val description = "Test01"
+
+ val name = strings.arg("entity name", "Entity name")
+ val uuid = strings.argOptional("entity uuid", "Entity uuid")
+
+ val displayName = strings.option(Some("d"), Some("display"), "Display name")
+ val types = strings.optionRepeated(Some("t"), Some("type"), "Entity type")
+
+ protected def execute(context: Boolean) {
+ }
+ }
+
+ test("multi") {
+ val cmd = new TestAll
+
+ cmd.run(SearchingTokenizer.tokenize("-t type01 --type type02 -d displayName01 name01 uuid01").toArray, false)
+
+ cmd.name.value should equal("name01")
+ cmd.uuid.value should equal(Some("uuid01"))
+ cmd.displayName.value should equal(Some("displayName01"))
+ cmd.types.value should equal(Seq("type01", "type02"))
+ }
+
+ test("after string encapsulated") {
+ val cmd = new TestAll
+
+ cmd.run(SearchingTokenizer.tokenize("-t type01 --type \"type 02\" -d displayName01 name01 uuid01").toArray, false)
+
+ cmd.name.value should equal("name01")
+ cmd.uuid.value should equal(Some("uuid01"))
+ cmd.displayName.value should equal(Some("displayName01"))
+ cmd.types.value should equal(Seq("type01", "type 02"))
+ }
+
+}
diff --git a/cli/src/test/scala/io/greenbus/cli/TokenizerTest.scala b/cli/src/test/scala/io/greenbus/cli/TokenizerTest.scala
new file mode 100644
index 0000000..fb15d6f
--- /dev/null
+++ b/cli/src/test/scala/io/greenbus/cli/TokenizerTest.scala
@@ -0,0 +1,43 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.cli
+
+import org.junit.runner.RunWith
+import org.scalatest.FunSuite
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.matchers.ShouldMatchers
+
+@RunWith(classOf[JUnitRunner])
+class TokenizerTest extends FunSuite with ShouldMatchers {
+
+ test("line tokenizing with quotes") {
+ val input = """ a thing is "something good" I think """
+ val input2 = """the quote is " on the end of the line """"
+
+ SearchingTokenizer.tokenize(input) should equal(List("a", "thing", "is", "something good", "I", "think"))
+ SearchingTokenizer.tokenize(input2) should equal(List("the", "quote", "is", " on the end of the line "))
+ }
+
+ test("line tokenizing with quotes realistic") {
+ val input = """ -o "a value" -b "another value" -d -v 5 """
+
+ SearchingTokenizer.tokenize(input) should equal(List("-o", "a value", "-b", "another value", "-d", "-v", "5"))
+ }
+
+}
diff --git a/client/pom.xml b/client/pom.xml
new file mode 100755
index 0000000..2e8423f
--- /dev/null
+++ b/client/pom.xml
@@ -0,0 +1,214 @@
+
+ 4.0.0
+
+
+ io.greenbus
+ greenbus-scala-base
+ 3.0.0
+ ../scala-base
+
+
+ greenbus-client
+ jar
+
+
+
+ Apache 2
+ http://www.apache.org/licenses/LICENSE-2.0.txt
+ A business-friendly OSS license
+
+
+
+
+
+
+
+ com.mycila.maven-license-plugin
+ maven-license-plugin
+
+
+
+ JAVADOC_STYLE
+
+
+ proto-include/google/**
+
+
+
+
+ org.totalgrid.maven
+ maven-protoc-plugin
+ 0.1.11
+
+ src/main/proto
+ true
+ ${project.build.directory}/generated-sources/java
+
+
+ proto-include/
+
+
+
+
+ msgscala
+ ${project.build.directory}/generated-sources/scala
+ protoc-gen-msgscala
+
+
+ msgjava
+ ${project.build.directory}/generated-sources/java
+ protoc-gen-msgjava
+
+
+
+
+
+
+ compile
+ testCompile
+
+
+
+
+
+
+ pl.project13.maven
+ git-commit-id-plugin
+ 1.9
+
+
+
+ initialize
+
+ revision
+
+
+
+
+ ${project.basedir}/.git
+
+
+
+ com.google.code.maven-replacer-plugin
+ maven-replacer-plugin
+ 1.3.9
+
+
+ generate-sources
+
+ replace
+
+
+
+
+
+ target/generated-sources/scala/io/greenbus/client/version/Version.scala
+
+
+ src/main/resources/Version.scala.template
+
+
+
+ PROJECT_VERSION
+ ${project.version}
+
+
+ GIT_COMMIT_ID
+ ${git.commit.id}
+
+
+
+
+
+ maven-assembly-plugin
+
+
+ jar-with-dependencies
+
+
+
+
+ make-assembly
+ package
+
+ single
+
+
+
+
+
+
+
+
+
+
+ doc
+
+
+
+ org.totalgrid.maven
+ maven-protoc-plugin
+ 0.1.11
+
+
+
+ msgscala
+ ${project.build.directory}/generated-sources/scala
+ protoc-gen-msgscala
+
+
+ msgjava
+ ${project.build.directory}/generated-sources/java
+ protoc-gen-msgjava
+
+
+ msgdoc
+ ${project.build.directory}/greenbus-msg-proto-docs
+ protoc-gen-msgdoc
+
+
+
+
+
+
+
+
+
+
+
+ io.greenbus.msg
+ greenbus-msg-amqp
+ 1.0.0
+
+
+ io.greenbus.msg
+ greenbus-msg-amqp-java
+ 1.0.0
+
+
+ io.greenbus.msg
+ greenbus-msg-messaging
+ 1.0.0
+
+
+ io.greenbus.msg
+ greenbus-msg-messaging-java
+ 1.0.0
+
+
+ io.greenbus.msg
+ greenbus-msg-proto-ext
+ 1.0.0
+
+
+ io.greenbus.msg
+ greenbus-msg-compiler
+ 1.0.0
+ jar-with-dependencies
+ provided
+
+
+
+
+
diff --git a/client/proto-include/CompilerExtensions.proto b/client/proto-include/CompilerExtensions.proto
new file mode 100644
index 0000000..1bb3598
--- /dev/null
+++ b/client/proto-include/CompilerExtensions.proto
@@ -0,0 +1,43 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+option java_package = "io.greenbus.msg.compiler.proto";
+option java_outer_classname = "CompilerExtensions";
+option java_generic_services = false;
+
+import "google/protobuf/descriptor.proto";
+
+extend google.protobuf.FieldOptions {
+ optional bool field_optional = 50002;
+}
+
+enum ServiceAddressing {
+ NEVER = 1;
+ OPTIONALLY = 2;
+ ALWAYS = 3;
+}
+
+extend google.protobuf.MethodOptions {
+ optional ServiceAddressing addressed = 50006;
+ optional string subscription_type = 50007;
+}
+
+extend google.protobuf.ServiceOptions {
+ optional string scala_package = 50011;
+ optional string java_package = 50012;
+}
diff --git a/client/proto-include/google/protobuf/descriptor.proto b/client/proto-include/google/protobuf/descriptor.proto
new file mode 100644
index 0000000..cc04aa8
--- /dev/null
+++ b/client/proto-include/google/protobuf/descriptor.proto
@@ -0,0 +1,433 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// http://code.google.com/p/protobuf/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// The messages in this file describe the definitions found in .proto files.
+// A valid .proto file can be translated directly to a FileDescriptorProto
+// without any other information (e.g. without reading its imports).
+
+
+
+package google.protobuf;
+option java_package = "com.google.protobuf";
+option java_outer_classname = "DescriptorProtos";
+
+// descriptor.proto must be optimized for speed because reflection-based
+// algorithms don't work during bootstrapping.
+option optimize_for = SPEED;
+
+// The protocol compiler can output a FileDescriptorSet containing the .proto
+// files it parses.
+message FileDescriptorSet {
+ repeated FileDescriptorProto file = 1;
+}
+
+// Describes a complete .proto file.
+message FileDescriptorProto {
+ optional string name = 1; // file name, relative to root of source tree
+ optional string package = 2; // e.g. "foo", "foo.bar", etc.
+
+ // Names of files imported by this file.
+ repeated string dependency = 3;
+
+ // All top-level definitions in this file.
+ repeated DescriptorProto message_type = 4;
+ repeated EnumDescriptorProto enum_type = 5;
+ repeated ServiceDescriptorProto service = 6;
+ repeated FieldDescriptorProto extension = 7;
+
+ optional FileOptions options = 8;
+}
+
+// Describes a message type.
+message DescriptorProto {
+ optional string name = 1;
+
+ repeated FieldDescriptorProto field = 2;
+ repeated FieldDescriptorProto extension = 6;
+
+ repeated DescriptorProto nested_type = 3;
+ repeated EnumDescriptorProto enum_type = 4;
+
+ message ExtensionRange {
+ optional int32 start = 1;
+ optional int32 end = 2;
+ }
+ repeated ExtensionRange extension_range = 5;
+
+ optional MessageOptions options = 7;
+}
+
+// Describes a field within a message.
+message FieldDescriptorProto {
+ enum Type {
+ // 0 is reserved for errors.
+ // Order is weird for historical reasons.
+ TYPE_DOUBLE = 1;
+ TYPE_FLOAT = 2;
+ TYPE_INT64 = 3; // Not ZigZag encoded. Negative numbers
+ // take 10 bytes. Use TYPE_SINT64 if negative
+ // values are likely.
+ TYPE_UINT64 = 4;
+ TYPE_INT32 = 5; // Not ZigZag encoded. Negative numbers
+ // take 10 bytes. Use TYPE_SINT32 if negative
+ // values are likely.
+ TYPE_FIXED64 = 6;
+ TYPE_FIXED32 = 7;
+ TYPE_BOOL = 8;
+ TYPE_STRING = 9;
+ TYPE_GROUP = 10; // Tag-delimited aggregate.
+ TYPE_MESSAGE = 11; // Length-delimited aggregate.
+
+ // New in version 2.
+ TYPE_BYTES = 12;
+ TYPE_UINT32 = 13;
+ TYPE_ENUM = 14;
+ TYPE_SFIXED32 = 15;
+ TYPE_SFIXED64 = 16;
+ TYPE_SINT32 = 17; // Uses ZigZag encoding.
+ TYPE_SINT64 = 18; // Uses ZigZag encoding.
+ };
+
+ enum Label {
+ // 0 is reserved for errors
+ LABEL_OPTIONAL = 1;
+ LABEL_REQUIRED = 2;
+ LABEL_REPEATED = 3;
+ // TODO(sanjay): Should we add LABEL_MAP?
+ };
+
+ optional string name = 1;
+ optional int32 number = 3;
+ optional Label label = 4;
+
+ // If type_name is set, this need not be set. If both this and type_name
+ // are set, this must be either TYPE_ENUM or TYPE_MESSAGE.
+ optional Type type = 5;
+
+ // For message and enum types, this is the name of the type. If the name
+ // starts with a '.', it is fully-qualified. Otherwise, C++-like scoping
+ // rules are used to find the type (i.e. first the nested types within this
+ // message are searched, then within the parent, on up to the root
+ // namespace).
+ optional string type_name = 6;
+
+ // For extensions, this is the name of the type being extended. It is
+ // resolved in the same manner as type_name.
+ optional string extendee = 2;
+
+ // For numeric types, contains the original text representation of the value.
+ // For booleans, "true" or "false".
+ // For strings, contains the default text contents (not escaped in any way).
+ // For bytes, contains the C escaped value. All bytes >= 128 are escaped.
+ // TODO(kenton): Base-64 encode?
+ optional string default_value = 7;
+
+ optional FieldOptions options = 8;
+}
+
+// Describes an enum type.
+message EnumDescriptorProto {
+ optional string name = 1;
+
+ repeated EnumValueDescriptorProto value = 2;
+
+ optional EnumOptions options = 3;
+}
+
+// Describes a value within an enum.
+message EnumValueDescriptorProto {
+ optional string name = 1;
+ optional int32 number = 2;
+
+ optional EnumValueOptions options = 3;
+}
+
+// Describes a service.
+message ServiceDescriptorProto {
+ optional string name = 1;
+ repeated MethodDescriptorProto method = 2;
+
+ optional ServiceOptions options = 3;
+}
+
+// Describes a method of a service.
+message MethodDescriptorProto {
+ optional string name = 1;
+
+ // Input and output type names. These are resolved in the same way as
+ // FieldDescriptorProto.type_name, but must refer to a message type.
+ optional string input_type = 2;
+ optional string output_type = 3;
+
+ optional MethodOptions options = 4;
+}
+
+// ===================================================================
+// Options
+
+// Each of the definitions above may have "options" attached. These are
+// just annotations which may cause code to be generated slightly differently
+// or may contain hints for code that manipulates protocol messages.
+//
+// Clients may define custom options as extensions of the *Options messages.
+// These extensions may not yet be known at parsing time, so the parser cannot
+// store the values in them. Instead it stores them in a field in the *Options
+// message called uninterpreted_option. This field must have the same name
+// across all *Options messages. We then use this field to populate the
+// extensions when we build a descriptor, at which point all protos have been
+// parsed and so all extensions are known.
+//
+// Extension numbers for custom options may be chosen as follows:
+// * For options which will only be used within a single application or
+// organization, or for experimental options, use field numbers 50000
+// through 99999. It is up to you to ensure that you do not use the
+// same number for multiple options.
+// * For options which will be published and used publicly by multiple
+// independent entities, e-mail kenton@google.com to reserve extension
+// numbers. Simply tell me how many you need and I'll send you back a
+// set of numbers to use -- there's no need to explain how you intend to
+// use them. If this turns out to be popular, a web service will be set up
+// to automatically assign option numbers.
+
+
+message FileOptions {
+
+ // Sets the Java package where classes generated from this .proto will be
+ // placed. By default, the proto package is used, but this is often
+ // inappropriate because proto packages do not normally start with backwards
+ // domain names.
+ optional string java_package = 1;
+
+
+ // If set, all the classes from the .proto file are wrapped in a single
+ // outer class with the given name. This applies to both Proto1
+ // (equivalent to the old "--one_java_file" option) and Proto2 (where
+ // a .proto always translates to a single class, but you may want to
+ // explicitly choose the class name).
+ optional string java_outer_classname = 8;
+
+ // If set true, then the Java code generator will generate a separate .java
+ // file for each top-level message, enum, and service defined in the .proto
+ // file. Thus, these types will *not* be nested inside the outer class
+ // named by java_outer_classname. However, the outer class will still be
+ // generated to contain the file's getDescriptor() method as well as any
+ // top-level extensions defined in the file.
+ optional bool java_multiple_files = 10 [default=false];
+
+ // Generated classes can be optimized for speed or code size.
+ enum OptimizeMode {
+ SPEED = 1; // Generate complete code for parsing, serialization,
+ // etc.
+ CODE_SIZE = 2; // Use ReflectionOps to implement these methods.
+ LITE_RUNTIME = 3; // Generate code using MessageLite and the lite runtime.
+ }
+ optional OptimizeMode optimize_for = 9 [default=SPEED];
+
+
+
+
+ // Should generic services be generated in each language? "Generic" services
+ // are not specific to any particular RPC system. They are generated by the
+ // main code generators in each language (without additional plugins).
+ // Generic services were the only kind of service generation supported by
+ // early versions of proto2.
+ //
+ // Generic services are now considered deprecated in favor of using plugins
+ // that generate code specific to your particular RPC system. If you are
+ // using such a plugin, set these to false. In the future, we may change
+ // the default to false, so if you explicitly want generic services, you
+ // should explicitly set these to true.
+ optional bool cc_generic_services = 16 [default=true];
+ optional bool java_generic_services = 17 [default=true];
+ optional bool py_generic_services = 18 [default=true];
+
+ // The parser stores options it doesn't recognize here. See above.
+ repeated UninterpretedOption uninterpreted_option = 999;
+
+ // Clients can define custom options in extensions of this message. See above.
+ extensions 1000 to max;
+}
+
+message MessageOptions {
+ // Set true to use the old proto1 MessageSet wire format for extensions.
+ // This is provided for backwards-compatibility with the MessageSet wire
+ // format. You should not use this for any other reason: It's less
+ // efficient, has fewer features, and is more complicated.
+ //
+ // The message must be defined exactly as follows:
+ // message Foo {
+ // option message_set_wire_format = true;
+ // extensions 4 to max;
+ // }
+ // Note that the message cannot have any defined fields; MessageSets only
+ // have extensions.
+ //
+ // All extensions of your type must be singular messages; e.g. they cannot
+ // be int32s, enums, or repeated messages.
+ //
+ // Because this is an option, the above two restrictions are not enforced by
+ // the protocol compiler.
+ optional bool message_set_wire_format = 1 [default=false];
+
+ // Disables the generation of the standard "descriptor()" accessor, which can
+ // conflict with a field of the same name. This is meant to make migration
+ // from proto1 easier; new code should avoid fields named "descriptor".
+ optional bool no_standard_descriptor_accessor = 2 [default=false];
+
+ // The parser stores options it doesn't recognize here. See above.
+ repeated UninterpretedOption uninterpreted_option = 999;
+
+ // Clients can define custom options in extensions of this message. See above.
+ extensions 1000 to max;
+}
+
+message FieldOptions {
+ // The ctype option instructs the C++ code generator to use a different
+ // representation of the field than it normally would. See the specific
+ // options below. This option is not yet implemented in the open source
+ // release -- sorry, we'll try to include it in a future version!
+ optional CType ctype = 1 [default = STRING];
+ enum CType {
+ // Default mode.
+ STRING = 0;
+
+ CORD = 1;
+
+ STRING_PIECE = 2;
+ }
+ // The packed option can be enabled for repeated primitive fields to enable
+ // a more efficient representation on the wire. Rather than repeatedly
+ // writing the tag and type for each element, the entire array is encoded as
+ // a single length-delimited blob.
+ optional bool packed = 2;
+
+
+ // Is this field deprecated?
+ // Depending on the target platform, this can emit Deprecated annotations
+ // for accessors, or it will be completely ignored; in the very least, this
+ // is a formalization for deprecating fields.
+ optional bool deprecated = 3 [default=false];
+
+ // EXPERIMENTAL. DO NOT USE.
+ // For "map" fields, the name of the field in the enclosed type that
+ // is the key for this map. For example, suppose we have:
+ // message Item {
+ // required string name = 1;
+ // required string value = 2;
+ // }
+ // message Config {
+ // repeated Item items = 1 [experimental_map_key="name"];
+ // }
+ // In this situation, the map key for Item will be set to "name".
+ // TODO: Fully-implement this, then remove the "experimental_" prefix.
+ optional string experimental_map_key = 9;
+
+ // The parser stores options it doesn't recognize here. See above.
+ repeated UninterpretedOption uninterpreted_option = 999;
+
+ // Clients can define custom options in extensions of this message. See above.
+ extensions 1000 to max;
+}
+
+message EnumOptions {
+
+ // The parser stores options it doesn't recognize here. See above.
+ repeated UninterpretedOption uninterpreted_option = 999;
+
+ // Clients can define custom options in extensions of this message. See above.
+ extensions 1000 to max;
+}
+
+message EnumValueOptions {
+ // The parser stores options it doesn't recognize here. See above.
+ repeated UninterpretedOption uninterpreted_option = 999;
+
+ // Clients can define custom options in extensions of this message. See above.
+ extensions 1000 to max;
+}
+
+message ServiceOptions {
+
+ // Note: Field numbers 1 through 32 are reserved for Google's internal RPC
+ // framework. We apologize for hoarding these numbers to ourselves, but
+ // we were already using them long before we decided to release Protocol
+ // Buffers.
+
+ // The parser stores options it doesn't recognize here. See above.
+ repeated UninterpretedOption uninterpreted_option = 999;
+
+ // Clients can define custom options in extensions of this message. See above.
+ extensions 1000 to max;
+}
+
+message MethodOptions {
+
+ // Note: Field numbers 1 through 32 are reserved for Google's internal RPC
+ // framework. We apologize for hoarding these numbers to ourselves, but
+ // we were already using them long before we decided to release Protocol
+ // Buffers.
+
+ // The parser stores options it doesn't recognize here. See above.
+ repeated UninterpretedOption uninterpreted_option = 999;
+
+ // Clients can define custom options in extensions of this message. See above.
+ extensions 1000 to max;
+}
+
+// A message representing a option the parser does not recognize. This only
+// appears in options protos created by the compiler::Parser class.
+// DescriptorPool resolves these when building Descriptor objects. Therefore,
+// options protos in descriptor objects (e.g. returned by Descriptor::options(),
+// or produced by Descriptor::CopyTo()) will never have UninterpretedOptions
+// in them.
+message UninterpretedOption {
+ // The name of the uninterpreted option. Each string represents a segment in
+ // a dot-separated name. is_extension is true iff a segment represents an
+ // extension (denoted with parentheses in options specs in .proto files).
+ // E.g.,{ ["foo", false], ["bar.baz", true], ["qux", false] } represents
+ // "foo.(bar.baz).qux".
+ message NamePart {
+ required string name_part = 1;
+ required bool is_extension = 2;
+ }
+ repeated NamePart name = 2;
+
+ // The value of the uninterpreted option, in whatever type the tokenizer
+ // identified it as during parsing. Exactly one of these should be set.
+ optional string identifier_value = 3;
+ optional uint64 positive_int_value = 4;
+ optional int64 negative_int_value = 5;
+ optional double double_value = 6;
+ optional bytes string_value = 7;
+}
diff --git a/client/src/main/java/io/greenbus/japi/client/ServiceConnection.java b/client/src/main/java/io/greenbus/japi/client/ServiceConnection.java
new file mode 100644
index 0000000..82d2cef
--- /dev/null
+++ b/client/src/main/java/io/greenbus/japi/client/ServiceConnection.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.japi.client;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import io.greenbus.msg.japi.ConnectionCloseListener;
+import io.greenbus.msg.japi.Session;
+import io.greenbus.msg.japi.amqp.AmqpServiceOperations;
+
+public interface ServiceConnection
+{
+
+ void addConnectionCloseListener( ConnectionCloseListener listener );
+
+ void removeConnectionCloseListener( ConnectionCloseListener listener );
+
+ void disconnect();
+
+ ListenableFuture login( String user, String password );
+
+ Session createSession();
+
+ AmqpServiceOperations getServiceOperations();
+
+}
diff --git a/client/src/main/java/io/greenbus/japi/client/ServiceConnectionFactory.java b/client/src/main/java/io/greenbus/japi/client/ServiceConnectionFactory.java
new file mode 100644
index 0000000..e73725e
--- /dev/null
+++ b/client/src/main/java/io/greenbus/japi/client/ServiceConnectionFactory.java
@@ -0,0 +1,128 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.japi.client;
+
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import io.greenbus.msg.amqp.AmqpBroker;
+import io.greenbus.msg.amqp.japi.AmqpConnection;
+import io.greenbus.msg.amqp.japi.AmqpConnectionFactory;
+import io.greenbus.msg.amqp.japi.AmqpSettings;
+import io.greenbus.msg.japi.ConnectionCloseListener;
+import io.greenbus.msg.japi.Session;
+import io.greenbus.msg.japi.amqp.AmqpServiceOperations;
+import io.greenbus.client.ServiceHeaders;
+import io.greenbus.client.ServiceMessagingCodec;
+import io.greenbus.client.exception.InternalServiceException;
+import io.greenbus.client.service.proto.LoginRequests;
+import io.greenbus.client.version.Version;
+import io.greenbus.japi.client.service.LoginService;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+public class ServiceConnectionFactory
+{
+
+ public static ServiceConnection create( AmqpSettings amqpSettings, AmqpBroker broker, long timeoutMs )
+ {
+
+ final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool( 5 );
+
+ final AmqpConnectionFactory amqpConnectionFactory = new AmqpConnectionFactory( amqpSettings, broker, timeoutMs, scheduledExecutorService );
+
+ final AmqpConnection amqpConnection = amqpConnectionFactory.connect();
+
+ return new DefaultConnection( amqpConnection, scheduledExecutorService );
+ }
+
+
+ private static class DefaultConnection implements ServiceConnection
+ {
+
+ private final AmqpConnection connection;
+ private final ScheduledExecutorService executorService;
+
+ public DefaultConnection( AmqpConnection connection, ScheduledExecutorService executorService )
+ {
+ this.connection = connection;
+ this.executorService = executorService;
+ }
+
+ public void addConnectionCloseListener( ConnectionCloseListener listener )
+ {
+ connection.addConnectionCloseListener( listener );
+ }
+
+ public void removeConnectionCloseListener( ConnectionCloseListener listener )
+ {
+ connection.removeConnectionCloseListener( listener );
+ }
+
+ public void disconnect()
+ {
+ connection.disconnect();
+ executorService.shutdown();
+ }
+
+ @Override
+ public ListenableFuture login( String user, String password )
+ {
+
+ final Session session = createSession();
+
+ final LoginService.Client client = LoginService.client( session );
+
+ final LoginRequests.LoginRequest request =
+ LoginRequests.LoginRequest.newBuilder().setName( user ).setPassword( password ).setLoginLocation( "" ).setClientVersion(
+ Version.clientVersion() ).build();
+
+ final ListenableFuture loginFuture = client.login( request );
+
+ return Futures.transform( loginFuture, new AsyncFunction() {
+ @Override
+ public ListenableFuture apply( LoginRequests.LoginResponse input ) throws Exception
+ {
+ try
+ {
+ session.addHeader( ServiceHeaders.tokenHeader(), input.getToken() );
+ return Futures.immediateFuture( session );
+ }
+ catch ( Throwable ex )
+ {
+ return Futures.immediateFailedFuture( new InternalServiceException( "Problem handling login response: " + ex.getMessage() ) );
+ }
+ }
+ } );
+ }
+
+ public Session createSession()
+ {
+ return connection.createSession( ServiceMessagingCodec.codec() );
+ }
+
+ public AmqpServiceOperations getServiceOperations()
+ {
+ return connection.getServiceOperations();
+ }
+ }
+
+
+}
diff --git a/client/src/main/proto/Auth.proto b/client/src/main/proto/Auth.proto
new file mode 100755
index 0000000..c811d6c
--- /dev/null
+++ b/client/src/main/proto/Auth.proto
@@ -0,0 +1,196 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package greenbus.client;
+
+option java_package = "io.greenbus.client.service.proto";
+option java_outer_classname = "Auth";
+
+
+import "Model.proto";
+
+/*
+ Represents an actor in the system to be authenticated and authorized to perform
+ service requests.
+
+ Agents have an associated password not featured in the resource view.
+*/
+message Agent {
+
+ /*
+ Unique identifier across all Agents.
+ */
+ optional ModelUUID uuid = 1;
+
+ /*
+ Unique name across all Agents.
+ */
+ optional string name = 2;
+
+ /*
+ Names of PermissionSets associated with this Agent.
+ */
+ repeated string permission_sets = 4;
+}
+
+/*
+ Narrows the system model to a more specific set of Entities relevant to an authorization Permission.
+*/
+message EntitySelector {
+
+ /*
+ Specifies the _style_, or type, of selector. Options are:
+
+ - `*`: Selects all Entities. Equivalent to not having specified an EntitySelector. No arguments are required.
+ - `self`: Selects only the Agent associated with the auth token. An example would be to provide rename and password change
+ privileges for a user. No arguments are required.
+ - `type`: Selects Entities that have at least one of the specified types. Arguments are the relevant type list.
+ Must provide at least one argument.
+ - `parent`: Selects Entities by hierarchical relationship. Arguments are a list of Entity names that form the parent
+ set. All Entities that are children (immediate or derived) of one of the parents through an `owns` relationship will
+ be selected. Must provide at least one argument.
+
+ */
+ optional string style = 2;
+
+ /*
+ Arguments for the selector whose meaning depend on the selector style.
+
+ @optional
+ */
+ repeated string arguments = 3;
+}
+
+/*
+ Represents allowing or disallowing a set of actions across a set of resources, with
+ an optional filter for a set of Entities.
+
+ When multiple resources and multiple actions are specified, the authorization is applied for all combinations
+ of resources and actions.
+
+ Permissions for all resources default to denial. If allows and denies overlap, the resource/action
+ will be denied.
+*/
+message Permission {
+
+ /*
+ Whether the Permission allows (true) or disallows (false) the actions on the resource.
+ */
+ optional bool allow = 2;
+
+ /*
+ Resources (e.g. `entity`, `point`, `agent`) the permission applies to.
+
+ Using `*` will specify all resources.
+ */
+ repeated string resources = 3;
+
+ /*
+ Actions (e.g. `create`, `read`, `update`, `delete`) the permission allows or disallows.
+
+ Using `*` will specify all actions.
+ */
+ repeated string actions = 4;
+
+ /*
+ When provided, narrows the specified resources to apply only to a set of Entities in the system
+ model.
+
+ @optional
+ */
+ repeated EntitySelector selectors = 5;
+}
+
+/*
+ Represents a set of Permissions authorizing an Agent to perform service requests.
+*/
+message PermissionSet {
+
+ /*
+ Unique identifier across all PermissionSets.
+ */
+ optional ModelID id = 1;
+
+ /*
+ Unique name across all PermissionSets.
+ */
+ optional string name = 2;
+
+ /*
+ Descriptions of the allowed and disallowed actions that can be taken on resources.
+ */
+ repeated Permission permissions = 4;
+}
+
+
+/*
+ Represents an auth token entry created as a result of authentication on user login.
+*/
+message AuthToken {
+
+ /*
+ ID of this AuthToken.
+ */
+ optional ModelID id = 1;
+
+ /*
+ UUID of the Agent whose credentials were used to create this token.
+ */
+ optional ModelUUID agent_uuid = 2;
+
+ /*
+ Contextual information about the user or process obtaining the auth token.
+ */
+ optional string login_location = 3;
+
+ /*
+ PermissionSets granted by this auth token.
+ */
+ repeated PermissionSet permission_sets = 4;
+
+ /*
+ Auth token resulting from a successful login. Can be included in subsequent service requests in order to
+ authorize them.
+ */
+ optional string token = 5;
+
+ /*
+ Expiration time for this auth token. After this time the auth token will be rejected and a new token
+ will need to be acquired.
+
+ Time is measured in milliseconds since midnight, January 1, 1970 UTC.
+ */
+ optional uint64 expiration_time = 6;
+
+ /*
+ Provides contextual information about the client software version.
+ */
+ optional string client_version = 7;
+
+ /*
+ Whether the auth token has been revoked by a user logout or another process.
+ */
+ optional bool revoked = 8;
+
+ /*
+ Record of the time the auth token was issued.
+
+ Time is measured in milliseconds since midnight, January 1, 1970 UTC.
+ */
+ optional uint64 issue_time = 9;
+}
diff --git a/client/src/main/proto/Calculations.proto b/client/src/main/proto/Calculations.proto
new file mode 100755
index 0000000..174ed53
--- /dev/null
+++ b/client/src/main/proto/Calculations.proto
@@ -0,0 +1,290 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package greenbus.client;
+
+option java_package = "io.greenbus.client.service.proto";
+option java_outer_classname = "Calculations";
+
+import "ServiceEnvelopes.proto";
+import "Model.proto";
+import "Measurements.proto";
+
+
+/*
+ Describes how and why evaluation of the calculation is initiated. Calculations can be evaluated periodically or whenever notified
+ of a change to an input.
+
+ One field must be provided.
+*/
+message TriggerStrategy {
+
+ /*
+ Specifies that the calculation is to be evaluated periodically. The value represents the period in milliseconds
+ between evaluations.
+
+ @optional
+ */
+ optional uint64 period_ms = 1;
+
+ /*
+ If `true`, specifies that any change to an input should trigger the evaluation of the calculation. A value of
+ `false` has no meaning.
+
+ @optional
+ */
+ optional bool update_any = 3;
+}
+
+
+/*
+ Describes the range of time-series values used as inputs for the calculation.
+*/
+message MeasurementRange {
+
+ /*
+ If set to `true`, the input does not retrieve Measurement history and instead begins accumulating
+ time-series values from the time the calculation endpoint came online.
+
+ @optional
+ */
+ optional bool since_last = 4;
+
+ /*
+ Specifies an offset in milliseconds to the beginning of a time window of Measurement history to use
+ for this input. An offset to past times should be negative. For example, a value of `-5000` will create
+ a range that includes all Measurements since five seconds before the current time.
+
+ If the number of Measurements in that time window exceeds the `limit` field, the latest Measurements
+ will be used.
+
+ @optional
+ */
+ optional uint64 from_ms = 1;
+
+ /*
+ Provides a limit on the size of the Measurement sequence retained for the calculation input. The
+ limit will also apply to the request for Measurement history when the calculation is initialized.
+
+ If not specified, the measurement calculator will use a default value.
+
+ @optional
+ */
+ optional uint32 limit = 3;
+}
+
+/*
+ Describes the strategy for calculation inputs that are a single value.
+*/
+message SingleMeasurement {
+
+ /*
+ Enumeration of strategies.
+ */
+ enum MeasurementStrategy{
+ MOST_RECENT = 1; // Use the most recent value.
+ }
+
+ /*
+ Specifies the strategy for interpreting Measurements as calculation inputs.
+
+ @required
+ */
+ optional MeasurementStrategy strategy = 1;
+}
+
+/*
+ Describes an input to a calculation. Contains a reference to another Point in the system, its mapping to a
+ variable in the calculation formula, and the range of Measurement values relevant to the calculation.
+
+ One of `range` or `single` must be specified.
+*/
+message CalculationInput {
+
+ /*
+ UUID of the Point for the input.
+
+ @required
+ */
+ optional ModelUUID point_uuid = 1;
+
+ /*
+ Variable name of this input, to be referenced in the formula.
+
+ @required
+ */
+ optional string variable_name = 2;
+
+ /*
+ Specifies parameters for inputs that include multiple time-series Measurement values.
+
+ @optional
+ */
+ optional MeasurementRange range = 3;
+
+ /*
+ Specifies parameters for inputs that are singular.
+
+ @optional
+ */
+ optional SingleMeasurement single = 4;
+}
+
+/*
+ Describes how the quality of inputs should affect the calculation.
+*/
+message InputQuality {
+
+ /*
+ Enumerates the strategies for handling input quality.
+ */
+ enum Strategy {
+
+ /*
+ Measurements of any quality will be used as inputs.
+ */
+ ACCEPT_ALL = 1;
+
+ /*
+ Only evaluate the calculation when all input Measurements have `GOOD` quality.
+ */
+ ONLY_WHEN_ALL_OK = 2;
+
+ /*
+ Removes bad quality Measurements from the inputs and evaluates the calculation. For singular inputs,
+ this will result in an input not being present and the calculation not being evaluated. For ranged inputs,
+ only the poor quality Measurements in the range will be filtered out, and the calculation will proceed.
+ */
+ REMOVE_BAD_AND_CALC = 3;
+ }
+
+ /*
+ Specifies the strategy for handling input quality.
+ */
+ optional Strategy strategy = 1;
+}
+
+/*
+ Contains the strategy for determining the quality of the Measurement generated by the calculation.
+*/
+message OutputQuality {
+
+ /*
+ Enumeration for the strategies for determining the quality of the Measurement generated by the calculation.
+ */
+ enum Strategy{
+ WORST_QUALITY = 1; // Uses the "worst" quality of the input Measurements.
+ ALWAYS_OK = 2; // Calculated Measurements always have good quality.
+ }
+
+ /*
+ Specifies the strategy for determining the quality of the Measurement generated by the calculation.
+
+ @required
+ */
+ optional Strategy strategy = 1;
+}
+
+/*
+ Describes the inputs, the conditions for evaluation, the formula to be evaluated, and the output format
+ of a calculation.
+*/
+message CalculationDescriptor {
+
+ /*
+ Specifies a unit to be attached to the resulting Measurement. If not provided, no unit will be attached.
+
+ @optional
+ */
+ optional string unit = 2;
+
+ /*
+ Determines how and why evaluation of the calculation is initiated.
+
+ @required
+ */
+ optional TriggerStrategy triggering = 4;
+
+ /*
+ Specifies the inputs to the calculation, including the mapping to a variable in the formula and strategies for
+ acquiring and interpreting Measurement values.
+
+ @required
+ */
+ repeated CalculationInput calc_inputs = 5;
+
+ /*
+ Specifies the strategy for handling the quality of input Measurements.
+
+ @required
+ */
+ optional InputQuality triggering_quality = 6;
+
+ /*
+ Specifies the strategy for determining the quality of the Measurement generated by the calculation.
+
+ @required
+ */
+ optional OutputQuality quality_output = 7;
+
+ /*
+ Specifies the formula to be evaluated.
+
+ @required
+ */
+ optional string formula = 9;
+
+}
+
+/*
+ Association between a Point and a description of a calculation that generates Measurements for it.
+*/
+message Calculation {
+
+ /*
+ UUID of the Point this Calculation is associated with.
+
+ @required
+ */
+ optional ModelUUID point_uuid = 1;
+
+ /*
+ Description of the calculation to be performed.
+
+ @required
+ */
+ optional CalculationDescriptor calculation = 8;
+}
+
+
+/*
+ Notification of a change to the Calculation for a Point.
+*/
+message CalculationNotification {
+
+ /*
+ Whether object was added, removed, or modified.
+ */
+ optional greenbus.client.envelope.SubscriptionEventType event_type = 1;
+
+ /*
+ Updated state of the object.
+ */
+ optional Calculation value = 2;
+}
+
diff --git a/client/src/main/proto/Commands.proto b/client/src/main/proto/Commands.proto
new file mode 100755
index 0000000..9bddcbc
--- /dev/null
+++ b/client/src/main/proto/Commands.proto
@@ -0,0 +1,174 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package greenbus.client;
+
+option java_package = "io.greenbus.client.service.proto";
+option java_outer_classname = "Commands";
+
+import "Model.proto";
+
+
+/*
+ Represents the access table for the Command system. CommandLock entries have one or two
+ modes, _allowed_ and _blocked_. CommandRequests cannot be issued unless they have an
+ _allowed_ entry. This selects the command for operation by a single user, for
+ as long as the lock is held. Blocking a Command makes it so no CommandRequests can be
+ issued and no other CommandLocks can be acquired.
+
+ Multiple Commands can be referenced in the same CommandLock. The Agent
+ associated with the CommandLock is determined by the request header.
+*/
+message CommandLock {
+
+ /*
+ Enumerates the effect of a CommandLock.
+ */
+ enum AccessMode {
+ ALLOWED = 1; // The Agent may issue a CommandRequest for the Commands.
+ BLOCKED = 2; // No other Agents may lock or issue the Commands.
+ }
+
+ /*
+ ID for the CommandLock entry.
+ */
+ optional ModelID id = 1;
+
+ /*
+ Command UUIDs the CommandLock pertains to.
+ */
+ repeated ModelUUID command_uuids = 2;
+
+ /*
+ The type of AccessMode the CommandLock represents.
+ */
+ optional AccessMode access = 3;
+
+ /*
+ The time when the CommandLock will no longer be valid.
+
+ CommandLocks with the AccessMode `BLOCKED` have no expiration time.
+
+ Time is measured in milliseconds since midnight, January 1, 1970 UTC.
+
+ @optional
+ */
+ optional uint64 expire_time = 4;
+
+ /*
+ The UUID of the Agent that created the CommandLock.
+ */
+ optional ModelUUID agent_uuid = 5;
+}
+
+/*
+ Represents a request for a front-end to perform a control operation.
+
+ CommandRequests may optionally contain a value.
+*/
+message CommandRequest {
+
+ /*
+ Enumeration of the possible value types a CommandRequest could hold.
+ */
+ enum ValType {
+ NONE = 0;
+ INT = 1;
+ DOUBLE = 2;
+ STRING = 4;
+ }
+
+ /*
+ UUID of the Command this request is for.
+
+ @required
+ */
+ optional ModelUUID command_uuid = 1;
+
+ /*
+ Specifies the type of value the CommandRequest contains.
+
+ Defaults to `NONE`.
+
+ @optional
+ */
+ optional ValType type = 3;
+
+ /*
+ Specifies an integer value. If present, `type` must be set to `INT`.
+
+ @optional
+ */
+ optional sint64 int_val = 4;
+
+ /*
+ Specifies a double floating-point value. If present, `type` must be set to `DOUBLE`.
+
+ @optional
+ */
+ optional double double_val = 5;
+
+ /*
+ Specifies a string value. If present, `type` must be set to `STRING`.
+
+ @optional
+ */
+ optional string string_val = 6;
+
+ /*
+ Flag set to prevent events from being logged when command request is issued. Requires special authorization.
+
+ @optional
+ */
+ optional bool unevented = 16;
+}
+
+/*
+ Represents the result of a front-end receiving a CommandRequest.
+*/
+message CommandResult {
+
+ /*
+ Used by the front-end to describe the result of the CommandRequest.
+ */
+ optional CommandStatus status = 1;
+
+ /*
+ If the CommandStatus was not `SUCCESS`, may optionally hold additional information.
+
+ @optional
+ */
+ optional string error_message = 2;
+}
+
+/*
+ Enumeration of Command statuses/results.
+*/
+enum CommandStatus {
+ SUCCESS = 1; // Command was a success.
+ TIMEOUT = 2; // A timeout occurred to a remote device.
+ NO_SELECT = 3; // The remote device reports there was no select before operate. (Separate from CommandLock.)
+ FORMAT_ERROR = 4; // The remote device does not recognize the control or data format.
+ NOT_SUPPORTED = 5; // The operation was not supported.
+ ALREADY_ACTIVE = 6; // An operation is already in progress.
+ HARDWARE_ERROR = 7; // The remote device reports a hardware error.
+ LOCAL = 8; // The remote device is only allowing local control.
+ TOO_MANY_OPS = 9; // The remote device rejects the operation because it has too many already in progress.
+ NOT_AUTHORIZED = 10; // The remote device rejects the authorization of the front-end.
+ UNDEFINED = 11; // The error is unrecognized.
+}
diff --git a/client/src/main/proto/Events.proto b/client/src/main/proto/Events.proto
new file mode 100755
index 0000000..f00ecf0
--- /dev/null
+++ b/client/src/main/proto/Events.proto
@@ -0,0 +1,279 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package greenbus.client;
+
+option java_package = "io.greenbus.client.service.proto";
+option java_outer_classname = "Events";
+
+import "Model.proto";
+
+import "ServiceEnvelopes.proto";
+
+
+/*
+ Represents a key-value pair, where the key is a string and the value is one of
+ multiple data types.
+
+ One of the data types must be specified.
+*/
+message Attribute {
+
+ /*
+ Specifies the name of the attribute.
+
+ @required
+ */
+ optional string name = 1;
+
+ /*
+ May contain the string value of the Attribute.
+
+ @optional
+ */
+ optional string value_string = 10;
+
+ /*
+ May contain the integer value of the Attribute.
+
+ @optional
+ */
+ optional sint64 value_sint64 = 11;
+
+ /*
+ May contain the double floating-point value of the Attribute.
+
+ @optional
+ */
+ optional double value_double = 12;
+
+ /*
+ May contain the boolean value of the Attribute.
+
+ @optional
+ */
+ optional bool value_bool = 13;
+}
+
+/*
+ Represents a record of a notable occurrence in the system at a particular time.
+
+ The message content and severity of an Event, as well as whether the Event also represents an Alarm, are determined
+ through an EventConfig corresponding to the `event_type` field. Users and applications raise events by specifying the
+ type of Event and the interpretation of that type is defined by the system configuration.
+
+ Publishing an Event may also raise an Alarm; if so the Alarm points to the Event to provide contextual information.
+*/
+message Event {
+
+ /*
+ Unique ID for this Event.
+ */
+ optional ModelID id = 1;
+
+ /*
+ Event type, corresponding to a registered type in the event configuration.
+ */
+ optional string event_type = 2;
+
+ /*
+ Specifies whether this Event has a corresponding Alarm.
+ */
+ optional bool alarm = 3 [default = false];
+
+ /*
+ Time the Event was entered into the system.
+
+ Time is measured in milliseconds since midnight, January 1, 1970 UTC.
+ */
+ optional uint64 time = 4;
+
+ /*
+ Time of occurrence for the underlying condition that caused the Event to be published.
+
+ Time is measured in milliseconds since midnight, January 1, 1970 UTC.
+
+ @optional
+ */
+ optional uint64 device_time = 5;
+
+ /*
+ Severity ranking for the Event. `1` is the most severe. Number of severity levels is configurable (default is 1-8).
+ */
+ optional uint32 severity = 6;
+
+ /*
+ Subsystem that authored this event.
+ */
+ optional string subsystem = 7;
+
+ /*
+ Name of the Agent whose credentials were used to publish this Event.
+ */
+ optional string agent_name = 8;
+
+ /*
+ UUID of the object in the system model associated with this Event.
+
+ @optional
+ */
+ optional ModelUUID entity_uuid = 9;
+
+ /*
+ Name of the object in the system model associated with this Event.
+
+ @optional
+ */
+ optional string entity_name = 10;
+
+ /*
+ ID of the segment of the model this Event is associated with.
+
+ @optional
+ */
+ optional string model_group = 12;
+
+ /*
+ The rendered message for the Event.
+ */
+ optional string rendered = 11;
+}
+
+/*
+ Represents a condition that may require operator attention. Alarms are created from particular types of
+ Events, and contain the additional state and workflow for operator management.
+
+ The Event that caused the Alarm to be raised is referenced to provide contextual information.
+*/
+message Alarm {
+
+ /*
+ Enumerates the possible states of an Alarm.
+ */
+ enum State {
+ UNACK_AUDIBLE = 1; // Audible alarm not acknowledged by an operator.
+ UNACK_SILENT = 2; // Silent alarm not acknowledged by an operator.
+ ACKNOWLEDGED = 3; // Alarm acknowledged by operator.
+ REMOVED = 4; // Alarm removed from the active set.
+ }
+
+ /*
+ Unique ID for the Alarm. This is a different ID than the Event ID.
+ */
+ optional ModelID id = 1;
+
+ /*
+ The current state of the Alarm. State may be modified by service requests on behalf of user actions.
+ */
+ optional State state = 2;
+
+ /*
+ The Event associated with this Alarm, provided for context.
+ */
+ optional Event event = 3;
+}
+
+
+/*
+ Represents instructions for generate Events corresponding to a particular type.
+*/
+message EventConfig {
+
+ /*
+ Enumerates what form the prospective Event will take.
+ */
+ enum Designation {
+ ALARM = 1; // Should publish an Event and a corresponding Alarm.
+ EVENT = 2; // Should publish an Event.
+ LOG = 3; // Should not publish an Event and instead write an entry to the system log.
+ }
+
+ /*
+ Event type this EventConfig configures.
+ */
+ optional string event_type = 1;
+
+ /*
+ Severity ranking for the prospective Event. `1` is the most severe. Number of severity levels is
+ configurable (default is 1-8).
+ */
+ optional uint32 severity = 2;
+
+ /*
+ Determines whether Events published with this type will be just Events, Events and Alarms, or will be demoted
+ to log entries.
+ */
+ optional Designation designation = 3;
+
+ /*
+ If `designation` is `ALARM`, specifies the initial state of the raised Alarm.
+
+ Irrelevant if an Alarm is not raised.
+
+ @optional
+ */
+ optional Alarm.State alarm_state = 4;
+
+ /*
+ Specifies the template for Event messages. Parameters are specified by enclosing the name of the key in the Attribute
+ list in braces.
+
+ Example:
+
+
+ Resource: "The {subject} jumped over the {object}."
+ Attributes: ["subject -> "cow", "object" -> "moon"]
+
+
+ If an Attribute is not present, the key name itself will appear in the message.
+ */
+ optional string resource = 5;
+
+ /*
+ Specifies whether this Event type is used by core system services and cannot be deleted.
+ */
+ optional bool built_in = 6;
+}
+
+
+/*
+ Notification of a change to an Event.
+*/
+message EventNotification {
+
+ /*
+ Updated state of the object.
+ */
+ optional Event value = 2;
+}
+
+/*
+ Notification of a change to an Alarm.
+*/
+message AlarmNotification {
+
+ /*
+ Whether object was added, removed, or modified.
+ */
+ optional greenbus.client.envelope.SubscriptionEventType event_type = 1;
+
+ /*
+ Updated state of the object.
+ */
+ optional Alarm value = 2;
+}
\ No newline at end of file
diff --git a/client/src/main/proto/FrontEnd.proto b/client/src/main/proto/FrontEnd.proto
new file mode 100755
index 0000000..8db8028
--- /dev/null
+++ b/client/src/main/proto/FrontEnd.proto
@@ -0,0 +1,105 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package greenbus.client;
+
+option java_package = "io.greenbus.client.service.proto";
+option java_outer_classname = "FrontEnd";
+
+import "ServiceEnvelopes.proto";
+import "Model.proto";
+
+
+
+/*
+ Represents an instance of a front-end having registered to provide services for and Endpoint.
+*/
+message FrontEndRegistration {
+
+ /*
+ The UUID for the Endpoint the front-end is servicing.
+ */
+ optional ModelUUID endpoint_uuid = 1;
+
+ /*
+ The service address the front-end may use for publishing Measurements to a measurement processor.
+ */
+ optional string input_address = 2;
+
+ /*
+ The service address the front-end subscribes to for receiving CommandRequests.
+
+ @optional
+ */
+ optional string command_address = 3;
+}
+
+/*
+ Represents the status of a front-end that is serving an Endpoint. Allows front-ends to report both their own
+ presence and the state of any remote connection.
+*/
+message FrontEndConnectionStatus {
+
+ /*
+ Enumerates the possible statuses of a front-end connection.
+ */
+ enum Status {
+ COMMS_UP = 1; // Nominal status.
+ COMMS_DOWN = 2; // A communications failure.
+ UNKNOWN = 3; // Unknown status.
+ ERROR = 4; // An error status, likely requiring administrator intervention.
+ }
+
+ /*
+ The UUID of the Endpoint served by this front-end.
+ */
+ optional ModelUUID endpoint_uuid = 1;
+
+ /*
+ The name of the Endpoint served by this front-end.
+ */
+ optional string endpoint_name = 2;
+
+ /*
+ The current reported status of the front-end connection.
+ */
+ optional Status state = 3;
+
+ /*
+ The timestamp of the most recent status update.
+
+ Time is measured in milliseconds since midnight, January 1, 1970 UTC.
+ */
+ optional uint64 update_time = 4;
+}
+
+/*
+ Notification of a change to a FrontEndConnectionStatus.
+*/
+message FrontEndConnectionStatusNotification {
+
+ /*
+ Whether object was added, removed, or modified.
+ */
+ optional greenbus.client.envelope.SubscriptionEventType event_type = 1;
+
+ /*
+ Updated state of the object.
+ */
+ optional FrontEndConnectionStatus value = 2;
+}
diff --git a/client/src/main/proto/Measurements.proto b/client/src/main/proto/Measurements.proto
new file mode 100755
index 0000000..2fb60e8
--- /dev/null
+++ b/client/src/main/proto/Measurements.proto
@@ -0,0 +1,360 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package greenbus.client;
+
+option java_package = "io.greenbus.client.service.proto";
+option java_outer_classname = "Measurements";
+
+import "Model.proto";
+
+
+/*
+ Detailed quality flags, based on the notion of quality in IEC 61850.
+*/
+message DetailQual {
+
+ /*
+ Representation overflow, value can't be trusted.
+
+ @optional
+ */
+ optional bool overflow = 1 [default = false];
+
+ /*
+ Occurs when value is outside a predefined range.
+
+ @optional
+ */
+ optional bool out_of_range = 2 [default = false];
+
+ /*
+ Value may not be correct due to a reference being out of calibration.
+
+ @optional
+ */
+ optional bool bad_reference = 3 [default = false];
+
+ /*
+ Value is rapidly changing and some values may have be suppressed.
+
+ @optional
+ */
+ optional bool oscillatory = 4 [default = false];
+
+ /*
+ A supervision function has detected an internal or external failure.
+
+ @optional
+ */
+ optional bool failure = 5 [default = false];
+
+ /*
+ An update was not made during a specific time internal.
+
+ @optional
+ */
+ optional bool old_data = 6 [default = false];
+
+ /*
+ An evaluation function has detected an inconsistency.
+
+ @optional
+ */
+ optional bool inconsistent = 7 [default = false];
+
+ /*
+ Value does not meet the stated accuracy of the source.
+
+ @optional
+ */
+ optional bool inaccurate = 8 [default = false];
+}
+
+
+/*
+ Quality for a Measurement value. Based on IEC 61850.
+*/
+message Quality {
+
+ /*
+ Enumerates possible validities for a Measurement.
+ */
+ enum Validity {
+ GOOD = 0; // No abnormal condition of the acquisition function or the information source is detected.
+ INVALID = 1; // Abnormal condition.
+ QUESTIONABLE = 2; // Supervision function detects abnormal behavior, however value could still be valid. Up to client how to interpret.
+ }
+
+ /*
+ Enumeration to distinguish between values part of the normal system process and those substituted.
+ (e.g. not in service or a measurement override).
+ */
+ enum Source {
+ PROCESS = 0; // Value is provided by an input function from the process I/O or calculated by an application.
+ SUBSTITUTED = 1; // Value is provided by input on an operator or by an automatic source.
+ }
+
+ /*
+ Overall validity of the Measurement value.
+
+ Default is `GOOD`.
+
+ @optional
+ */
+ optional Validity validity = 1 [default = GOOD];
+
+ /*
+ Distinguishes between values resulting from normal system processes and those substituted.
+
+ Default is `PROCESS`.
+
+ @optional.
+ */
+ optional Source source = 2 [default = PROCESS];
+
+ /*
+ Provides extra information when `validity` is `INVALID` or `QUESTIONABLE`.
+
+ @optional
+ */
+ optional DetailQual detail_qual = 3;
+
+ /*
+ Classifies a value as a test value, not to be used for operational purposes.
+
+ Default is `false`.
+
+ @optional
+ */
+ optional bool test = 4 [default = false];
+
+ /*
+ Further update of the value has been blocked by an operator. If set, `DetailQual.old_data` should be set to true.
+
+ Default is `false`.
+
+ @optional
+ */
+ optional bool operator_blocked = 5 [default = false];
+}
+
+
+/*
+ Represents a value at a particular point in time, with annotations for quality and unit of measurement.
+*/
+message Measurement {
+
+ /*
+ Enumeration of possible data types stored in the Measurement.
+ */
+ enum Type {
+ INT = 0; // Corresponds to `int_val` in Measurement.
+ DOUBLE = 1; // Corresponds to `double_val` in Measurement.
+ BOOL = 2; // Corresponds to `bool_val` in Measurement.
+ STRING = 3; // Corresponds to `string_val` in Measurement.
+ NONE = 4; // No data type.
+ }
+
+ /*
+ Determines the data type of the Measurement. The values of the enumeration correspond to the
+ `int_val`, `double_val`, `bool_val` and `string_val` fields. The selected field type must be
+ present.
+
+ More than one of these fields may be present (possibly due to a transformation); if so this type
+ field specifies to primary data type.
+
+ @required
+ */
+ required Type type = 2;
+
+ /*
+ Integer (64-bit) data value.
+
+ @optional
+ */
+ optional sint64 int_val = 3;
+
+ /*
+ Double (64-bit) floating-point data value.
+
+ @optional
+ */
+ optional double double_val = 4;
+
+ /*
+ Boolean data value.
+
+ @optional
+ */
+ optional bool bool_val = 5;
+
+ /*
+ String data value.
+
+ @optional
+ */
+ optional string string_val = 6;
+
+ /*
+ Annotation of health/validity of this value.
+
+ Not specifying a quality is generally equivalent to good quality.
+
+ @optional
+ */
+ optional Quality quality = 7;
+
+ // measurement time of occurrence information, both times are unix style time stamps
+ // "best known time" of occurrence, this should be measured as close to the field as possible. If the protocol
+ // is able to collect or assign a "good field time" to the measurement then is_device_time should be set, this
+ // indicates that the time should be considered to be accurate (but possibly in a slightly different stream from
+ // measurements that are not device_time)
+
+ /*
+ Time of occurrence for the measurement value.
+
+ Time is measured in milliseconds since midnight, January 1, 1970 UTC.
+
+ @required
+ */
+ optional uint64 time = 9 [default = 0];
+
+ /*
+ True if `time` field is a device timestamp, false or unset if it represents wall time.
+
+ @optional
+ */
+ optional bool is_device_time = 10 [default = false];
+
+ /*
+ The time when the measurement was processed into the system. The difference this and the `time` field
+ may be an approximate measurement of delay in the measurement stream or a measure of clock differences between
+ the field devices and the system.
+
+ Time is measured in milliseconds since midnight, January 1, 1970 UTC.
+
+ @optional
+ */
+ optional uint64 system_time = 11 [default = 0];
+}
+
+/*
+ Associates a Measurement value with a Point identified by name.
+*/
+message NamedMeasurementValue {
+ optional string point_name = 1; // Point name.
+ optional Measurement value = 2; // Measurement value.
+}
+
+/*
+ Associates a sequence of Measurement values with a Point identified by name.
+*/
+message NamedMeasurementValues {
+ optional string point_name = 1; // Point name.
+ repeated Measurement value = 2; // Sequence of Measurement values.
+}
+
+/*
+ Associates a Measurement value with a Point identified by UUID.
+*/
+message PointMeasurementValue {
+ optional ModelUUID point_uuid = 1; // Point UUID.
+ optional Measurement value = 2; // Measurement value.
+}
+
+/*
+ Associates a sequence of Measurement values with a Point identified by UUID.
+*/
+message PointMeasurementValues {
+ optional ModelUUID point_uuid = 1; // Point UUID.
+ repeated Measurement value = 2; // Sequence of Measurement values.
+}
+
+/*
+ Batch of Measurements to be published simultaneously. Measurements in the batch are associated with
+ Points identified by UUID or name.
+
+ At least one Measurement must be included.
+*/
+message MeasurementBatch {
+
+ /*
+ Measurements associated with Points by UUID.
+
+ @optional
+ */
+ repeated PointMeasurementValue point_measurements = 1;
+
+ /*
+ Measurements associated with Points by name.
+
+ @optional
+ */
+ repeated NamedMeasurementValue named_measurements = 2;
+
+ /*
+ A collective timestamp for Measurements that are bundled in the batch without individual timestamps. For example, this may be
+ the time that the values were collected from the field.
+
+ If no wall time is specified, the measurement processor will use the time at which it received the batch.
+
+ Time is measured in milliseconds since midnight, January 1, 1970 UTC.
+
+ @optional
+ */
+ optional uint64 wall_time = 3;
+}
+
+/*
+ Notification of a new Measurement value for a particular Point. The UUID and name of the Point are provided.
+*/
+message MeasurementNotification {
+
+ /*
+ Point UUID associated with this Measurement.
+ */
+ optional ModelUUID point_uuid = 1;
+
+ /*
+ Point name associated with this Measurement.
+ */
+ optional string point_name = 2;
+
+ /*
+ Measurement value.
+ */
+ optional Measurement value = 3;
+}
+
+/*
+ Notification of batch of Measurements published for a particular Endpoint. The UUID and name of the Point for each Measurement are provided.
+*/
+message MeasurementBatchNotification {
+
+ /*
+ Endpoint UUID associated with this batch of Measurements.
+ */
+ optional ModelUUID endpoint_uuid = 1;
+
+ /*
+ Set of Measurements published in this batch.
+ */
+ repeated MeasurementNotification values = 3;
+}
+
diff --git a/client/src/main/proto/Model.proto b/client/src/main/proto/Model.proto
new file mode 100755
index 0000000..e205f23
--- /dev/null
+++ b/client/src/main/proto/Model.proto
@@ -0,0 +1,436 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package greenbus.client;
+
+option java_package = "io.greenbus.client.service.proto";
+option java_outer_classname = "Model";
+
+import "ServiceEnvelopes.proto";
+
+/*
+ Represents the universally unique identifier for a modeled service object. UUIDs
+ are intended to be unique across all model objects in the system regardless of type
+ (Point, Endpoint, etc.) and barring the importing of pre-existing UUID values, will
+ be unique across multiple systems as well.
+
+ @simple
+*/
+message ModelUUID {
+ required string value = 1; // Opaque value, should not be interpreted by the client.
+}
+
+/*
+ Used to identify individual records for service objects not part of the model (i.e.
+ Events). Not unique across different types of service objects.
+
+ @simple
+*/
+message ModelID {
+ required string value = 1; // Opaque value, should not be interpreted by the client.
+}
+
+/*
+ Represents a component of the system model.
+
+ Entities fall into two categories: those that are user-defined, and those with more
+ specific built-in representations (i.e. Points, Commands, Endpoints). Built-in Entities
+ are viewable using Entity queries but must be modified through their specific service interfaces.
+ User-defined Entities can be used to provide structure to the otherwise flat list of inputs
+ and outputs (Points and Commands) in the telemetry model.
+
+ The meaning of an Entity can be defined by its name, a list of types that
+ categorize it, or its relationships to other Entities and modeled objects in
+ the system model graph.
+*/
+message Entity {
+
+ /*
+ Unique identifier across all Entities.
+ */
+ optional ModelUUID uuid = 1;
+
+ /*
+ Unique name across all Entities.
+ */
+ optional string name = 2;
+
+ /*
+ Set of types to provide semantic meaning for role of Entity.
+ */
+ repeated string types = 3;
+}
+
+/*
+ Represents a directed edge between Entities. Together, Entities and edges form a graph
+ data structure with the following constraints:
+
+ - There are no cycles (following parent to child edges will never arrive back at a previous parent).
+ - There is only one path between any two nodes (there are no "diamonds").
+
+ Note that unlike simple trees, Entities may have multiple parents, as long as those parents do not
+ have a common ancestor.
+
+ Edges are additionally partitioned by relationship type. Edges of different relationships have no
+ effect on each other and thus are entirely separate graphs/trees, but exist potentially on the same
+ set of nodes (Entities). The name of the relationship is used to provide semantic meaning for
+ these structures.
+
+ To enable querying for children of children without multiple queries, derived edges (`distance > 1`)
+ are stored with immediate edges (`distance = 1`). Service requests only interact (put, delete)
+ immediate edges; derived edges are maintained by the system.
+*/
+message EntityEdge {
+
+ /*
+ ID for this edge, for quick identification.
+ */
+ optional ModelID id = 1;
+
+ /*
+ Parent Entity UUID.
+ */
+ optional ModelUUID parent = 2;
+
+ /*
+ Child Entity UUID.
+ */
+ optional ModelUUID child = 3;
+
+ /*
+ Specific relationship structure this edge is a part of.
+ */
+ optional string relationship = 4;
+
+ /*
+ Entities this edge spans; immediate edges have `distance = 1`.
+ */
+ optional uint32 distance = 5;
+}
+
+/*
+ Enumerates how the value of Measurements associated with a Point should be interpreted.
+*/
+enum PointCategory {
+ ANALOG = 1; // Point is a scalar value.
+ COUNTER = 2; // Point is an integer value representing an accumulation.
+ STATUS = 3; // Point is one of an enumeration of defined value states (e.g. a boolean value).
+}
+
+/*
+ Enumerates whether CommandRequests associated with a Command contain a value, and what data type the value is.
+
+ Commands without values are referred to as _controls_ and Commands with values are referred to as _setpoints_.
+*/
+enum CommandCategory {
+ CONTROL = 1; // CommandRequests contain no values.
+ SETPOINT_INT = 2; // CommandRequests contain an integer value.
+ SETPOINT_DOUBLE = 3; // CommandRequests contain a double floating-point value.
+ SETPOINT_STRING = 4; // CommandRequests contain a string value.
+}
+
+/*
+ Represents a data input for the system.
+*/
+message Point {
+
+ /*
+ Unique identifier across all Entities.
+ */
+ optional ModelUUID uuid = 1;
+
+ /*
+ Unique name across all Entities.
+ */
+ optional string name = 2;
+
+ /*
+ Set of types to provide semantic meaning for role of a Point.
+ */
+ repeated string types = 3;
+
+ /*
+ A high-level category for the data of Measurements associated with this Point
+ */
+ optional PointCategory point_category = 4;
+
+ /*
+ The unit of Measurements associated with this Point.
+ */
+ optional string unit = 5;
+
+ /*
+ Endpoint this Point is associated with.
+
+ @optional
+ */
+ optional ModelUUID endpoint_uuid = 10;
+}
+
+/*
+ Represents a registered control or output for the system.
+*/
+message Command {
+
+ /*
+ Unique identifier across all Entities.
+ */
+ optional ModelUUID uuid = 1;
+
+ /*
+ Unique name across all Entities.
+ */
+ optional string name = 2;
+
+ /*
+ Set of types to provide semantic meaning for role of a Command.
+ */
+ repeated string types = 3;
+
+ /*
+ An explicative name that does not need to be globally unique.
+ */
+ optional string display_name = 5;
+
+ /*
+ Specifies whether CommandRequests associated with a Command contain a value, and what data type the value is.
+ */
+ optional CommandCategory command_category = 7;
+
+ /*
+ Endpoint this Command is associated with.
+
+ @optional
+ */
+ optional ModelUUID endpoint_uuid = 10;
+}
+
+/*
+ Represents a source of Commands and Points. Endpoints divide the input and output of the system into
+ groups by front-end type (`protocol`) and ultimate source.
+*/
+message Endpoint {
+
+ /*
+ Unique identifier across all Entities.
+ */
+ optional ModelUUID uuid = 1;
+
+ /*
+ Unique name across all Entities.
+ */
+ optional string name = 2;
+
+ /*
+ Set of types to provide semantic meaning for role of an Endpoint.
+ */
+ repeated string types = 3;
+
+ /*
+ Identifies the type of front-end (the _protocol_) that is intended to service the Points and Commands
+ associated with this Endpoint.
+ */
+ optional string protocol = 5;
+
+ /*
+ Specifies whether an Endpoint has been disabled, so that Measurements will not be published and Commands
+ will not be processed for it.
+ */
+ optional bool disabled = 6;
+}
+
+/*
+ Holds a value of some specified type. Only one of the value types should be present.
+*/
+message StoredValue {
+
+ /*
+ Boolean value.
+
+ @optional
+ */
+ optional bool bool_value = 5;
+
+ /*
+ 32-bit signed integer value.
+
+ @optional
+ */
+ optional int32 int32_value = 6;
+
+ /*
+ 64-bit signed integer value.
+
+ @optional
+ */
+ optional int64 int64_value = 7;
+
+ /*
+ 32-bit unsigned integer value.
+
+ @optional
+ */
+ optional uint32 uint32_value = 8;
+
+ /*
+ 64-bit unsigned integer value.
+
+ @optional
+ */
+ optional uint64 uint64_value = 9;
+
+ /*
+ 64-bit floating point value.
+
+ @optional
+ */
+ optional double double_value = 10;
+
+ /*
+ String value.
+
+ @optional
+ */
+ optional string string_value = 11;
+
+ /*
+ Byte array value.
+
+ @optional
+ */
+ optional bytes byte_array_value = 12;
+}
+
+/*
+ Represents a key-value pair associated with a model object's UUID. Each combination of UUIDs
+ and keys are unique.
+*/
+message EntityKeyValue {
+
+ /*
+ UUID of the model object the key-value is associated with.
+ */
+ optional ModelUUID uuid = 1;
+
+ /*
+ Key string for the key-value.
+ */
+ optional string key = 2;
+
+ /*
+ Holds value for the key-value, may be one of several data types.
+ */
+ optional StoredValue value = 3;
+}
+
+/*
+ Notification of a change to an EntityKeyValue.
+*/
+message EntityKeyValueNotification {
+
+ /*
+ Whether object was added, removed, or modified.
+ */
+ optional greenbus.client.envelope.SubscriptionEventType event_type = 1;
+
+ /*
+ Updated state of the object.
+ */
+ optional EntityKeyValue value= 3;
+}
+
+
+/*
+ Notification of a change to an Entity.
+*/
+message EntityNotification {
+
+ /*
+ Whether object was added, removed, or modified.
+ */
+ optional greenbus.client.envelope.SubscriptionEventType event_type = 1;
+
+ /*
+ Updated state of the object.
+ */
+ optional Entity value = 2;
+}
+
+/*
+ Notification of a change to an EntityEdge.
+*/
+message EntityEdgeNotification {
+
+ /*
+ Whether object was added, removed, or modified.
+ */
+ optional greenbus.client.envelope.SubscriptionEventType event_type = 1;
+
+ /*
+ Updated state of the object.
+ */
+ optional EntityEdge value = 2;
+}
+
+/*
+ Notification of a change to an Endpoint.
+*/
+message EndpointNotification {
+
+ /*
+ Whether object was added, removed, or modified.
+ */
+ optional greenbus.client.envelope.SubscriptionEventType event_type = 1;
+
+ /*
+ Updated state of the object.
+ */
+ optional Endpoint value = 2;
+}
+
+/*
+ Notification of a change to a Point.
+*/
+message PointNotification {
+
+ /*
+ Whether object was added, removed, or modified.
+ */
+ optional greenbus.client.envelope.SubscriptionEventType event_type = 1;
+
+ /*
+ Updated state of the object.
+ */
+ optional Point value = 2;
+}
+
+/*
+ Notification of a change to a Command.
+*/
+message CommandNotification {
+
+ /*
+ Whether object was added, removed, or modified.
+ */
+ optional greenbus.client.envelope.SubscriptionEventType event_type = 1;
+
+ /*
+ Updated state of the object.
+ */
+ optional Command value = 2;
+}
\ No newline at end of file
diff --git a/client/src/main/proto/Processing.proto b/client/src/main/proto/Processing.proto
new file mode 100755
index 0000000..5ab476b
--- /dev/null
+++ b/client/src/main/proto/Processing.proto
@@ -0,0 +1,461 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package greenbus.client;
+
+option java_package = "io.greenbus.client.service.proto";
+option java_outer_classname = "Processing";
+
+import "ServiceEnvelopes.proto";
+import "Model.proto";
+import "Measurements.proto";
+
+
+/*
+ Provide the ability to block new Measurements from being published, and to optionally
+ replace the latest value provided by a front end with a user-specified override value. Blocked (not in service)
+ Measurements will not be stored in the current value and historian databases, and will not be published to the
+ Measurement notification stream.
+*/
+message MeasOverride {
+
+ /*
+ UUID for the Point to be overridden.
+
+ @required
+ */
+ optional ModelUUID point_uuid = 1;
+
+ /*
+ If provided, will appear as the latest Measurement value for the point.
+
+ @optional
+ */
+ optional Measurement measurement = 2;
+
+ /*
+ If a Measurement is present, specifies that the replacement value should be
+ processed as if it were a communications value.
+
+ Does nothing if a Measurement value is not provided.
+
+ @optional
+ */
+ optional bool test_value = 3;
+}
+
+/*
+ Describes both the current and previous value of a condition necessary to take some action.
+*/
+enum ActivationType {
+ HIGH = 1; // Activated whenever the condition `true`.
+ LOW = 2; // Activated whenever the condition is `false`.
+ RISING = 3; // Activated whenever the condition is `true` _and_ the previous state of the condition was `false`.
+ FALLING = 4; // Activated whenever the condition is `false` _and_ the previous state of the condition was `true`.
+ TRANSITION = 5; // Activated whenever the latest condition value does not match the previous state of the condition.
+}
+
+/*
+ Describes a transformation of the Measurement, or an Event to be generated.
+
+ An Action is only taken when the current value of the Trigger condition and the previous value match the ActivationType.
+
+ One of the action types must be specified.
+*/
+message Action {
+
+ /*
+ Name of the action, for logging purposes.
+
+ @required
+ */
+ optional string action_name = 1;
+
+ /*
+ Describes under what conditions (current and previous) the action is to be taken.
+
+ @required
+ */
+ optional ActivationType type = 2;
+
+ /*
+ Specifies whether the Action is temporarily disabled.
+
+ @optional
+ */
+ optional bool disabled = 3;
+
+ /*
+ Type of action that prevents the Measurement from being stored or published.
+
+ @optional
+ */
+ optional bool suppress = 5;
+
+ /*
+ Type of action that performs a linear transformation on the analog value of the Measurement.
+
+ @optional
+ */
+ optional LinearTransform linear_transform = 10;
+
+ /*
+ Type of action that gives the Measurement the specified Quality.
+
+ @optional
+ */
+ optional Quality quality_annotation = 11;
+
+ /*
+ Type of action that strips the value of the Measurement before it is stored/published.
+
+ @optional
+ */
+ optional bool strip_value = 12;
+
+ /*
+ Type of action that sets the Measurement value to the specified boolean value.
+
+ @optional
+ */
+ optional bool set_bool = 13;
+
+ /*
+ Type of action that publishes an Event related to the Measurement's Point.
+
+ @optional
+ */
+ optional EventGeneration event = 15;
+
+ /*
+ Type of action that transforms a boolean Measurement value to a string value.
+
+ @optional
+ */
+ optional BoolEnumTransform bool_transform = 16;
+
+ /*
+ Type of action that transforms an integer Measurement value to a string value.
+
+ @optional
+ */
+ optional IntEnumTransform int_transform = 17;
+}
+
+/*
+ Describes a linear transformation on an analog value.
+
+ The transformation, where `x` is the original value:
+
+ `trans(x) = scale * x + offset`
+*/
+message LinearTransform {
+
+ /*
+ Scaling factor of the input.
+
+ @required
+ */
+ optional double scale = 2;
+
+ /*
+ Addition operation performed after the scaling.
+
+ @required
+ */
+ optional double offset = 3;
+
+ /*
+ Specifies whether integer analog values should be converted to floating point values in the resulting Measurement.
+
+ Defaults to `false`.
+
+ @optional
+ */
+ optional bool forceToDouble = 4 [default = false];
+}
+
+/*
+ Describes an Event to generate from a Measurement.
+*/
+message EventGeneration {
+
+ /*
+ Specifies the Event type. Corresponds to the types configured in the Event/Alarm subsystem.
+
+ @required
+ */
+ optional string event_type = 1;
+}
+
+/*
+ Describes the transformation of boolean values to string values by specifying strings for both `true` and `false`.
+*/
+message BoolEnumTransform {
+
+ /*
+ String value to attach to the Measurement when its boolean value is `true`.
+
+ @required
+ */
+ optional string true_string = 1;
+
+ /*
+ String value to attach to the Measurement when its boolean value is `false`.
+
+ @required
+ */
+ optional string false_string = 2;
+}
+
+/*
+ Describes the transformation of integer values to string values by specifying a set of mappings between integers and strings.
+
+ If a mapping does not exist for the Measurement's integer value, the Measurement will not be changed.
+*/
+message IntEnumTransform {
+
+ /*
+ Associations between integers and strings.
+
+ @required
+ */
+ repeated IntToStringMapping mappings = 1;
+
+ /*
+ Value to use if no mappings match.
+
+ @optional
+ */
+ optional string default_value = 2;
+}
+
+message IntToStringMapping {
+
+ /*
+ Integer input value.
+
+ @required
+ */
+ optional sint64 value = 1;
+
+ /*
+ Resulting string value.
+
+ @required
+ */
+ optional string string = 2;
+}
+
+/*
+ Description of a condition and a set of Actions.
+
+ For every new Measurement value, the current boolean value of the condition is evaluated. The current value
+ and the previous value are compared to the ActivationType of each Action to determine if the Action should
+ be taken.
+
+ When multiple condition types are specified, the boolean value is the logical _and_ of all conditions.
+*/
+message Trigger {
+
+ /*
+ Name that identifies the Trigger uniquely in the TriggerSet
+
+ @required
+ */
+ optional string trigger_name = 2;
+
+ /*
+ If the activation type is matched no further Triggers in the TriggerSet will be evaluated.
+
+ @optional
+ */
+ optional ActivationType stop_processing_when = 3;
+
+ /*
+ Sequence of Actions to be evaluated.
+
+ @required
+ */
+ repeated Action actions = 9;
+
+ /*
+ Condition describing boundaries for analog values in Measurements.
+
+ @optional
+ */
+ optional AnalogLimit analog_limit = 10;
+
+ /*
+ Condition matching Quality annotations of Measurements. Will be triggered if any fields set in the Trigger
+ Quality match the values of the same field set in the Measurement Quality.
+
+ @optional
+ */
+ optional Quality quality = 12;
+
+ /*
+ Condition triggered when the Measurement value type enumeration matches this value.
+
+ @optional
+ */
+ optional Measurement.Type value_type = 14;
+
+ /*
+ Condition triggered when the Measurement has a boolean value that matches this value.
+
+ @optional
+ */
+ optional bool bool_value = 15;
+
+ /*
+ Condition triggered when the Measurement has a string value that matches this value.
+
+ @optional
+ */
+ optional string string_value = 16;
+
+ /*
+ Condition triggered when the Measurement has an integer value that matches this value.
+
+ @optional
+ */
+ optional sint64 int_value = 17;
+
+ /*
+ Condition triggered when the Measurement value and quality are not the same as the
+ previously published/stored Measurement.
+
+ @optional
+ */
+ optional Filter filter = 18;
+}
+
+/*
+ Associates a set of Triggers with a Point. The sequence of Triggers describe the conditional evaluation of
+ transformations of Measurement value and quality, as well as the generation of Events.
+
+ The sequence of Triggers are evaluated in order. Triggers may have as one of their side effects that no further
+ Triggers will be processed, at which point the rest of the sequence is ignored.
+*/
+message TriggerSet {
+
+ /*
+ Sequence of Triggers to evaluate for Measurements associated with the Point.
+
+ @required
+ */
+ repeated Trigger triggers = 2;
+}
+
+/*
+ Describes boundary conditions for analog values. Can include both upper and lower limits, or just one
+ of either.
+
+ When the condition had been previously triggered (the value was outside of the nominal range), the optional deadband
+ field describes how far the value must return within the nominal range to no longer be triggering the condition. This
+ can be used to prevent a value oscillating near the limit from repeatedly triggering the condition.
+*/
+message AnalogLimit {
+
+ /*
+ Upper limit; the nominal range is defined to be below this value.
+
+ @optional
+ */
+ optional double upper_limit = 1;
+
+ /*
+ Lower limit; the nominal range is defined to be above this value.
+
+ @optional
+ */
+ optional double lower_limit = 2;
+
+ /*
+ Once the condition was triggered, distance the value must return within the nominal range for the condition to
+ no longer be triggered.
+
+ @optional
+ */
+ optional double deadband = 3;
+}
+
+/*
+ Describes a condition that triggers when a new Measurement has the same value and quality as the most recent
+ Measurement that did _not_ trigger the condition.
+
+ Optionally, a deadband can be provided to suppress minor changes in analog values.
+*/
+message Filter {
+
+ /*
+ Enumeration for whether the condition triggers for exact duplicates or contains a deadband.
+ */
+ enum FilterType {
+ DUPLICATES_ONLY = 1; // Recognize exact duplicates.
+ DEADBAND = 2; // Recognize values that have changed within a deadband.
+ }
+
+ /*
+ Specifies whether the condition triggers for exact duplicates or contains a deadband.
+
+ @required
+ */
+ optional FilterType type = 1;
+
+ /*
+ If set, the condition will only be triggered if a Measurement has an analog value where the difference between it and
+ the previously stored value exceeds the deadband.
+
+ @optional
+ */
+ optional double deadband_value = 2;
+}
+
+/*
+ Notification of a change to the MeasOverride of a Point.
+*/
+message OverrideNotification {
+
+ /*
+ Whether object was added, removed, or modified.
+ */
+ optional greenbus.client.envelope.SubscriptionEventType event_type = 1;
+
+ /*
+ Updated state of the object.
+ */
+ optional MeasOverride value = 2;
+}
+
+/*
+ Notification of a change to the TriggerSet of a Point.
+*/
+message TriggerSetNotification {
+
+ /*
+ Whether object was added, removed, or modified.
+ */
+ optional greenbus.client.envelope.SubscriptionEventType event_type = 1;
+
+ /*
+ Updated state of the object.
+ */
+ optional TriggerSet value = 2;
+}
\ No newline at end of file
diff --git a/client/src/main/proto/ServiceEnvelopes.proto b/client/src/main/proto/ServiceEnvelopes.proto
new file mode 100755
index 0000000..a5a9ae0
--- /dev/null
+++ b/client/src/main/proto/ServiceEnvelopes.proto
@@ -0,0 +1,95 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package greenbus.client.envelope;
+
+option java_package = "io.greenbus.client.proto";
+option java_outer_classname = "Envelope";
+
+/*
+ Common classifier for notifications of changes in service objects.
+*/
+enum SubscriptionEventType{
+ ADDED = 1;
+ MODIFIED = 2;
+ REMOVED = 3;
+}
+
+/*
+ Represents the status of service responses. Modeled on HTTP status codes.
+
+ - The 200 range are successful responses.
+ - The 400 range represent request rejections.
+ - The 500 range represent internal errors.
+
+ For more information on the HTTP status codes:
+
+ http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+*/
+enum Status {
+ OK = 200;
+ CREATED = 201;
+ UPDATED = 202;
+ DELETED = 204;
+ NOT_MODIFIED = 205;
+ BAD_REQUEST = 400;
+ UNAUTHORIZED = 401; // No valid user authentication provided.
+ FORBIDDEN = 403; // A valid user has attempted to exceed their privileges.
+ LOCKED = 423;
+ INTERNAL_ERROR = 500;
+ LOCAL_ERROR = 501;
+ UNEXPECTED_RESPONSE = 502;
+ RESPONSE_TIMEOUT = 503;
+ BUS_UNAVAILABLE = 504;
+}
+
+/*
+ Header structure for providing extra information in service requests. Uses a
+ key-value format.
+*/
+message RequestHeader{
+ optional string key = 1;
+ optional string value = 2;
+}
+
+/*
+ Outermost envelope for service requests. Contains request headers and an opaque
+ payload.
+
+ Relies on out-of-band information (the communication channel, a header) to
+ determine the format of the payload.
+*/
+message ServiceRequest{
+ repeated RequestHeader headers = 1;
+ optional bytes payload = 2;
+}
+
+/*
+ Outermost envelope for service responses. Status may be a success or an error.
+ If the response is an error, the error_message field will contain additional
+ information. If the status is a success, the payload field contains the
+ response object(s).
+
+ Relies on out-of-band information (the communication channel, a header) to
+ determine the format of the payload.
+*/
+message ServiceResponse{
+ optional Status status = 1; // Status code, check for 2-- or print error message.
+ optional string error_message = 2; // Error message if applicable.
+ optional bytes payload = 3; // Can be one or many depending on request.
+}
diff --git a/client/src/main/proto/services/AuthRequests.proto b/client/src/main/proto/services/AuthRequests.proto
new file mode 100644
index 0000000..6c6ee36
--- /dev/null
+++ b/client/src/main/proto/services/AuthRequests.proto
@@ -0,0 +1,341 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package greenbus.client;
+
+option java_package = "io.greenbus.client.service.proto";
+option java_outer_classname = "AuthRequests";
+option java_generic_services = false;
+
+import "google/protobuf/descriptor.proto";
+import "CompilerExtensions.proto";
+
+import "Auth.proto";
+import "Model.proto";
+import "services/ModelRequests.proto";
+
+/*
+ Description used to create or modify an Agent.
+
+*/
+message AgentTemplate {
+
+ /*
+ Identifies a pre-existing Agent, or provides a pre-determined UUID for a new Agent.
+
+ @optional
+ */
+ optional ModelUUID uuid = 1;
+
+ /*
+ Identifies a pre-existing Agent, provides a name for a created Agent, or a new name for an existing Agent.
+
+ - If a UUID is not provided and the Agent does not exist an Agent will be created with this name.
+ - If a UUID is not provided and the Agent exists, the Agent will be identified and the non-name fields will be be modified.
+ - If a UUID is provided and the Agent does not exist, an Agent will be created with this name.
+ - If a UUID is provided and the Agent already exists, the Agent will be renamed to this name.
+
+ @required
+ */
+ optional string name = 2;
+
+ /*
+ Password for the Agent.
+
+ - If creating a new Agent, a password is required.
+ - If updating an Agent, a password is not required. Hence updating other fields in the Agent is possible without knowing the original password or changing to a new one.
+
+ @optional
+ */
+ optional string password = 3;
+
+ /*
+ Names of PermissionSets to be assigned to this Agent. The PermissionSets must already exist.
+
+ After modifying an existing Agent, these will be the full set of PermissionSets for that Agent. Any pre-existing PermissionSet assignments not in this list will be removed.
+
+ Any empty list (including if the field is uninitialized) will result in the Agent having no PermissionSets.
+
+ @required
+ */
+ repeated string permission_sets = 4;
+}
+
+/*
+ Description of an update to an Agent's password.
+*/
+message AgentPasswordUpdate {
+
+ /*
+ Identifies the Agent.
+
+ @required
+ */
+ optional ModelUUID uuid = 1;
+
+ /*
+ New password for the Agent.
+
+ @required
+ */
+ optional string password = 2;
+}
+
+/*
+ Collection of keys that identify a set of Agents.
+*/
+message AgentKeySet {
+
+ /*
+ UUIDs of Agents.
+
+ @optional
+ */
+ repeated ModelUUID uuids = 1;
+
+ /*
+ Names of Agents.
+
+ @optional
+ */
+ repeated string names = 2;
+}
+
+/*
+ Query parameters to select a set of Agents.
+
+ Agents can be matched by their types or their permission sets. Including no search parameters will return
+ all Agents in the system.
+
+ Uses paging to handle large result sets.
+*/
+message AgentQuery {
+
+ /*
+ Parameters to match Agents based on the names of their permission sets.
+
+ @optional
+ */
+ repeated string permission_sets = 4;
+
+ /*
+ Parameters to narrow the result sets for queries.
+
+ @optional
+ @nested
+ */
+ optional EntityPagingParams paging_params = 14;
+}
+
+/*
+ Description used to create or modify an PermissionSet.
+
+*/
+message PermissionSetTemplate {
+
+ /*
+ Identifies a pre-existing PermissionSet.
+
+ @optional
+ */
+ optional ModelID id = 1;
+
+ /*
+ Name of a new PermissionSet or name to change a PermissionSet to if an ID is also provided.
+
+ @required
+ */
+ optional string name = 2;
+
+ /*
+ When modifying the PermissionSet, this field will be the Permissions for this PermissionSet.
+ Any pre-existing Permissions not in this list will be removed, and new Permissions will be added.
+
+ @required
+ */
+ repeated Permission permissions = 4;
+}
+
+
+/*
+ Collection of keys that identify a set of PermissionSets.
+*/
+message PermissionSetKeySet {
+
+ /*
+ IDs of PermissionSets.
+
+ @optional
+ */
+ repeated ModelID ids = 1;
+
+ /*
+ Names of PermissionSets.
+
+ @optional
+ */
+ repeated string names = 2;
+}
+
+/*
+ Query parameters to select a set of PermissionSets.
+
+ PermissionSets can be matched by their types. Including no search parameters will return
+ all PermissionSets in the system.
+
+ Uses paging to handle large result sets.
+
+*/
+message PermissionSetQuery {
+
+ /*
+ Last UUID (in the returned sorted order) of a previous result set. This result set will be the
+ page beginning from the next UUID.
+
+ @optional
+ */
+ optional ModelID last_id = 14;
+
+ /*
+ The number of elements to return. Used to limit the size of result sets in large models. If not set,
+ a server-determined page size will be used.
+
+ If the size of the result set equals the page size, there may be more data available.
+
+ @optional
+ */
+ optional uint32 page_size = 15;
+}
+
+
+message GetAgentsRequest {
+ optional AgentKeySet request = 1;
+}
+message GetAgentsResponse {
+ repeated Agent results = 1;
+}
+message AgentQueryRequest {
+ optional AgentQuery request = 1;
+}
+message AgentQueryResponse {
+ repeated Agent results = 1;
+}
+message PutAgentsRequest {
+ repeated AgentTemplate templates = 1;
+}
+message PutAgentsResponse {
+ repeated Agent results = 1;
+}
+message PutAgentPasswordsRequest {
+ repeated AgentPasswordUpdate updates = 1;
+}
+message PutAgentPasswordsResponse {
+ repeated Agent results = 1;
+}
+message DeleteAgentsRequest {
+ repeated ModelUUID agent_uuids = 1;
+}
+message DeleteAgentsResponse {
+ repeated Agent results = 1;
+}
+
+message GetPermissionSetsRequest {
+ optional PermissionSetKeySet request = 1;
+}
+message GetPermissionSetsResponse {
+ repeated PermissionSet results = 1;
+}
+message PermissionSetQueryRequest {
+ optional PermissionSetQuery request = 1;
+}
+message PermissionSetQueryResponse {
+ repeated PermissionSet results = 1;
+}
+message PutPermissionSetsRequest {
+ repeated PermissionSetTemplate templates = 1;
+}
+message PutPermissionSetsResponse {
+ repeated PermissionSet results = 1;
+}
+message DeletePermissionSetsRequest {
+ repeated ModelID permission_set_ids = 1;
+}
+message DeletePermissionSetsResponse {
+ repeated PermissionSet results = 1;
+}
+
+
+
+/*
+ Service for the authorization management system. Enables querying and updating
+ the set of Agents and PermissionSets.
+
+ See the LoginService for login/logout methods.
+*/
+service AuthService {
+ option (scala_package) = "io.greenbus.client.service";
+ option (java_package) = "io.greenbus.japi.client.service";
+
+ /*
+ Returns a set of Agents identified specifically by UUID or name.
+ */
+ rpc get_agents(GetAgentsRequest) returns (GetAgentsResponse);
+
+ /*
+ Returns all Agents or those matching query parameters. Uses paging to
+ handle large result sets.
+ */
+ rpc agent_query(AgentQueryRequest) returns (AgentQueryResponse);
+
+ /*
+ Creates or modifies a set of Agents.
+ */
+ rpc put_agents(PutAgentsRequest) returns (PutAgentsResponse);
+
+ /*
+ Updates the password of a set of Agents.
+ */
+ rpc put_agent_passwords(PutAgentPasswordsRequest) returns (PutAgentPasswordsResponse);
+
+ /*
+ Deletes a set of Agents identified by UUID. Returns the final state of the deleted Agents.
+ */
+ rpc delete_agents(DeleteAgentsRequest) returns (DeleteAgentsResponse);
+
+ /*
+ Returns a set of PermissionSets identified specifically by UUID or name.
+ */
+ rpc get_permission_sets(GetPermissionSetsRequest) returns (GetPermissionSetsResponse);
+
+ /*
+ Returns all PermissionSets or those matching query parameters. Uses paging to
+ handle large result sets.
+ */
+ rpc permission_set_query(PermissionSetQueryRequest) returns (PermissionSetQueryResponse);
+
+ /*
+ Creates or modifies a set of PermissionSets.
+ */
+ rpc put_permission_sets(PutPermissionSetsRequest) returns (PutPermissionSetsResponse);
+
+ /*
+ Deletes a set of PermissionSets identified by UUID. Returns the final state of the deleted PermissionSets.
+ */
+ rpc delete_permission_sets(DeletePermissionSetsRequest) returns (DeletePermissionSetsResponse);
+
+}
diff --git a/client/src/main/proto/services/CommandRequests.proto b/client/src/main/proto/services/CommandRequests.proto
new file mode 100644
index 0000000..bb5a2ca
--- /dev/null
+++ b/client/src/main/proto/services/CommandRequests.proto
@@ -0,0 +1,205 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package greenbus.client;
+
+option java_package = "io.greenbus.client.service.proto";
+option java_outer_classname = "CommandRequests";
+option java_generic_services = false;
+
+import "google/protobuf/descriptor.proto";
+import "CompilerExtensions.proto";
+
+import "Commands.proto";
+import "Model.proto";
+import "ServiceEnvelopes.proto";
+import "services/ModelRequests.proto";
+
+/*
+ Represents a request to acquire a CommandLock in order to issue CommandRequests for a
+ set of Commands.
+*/
+message CommandSelect {
+
+ /*
+ Specifies the Commands the CommandSelect will affect, by UUID.
+
+ Must include one or more Commands.
+
+ @required
+ */
+ repeated ModelUUID command_uuids = 1;
+
+ /*
+ Duration in milliseconds the CommandLock should last before expiring.
+
+ If not set, a server-determined default will be used.
+
+ @optional
+ */
+ optional uint64 expire_duration = 2;
+}
+
+/*
+ Represents a request to acquire a CommandLock in order to block CommandRequests from being
+ issued and CommandSelects succeeding for a set of Commands.
+*/
+message CommandBlock {
+
+ /*
+ Specifies the Commands the CommandBlock will affect, by UUID.
+
+ Must include one or more Commands.
+
+ @required
+ */
+ repeated ModelUUID command_uuids = 1;
+}
+
+/*
+ Query parameters to select a set of CommandLocks.
+
+ CommandLocks can be matched by the Commands they refer to, their AccessMode, or the Agents that acquired them.
+ Including no search parameters will return all CommandLocks in the system.
+
+ Uses paging to handle large result sets.
+*/
+message CommandLockQuery {
+
+ /*
+ Parameters to match CommandLocks by the UUIDs of the Commands they pertain to. All CommandLocks that
+ refer to at least one of the specified Commands will be returned.
+
+ @optional
+ */
+ repeated ModelUUID command_uuids = 1;
+
+ /*
+ Parameters to filter CommandLocks to those matching a particular AccessMode.
+
+ @optional
+ */
+ optional CommandLock.AccessMode access = 2;
+
+
+ /*
+ Parameters to match CommandLocks by the UUIDs of the Agents that acquired them.
+
+ @optional
+ */
+ repeated ModelUUID agent_uuids = 4;
+
+ /*
+ Last ID (in the returned sorted order) of a previous result set. This result set will be the
+ page beginning from the next ID.
+
+ @optional
+ */
+ optional ModelID last_id = 14;
+
+ /*
+ The number of elements to return. Used to limit the size of result sets in large models. If not set,
+ a server-determined page size will be used.
+
+ If the size of the result set equals the page size, there may be more data available.
+
+ @optional
+ */
+ optional uint32 page_size = 15;
+}
+
+message PostCommandRequestRequest {
+ optional CommandRequest request = 1;
+}
+message PostCommandRequestResponse {
+ optional CommandResult result = 1;
+}
+
+message GetCommandLockRequest {
+ repeated ModelID lock_ids = 1;
+}
+message GetCommandLockResponse {
+ repeated CommandLock results = 1;
+}
+message CommandLockQueryRequest {
+ optional CommandLockQuery request = 1;
+}
+message CommandLockQueryResponse {
+ repeated CommandLock results = 1;
+}
+message PostCommandSelectRequest {
+ optional CommandSelect request = 1;
+}
+message PostCommandSelectResponse {
+ optional CommandLock result = 1;
+}
+message PostCommandBlockRequest {
+ optional CommandBlock request = 1;
+}
+message PostCommandBlockResponse {
+ optional CommandLock result = 1;
+}
+
+message DeleteCommandLockRequest {
+ repeated ModelID lock_ids = 1;
+}
+message DeleteCommandLockResponse {
+ repeated CommandLock results = 1;
+}
+
+/*
+ Service for issuing CommandRequests and acquiring and managing CommandLocks.
+*/
+service CommandService {
+ option (scala_package) = "io.greenbus.client.service";
+ option (java_package) = "io.greenbus.japi.client.service";
+
+ /*
+ Issues a CommandRequest.
+ */
+ rpc issue_command_request(PostCommandRequestRequest) returns (PostCommandRequestResponse) {
+ option (addressed) = OPTIONALLY;
+ }
+
+ /*
+ Returns a set of CommandLocks specified by ID.
+ */
+ rpc get_command_locks(GetCommandLockRequest) returns (GetCommandLockResponse);
+
+ /*
+ Returns all CommandLocks or those matching query parameters. Uses paging to
+ handle large result sets.
+ */
+ rpc command_lock_query(CommandLockQueryRequest) returns (CommandLockQueryResponse);
+
+ /*
+ Selects a set of Commands for exclusive access in order to issue CommandRequests.
+ */
+ rpc select_commands(PostCommandSelectRequest) returns (PostCommandSelectResponse);
+
+ /*
+ Blocks a set of Commands to prevent CommandRequests from being issued and CommandSelects from being
+ acquired by other Agents.
+ */
+ rpc block_commands(PostCommandBlockRequest) returns (PostCommandBlockResponse);
+
+ /*
+ Deletes a set of CommandLocks specified by ID.
+ */
+ rpc delete_command_locks(DeleteCommandLockRequest) returns (DeleteCommandLockResponse);
+}
diff --git a/client/src/main/proto/services/EventRequests.proto b/client/src/main/proto/services/EventRequests.proto
new file mode 100644
index 0000000..6424577
--- /dev/null
+++ b/client/src/main/proto/services/EventRequests.proto
@@ -0,0 +1,680 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package greenbus.client;
+
+option java_package = "io.greenbus.client.service.proto";
+option java_outer_classname = "EventRequests";
+option java_generic_services = false;
+
+import "google/protobuf/descriptor.proto";
+import "CompilerExtensions.proto";
+
+import "Events.proto";
+import "Model.proto";
+import "services/ModelRequests.proto";
+
+/*
+ Query parameters to select a set of Events.
+*/
+message EventQueryParams {
+
+ /*
+ Specifies a set of Event types to be directly matched. Events with one of any of the included Event types will
+ be returned.
+
+ @optional
+ */
+ repeated string event_type = 1;
+
+ /*
+ Searches for Events with a time greater than or equal to this value.
+
+ Time is measured in milliseconds since midnight, January 1, 1970 UTC.
+
+ @optional
+ */
+ optional uint64 time_from = 2;
+
+ /*
+ Searches for Events with a time less than or equal to this value.
+
+ Time is measured in milliseconds since midnight, January 1, 1970 UTC.
+
+ @optional
+ */
+ optional uint64 time_to = 3;
+
+ /*
+ Specifies a set of Event severities to be directly matched. Events with one of any of the included Event severities will
+ be returned.
+
+ @optional
+ */
+ repeated uint32 severity = 4;
+
+ /*
+ Specifies a lower bound on severities. Any Event at this severity or higher (numerically lower) will
+ be returned.
+
+ If present, overrides the `severity` parameter.
+
+ @optional
+ */
+ optional uint32 severity_or_higher = 5;
+
+ /*
+ Specifies a set of Event subystem fields to be directly matched. Events with one of any of the included subystems will
+ be returned.
+
+ @optional
+ */
+ repeated string subsystem = 6;
+
+ /*
+ Specifies a set of Event agent names to be directly matched. Events with one of any of the included agent names will
+ be returned.
+
+ @optional
+ */
+ repeated string agent = 7;
+
+ /*
+ Specifies a set of Entities by keys. Events associated with any of the specified Entities will be returned.
+
+ @nested
+ @optional
+ */
+ optional EntityKeySet entities = 8;
+
+ /*
+ Specifies a set of model segments. Events associated with any of the specified model segments will be returned.
+
+ @optional
+ */
+ repeated string model_group = 10;
+
+ /*
+ If `true`, only returns Events that caused an Alarm to be raised. If `false`, only returns Events that did not
+ raise an Alarm.
+
+ @optional
+ */
+ optional bool is_alarm = 9;
+
+
+ /*
+ Specifies which side of a page window to return Events for.
+
+ If the number of Events in the time window exceeds the limit on the number of Events to be
+ returned by the query, the sequence of Events will be justified to either the lower or higher
+ boundary of the window.
+
+ - If `false`, the returned Events will begin with the first in the time window and proceed until the time window
+ ends or the limit is met. Paging using this method would move forward in time.
+ - If `true`, the sequence of returned Events begins at the `limit`th value below the high boundary of the time window,
+ and proceeds to the high boundary. Paging using this method would move backward in time.
+
+ In both cases, the sequence of Events is returned in ascending time-order (oldest to newest).
+
+ The default value is `true`.
+
+ @optional
+ */
+ optional bool latest = 16;
+
+}
+
+/*
+ Query for Events.
+
+ Including no search parameters will return all Events in the system.
+
+ Paging by ID is used to handle large result sets. Results will be returned sorted first by the requested time
+ ordering and then by ID to distinguish between Events that are recorded in the same millisecond.
+
+ - When paging "forward" (`latest = false`), the `last_id` field should be set to the last value in the result order.
+ - When paging "backward" (`latest = true`), the `last_id` field should be set to the first value in the result order.
+*/
+message EventQuery {
+
+ /*
+ The query parameters to find a set of Events.
+
+ @optional
+ @nested
+ */
+ optional EventQueryParams query_params = 1;
+
+ /*
+ Last ID (in the returned sorted order) of a previous result set. This result set will be the
+ page beginning from the next ID.
+
+ @optional
+ */
+ optional ModelID last_id = 25;
+
+ /*
+ The number of elements to return. Used to limit the size of result sets in large models. If not set,
+ a server-determined limit will be used.
+
+ If the size of the result set equals the limit, there may be more data available.
+
+ @optional
+ */
+ optional uint32 page_size = 26;
+}
+
+/*
+ Subscription parameters for Events.
+
+ Including no subscription parameters will subscribe to all future Event notifications.
+
+ Subscribes and returns an initial result set. The initial result set is governed by the `time_from` and `limit`
+ fields, which put a bound on the number of the _latest_ Events to return that also match the subscription parameters.
+ Not specifying these parameters will use a default `limit`; setting `limit = 0` will result in only subscribing to
+ future Events.
+*/
+message EventSubscriptionQuery {
+
+ /*
+ Specifies a set of Event types to be directly matched. Events with one of any of the included Event types will
+ be returned and notifications for Events with these types will be delivered.
+
+ @optional
+ */
+ repeated string event_type = 1;
+
+ /*
+ Specifies a set of Event severities to be directly matched. Events with one of any of the included Event severities will
+ be returned and notifications for Events with these severities will be delivered.
+
+ @optional
+ */
+ repeated uint32 severity = 2;
+
+ /*
+ Specifies a set of subsystems to be directly matched. Events with one of any of the included subsystems will
+ be returned and notifications for Events with these subsystems will be delivered.
+
+ @optional
+ */
+ repeated string subsystem = 3;
+
+ /*
+ Specifies a set of agent names to be directly matched. Events with one of any of the included agent names will
+ be returned and notifications for Events with these agent names will be delivered.
+
+ @optional
+ */
+ repeated string agent = 4;
+
+ /*
+ Specifies a set of Event types to be directly matched. Events with one of any of the included Event types will
+ be returned and notifications for Events with these types will be delivered.
+
+ __Note__: Event notifications are filtered by Entity UUID, not name. Using a name parameter resolves
+ the UUID of the Entity associated with that name at the time of the request, and is
+ equivalent to the client resolving the UUID before making the request. As a result, if the
+ Entity a particular name refers to changes (due to a deletion/addition or two renames) Event notifications will be delivered
+ for only for Events pertaining to the original Entity.
+
+ @nested
+ @optional
+ */
+ optional EntityKeySet entities = 5;
+
+ /*
+ If, `true`, only returns and subscribes to notifications for Events that raised an Alarm. If `false`, only returns
+ and subscribes to notifications for Events that did not raise an Alarm.
+
+ @optional
+ */
+ optional bool is_alarm = 6;
+
+ /*
+ Searches for Events with a time greater than or equal to this value.
+
+ Only relevant to the initial result set. Does not affect the subscription itself.
+
+ Time is measured in milliseconds since midnight, January 1, 1970 UTC.
+
+ @optional
+ */
+ optional uint64 time_from = 14;
+
+ /*
+ The number of elements to return. If not set, a server-determined limit will be used.
+
+ Only relevant to the initial result set. Does not affect the subscription itself.
+
+ @optional
+ */
+ optional uint32 limit = 15;
+}
+
+/*
+ Description used to create an Event.
+*/
+message EventTemplate {
+
+ /*
+ Event type, corresponding to a registered type in the event configuration.
+
+ @required
+ */
+ optional string event_type = 1;
+
+ /*
+ Subsystem that authored this event.
+
+ @optional
+ */
+ optional string subsystem = 2;
+
+ /*
+ Time of occurrence for the underlying condition that caused the Event to be published.
+
+ Time is measured in milliseconds since midnight, January 1, 1970 UTC.
+
+ @optional
+ */
+ optional uint64 device_time = 5;
+
+ /*
+ UUID of the object in the system model associated with this Event.
+
+ @optional
+ */
+ optional ModelUUID entity_uuid = 9;
+
+ /*
+ ID of the segment of the model this Event is associated with.
+
+ @optional
+ */
+ optional string model_group = 11;
+
+ /*
+ Specifies key-value pairs that correspond to parameters in the resource string
+ for the EventConfig associated with `event_type`.
+
+ @optional
+ */
+ repeated Attribute args = 10;
+}
+
+/*
+ Query parameters to select a set of Alarms. Relies on the EventQuery to search by the Events associated
+ with Alarms, adding Alarm-specific query parameters.
+
+ Including no search parameters will return all Alarms in the system.
+
+ Paging by ID is used to handle large result sets. Results will be returned sorted first by the requested time
+ ordering and then by ID to distinguish between Events that are recorded in the same millisecond.
+
+ - When paging "forward" (`latest = false`), the `last_id` field should be set to the last value in the result order.
+ - When paging "backward" (`latest = true`), the `last_id` field should be set to the first value in the result order.
+*/
+message AlarmQuery {
+
+ /*
+ Parameters describing the Event associated with the Alarm.
+
+ @nested
+ @optional
+ */
+ optional EventQueryParams event_query_params = 1;
+
+ /*
+ Specifies a set of Alarm states to be directly matched. Alarms with one of any of the included Alarm states will
+ be returned.
+ */
+ repeated Alarm.State alarm_states = 2;
+
+ /*
+ Last ID (in the returned sorted order) of a previous result set. This result set will be the
+ page beginning from the next ID.
+
+ The ID used should be for the Alarm, not an associated Event.
+
+ @optional
+ */
+ optional ModelID last_id = 25;
+
+ /*
+ The number of elements to return. Used to limit the size of result sets in large models. If not set,
+ a server-determined limit will be used.
+
+ If the size of the result set equals the limit, there may be more data available.
+
+ @optional
+ */
+ optional uint32 page_size = 26;
+}
+
+/*
+ Subscription parameters for Alarms. Relies on the EventSubscriptionQuery to define the Events associated
+ with Alarms, adding Alarm-specific subscription parameters.
+
+ Including no subscription parameters will subscribe to all future Event notifications.
+*/
+message AlarmSubscriptionQuery {
+
+ /*
+ Parameters describing the Event associated with the Alarm.
+
+ @nested
+ @optional
+ */
+ optional EventSubscriptionQuery event_query = 1;
+
+ /*
+
+ Specifies a set of Alarm states to be directly matched. Alarms with one of any of the included Alarm states will
+ be returned and notifications for Alarms with these states will be delivered.
+
+ @optional
+ */
+ repeated Alarm.State alarm_states = 2;
+}
+
+/*
+ Represents a modification to the state of an existing Alarm.
+*/
+message AlarmStateUpdate {
+
+ /*
+ ID of the Alarm to be updated.
+
+ @required
+ */
+ optional ModelID alarm_id = 1;
+
+ /*
+ New state the Alarm should be in.
+
+ @required
+ */
+ optional Alarm.State alarm_state = 2;
+}
+
+/*
+ Description used to create or modify an EventConfig.
+*/
+message EventConfigTemplate {
+
+ /*
+ Event type this EventConfig configures.
+
+ @required
+ */
+ optional string event_type = 1;
+
+ /*
+ Severity ranking for the prospective Event. `1` is the most severe. Number of severity levels is
+ configurable (default is 1-8).
+
+ @required
+ */
+ optional uint32 severity = 2;
+
+ /*
+ Determines whether Events published with this type will be just Events, Events and Alarms, or will be demoted
+ to log entries.
+
+ @required
+ */
+ optional EventConfig.Designation designation = 3;
+
+ /*
+ If `designation` is `ALARM`, specifies the initial state of the raised Alarm.
+
+ Defaults to `UNACK_SILENT`. Irrelevant if an Alarm is not raised.
+
+ @optional
+ */
+ optional Alarm.State alarm_state = 4;
+
+ /*
+ Specifies the template for Event messages. Parameters are specified by enclosing the index into the Attribute
+ list in braces.
+
+ Example:
+
+
+ Resource: "The {subject} jumped over the {object}."
+ Attributes: ["subject -> "cow", "object" -> "moon"]
+
+
+ If an Attribute is not present, the key name itself will appear in the message.
+
+ @required
+ */
+ optional string resource = 5;
+}
+
+/*
+ Query parameters to select a set of EventConfigs. EventConfigs can be filtered by severity, designation,
+ or the initial state of Alarms.
+
+ Including no search parameters will return all EventConfigs in the system.
+
+ Paging by the `event_type` field is used to handle large result sets.
+*/
+message EventConfigQuery {
+
+ /*
+ Specifies a set of severities to be directly matched. EventConfigs with one of any of the included severities will
+ be returned.
+
+ @optional
+ */
+ repeated uint32 severity = 1;
+
+ /*
+ Specifies a lower bound on severities. Any EventConfig at this severity or higher (numerically lower) will
+ be returned.
+
+ If present, overrides the `severity` parameter.
+
+ @optional
+ */
+ optional uint32 severity_or_higher = 2;
+
+ /*
+ Specifies a set of designations to be directly matched. EventConfigs with one of any of the included designations
+ will be returned.
+
+ @optional
+ */
+ repeated EventConfig.Designation designation = 3;
+
+ /*
+ Specifies a set of Alarm states to be directly matched. EventConfigs with one of any of the included Alarm states
+ will be returned.
+
+ @optional
+ */
+ repeated Alarm.State alarm_state = 4;
+
+ /*
+ Last Event type (in the returned sorted order) of a previous result set. This result set will be the
+ page beginning from the next Event type.
+
+ @optional
+ */
+ optional string last_event_type = 14;
+
+ /*
+ The number of elements to return. Used to limit the size of result sets in large models. If not set,
+ a server-determined page size will be used.
+
+ If the size of the result set equals the page size, there may be more data available.
+
+ @optional
+ */
+ optional uint32 page_size = 15;
+}
+
+
+message GetEventsRequest {
+ repeated ModelID event_id = 1;
+}
+message GetEventsResponse {
+ repeated Event results = 1;
+}
+message EventQueryRequest {
+ optional EventQuery request = 1;
+}
+message EventQueryResponse {
+ repeated Event results = 1;
+}
+message PostEventsRequest {
+ repeated EventTemplate request = 1;
+}
+message PostEventsResponse {
+ repeated Event results = 1;
+}
+message SubscribeEventsRequest {
+ optional EventSubscriptionQuery request = 1;
+}
+message SubscribeEventsResponse {
+ repeated Event results = 1;
+}
+
+message AlarmQueryRequest {
+ optional AlarmQuery request = 1;
+}
+message AlarmQueryResponse {
+ repeated Alarm results = 1;
+}
+message PutAlarmStateRequest {
+ repeated AlarmStateUpdate request = 1;
+}
+message PutAlarmStateResponse {
+ repeated Alarm results = 1;
+}
+message SubscribeAlarmsRequest {
+ optional AlarmSubscriptionQuery request = 1;
+}
+message SubscribeAlarmsResponse {
+ repeated Alarm results = 1;
+}
+
+
+message EventConfigQueryRequest {
+ optional EventConfigQuery request = 1;
+}
+message EventConfigQueryResponse {
+ repeated EventConfig results = 1;
+}
+message GetEventConfigRequest {
+ repeated string event_type = 1;
+}
+message GetEventConfigResponse {
+ repeated EventConfig results = 1;
+}
+message PutEventConfigRequest {
+ repeated EventConfigTemplate request = 1;
+}
+message PutEventConfigResponse {
+ repeated EventConfig results = 1;
+}
+message DeleteEventConfigRequest {
+ repeated string event_type = 1;
+}
+message DeleteEventConfigResponse {
+ repeated EventConfig results = 1;
+}
+
+
+service EventService {
+ option (scala_package) = "io.greenbus.client.service";
+ option (java_package) = "io.greenbus.japi.client.service";
+
+ /*
+ Returns Events identified by ID.
+ */
+ rpc get_events(GetEventsRequest) returns (GetEventsResponse);
+
+ /*
+ Returns all Events or those matching query parameters.
+ */
+ rpc event_query(EventQueryRequest) returns (EventQueryResponse);
+
+ /*
+ Requests Events be created.
+ */
+ rpc post_events(PostEventsRequest) returns (PostEventsResponse);
+
+ /*
+ Subscribes to Event notifications, and can return the latest Events.
+ */
+ rpc subscribe_to_events(SubscribeEventsRequest) returns (SubscribeEventsResponse) {
+ option (subscription_type) = "greenbus.client.EventNotification";
+ }
+
+ /*
+ Returns all Alarms or those matching query parameters.
+ */
+ rpc alarm_query(AlarmQueryRequest) returns (AlarmQueryResponse);
+
+ /*
+ Updates the state of a set of Alarms.
+
+ The valid Alarm state transitions are:
+
+ - `UNACK_AUDIBLE` -> ( `UNACK_SILENT`, `ACKNOWLEDGED` )
+ - `UNACK_SILENT` -> ( `ACKNOWLEDGED` )
+ - `ACKNOWLEDGED` -> ( `REMOVED` )
+
+ Additionally, an update that does not change the state of the Alarm is always legal.
+
+ */
+ rpc put_alarm_state(PutAlarmStateRequest) returns (PutAlarmStateResponse);
+
+ /*
+ Subscribes to Alarm notifications, and can return the latest Alarm.
+ */
+ rpc subscribe_to_alarms(SubscribeAlarmsRequest) returns (SubscribeAlarmsResponse) {
+ option (subscription_type) = "greenbus.client.AlarmNotification";
+ }
+
+ /*
+ Returns all EventConfigs or those matching query parameters.
+ */
+ rpc event_config_query(EventConfigQueryRequest) returns (EventConfigQueryResponse);
+
+ /*
+ Returns EventConfigs identified by Event type.
+ */
+ rpc get_event_configs(GetEventConfigRequest) returns (GetEventConfigResponse);
+
+ /*
+ Creates or modifies a set of EventConfigs.
+ */
+ rpc put_event_configs(PutEventConfigRequest) returns (PutEventConfigResponse);
+
+ /*
+ Deletes a set of EventConfigs identified by Event type. Returns the final state of the deleted EventConfigs.
+
+ Will fail if specified EventConfigs are built-in.
+ */
+ rpc delete_event_configs(DeleteEventConfigRequest) returns (DeleteEventConfigResponse);
+}
diff --git a/client/src/main/proto/services/FrontEndRequests.proto b/client/src/main/proto/services/FrontEndRequests.proto
new file mode 100644
index 0000000..0fa68db
--- /dev/null
+++ b/client/src/main/proto/services/FrontEndRequests.proto
@@ -0,0 +1,156 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package greenbus.client;
+
+option java_package = "io.greenbus.client.service.proto";
+option java_outer_classname = "FrontEndRequests";
+option java_generic_services = false;
+
+import "google/protobuf/descriptor.proto";
+import "CompilerExtensions.proto";
+
+import "FrontEnd.proto";
+import "Model.proto";
+import "services/ModelRequests.proto";
+
+
+/*
+ Represents a front-end's registration to provide service for an Endpoint.
+
+ An address may be provided where CommandRequests for this Endpoint should be sent. If no address is included, the
+ Endpoint will not support CommandRequests.
+*/
+message FrontEndRegistrationTemplate {
+
+ /*
+ UUID of the Endpoint the front-end is registering for.
+
+ @required
+ */
+ optional ModelUUID endpoint_uuid = 1;
+
+ /*
+ Service address that should be used to send CommandRequests to this front-end for this Endpoint.
+
+ @optional
+ */
+ optional string command_address = 2;
+
+ /*
+ Allows the front-end instance to associate the registration with a unique persistent identity. If the front-end loses
+ the registration information (perhaps due to a restart) re-registration with the same node ID may be
+ accepted even if the registration has not timed out.
+
+ Node IDs should be unique for all front-ends and only re-used by the same logical process on restarts.
+
+ @optional
+ */
+ optional string fep_node_id = 5;
+
+ /*
+ Used by the front-end to indicate that it is currently excluding other front-ends from make a connection for this Endpoint
+ (perhaps by holding open a connection that will only accept a single client). Registrations using this flag
+ take precedence and will abort the registrations of other front-ends (which presumably are unable to connect).
+
+ This is intended to allow front-end instances that were separated from the network for long enough for a fail-over to
+ occur to reclaim their registration instead of releasing an expensive external connection.
+
+ @optional
+ */
+ optional bool holding_lock = 6;
+}
+
+/*
+ Used by a front-end to notify the system of updates to the status of itself or the external source it communicates with.
+*/
+message FrontEndStatusUpdate {
+
+ /*
+ UUID of the Endpoint the front-end is registering for.
+
+ @required
+ */
+ optional ModelUUID endpoint_uuid = 1;
+
+ /*
+ UUID of the Endpoint the front-end is registering for.
+
+ @required
+ */
+ optional FrontEndConnectionStatus.Status status = 2;
+}
+
+
+message PutFrontEndRegistrationRequest {
+ optional FrontEndRegistrationTemplate request = 1;
+}
+message PutFrontEndRegistrationResponse {
+ optional FrontEndRegistration results = 1;
+}
+
+message GetFrontEndConnectionStatusRequest {
+ optional EntityKeySet endpoints = 1;
+}
+message GetFrontEndConnectionStatusResponse {
+ repeated FrontEndConnectionStatus results = 1;
+}
+message SubscribeFrontEndConnectionStatusRequest {
+ optional EndpointSubscriptionQuery endpoints = 1;
+}
+message SubscribeFrontEndConnectionStatusResponse {
+ repeated FrontEndConnectionStatus results = 1;
+}
+message PutFrontEndConnectionStatusRequest {
+ repeated FrontEndStatusUpdate updates = 1;
+}
+message PutFrontEndConnectionStatusResponse {
+ repeated FrontEndConnectionStatus results = 1;
+}
+
+service FrontEndService {
+ option (scala_package) = "io.greenbus.client.service";
+ option (java_package) = "io.greenbus.japi.client.service";
+
+ /*
+ Registers a set of front-ends to provide services for a set of Endpoints.
+ */
+ rpc put_front_end_registration(PutFrontEndRegistrationRequest) returns (PutFrontEndRegistrationResponse) {
+ option (addressed) = OPTIONALLY;
+ }
+
+ /*
+ Gets front-end connection statuses by Endpoint UUIDs and names.
+ */
+ rpc get_front_end_connection_statuses(GetFrontEndConnectionStatusRequest) returns (GetFrontEndConnectionStatusResponse);
+
+ /*
+ Notifies the system of updates to the status of front-end services for a set of Endpoints.
+ */
+ rpc put_front_end_connection_statuses(PutFrontEndConnectionStatusRequest) returns (PutFrontEndConnectionStatusResponse) {
+ option (addressed) = OPTIONALLY;
+ }
+
+ /*
+ Subscribes to notifications for changes to the statuses of front-end connections for a particular set of Endpoints.
+ */
+ rpc subscribe_to_front_end_connection_statuses(SubscribeFrontEndConnectionStatusRequest) returns (SubscribeFrontEndConnectionStatusResponse) {
+ option (subscription_type) = "greenbus.client.FrontEndConnectionStatusNotification";
+ }
+
+}
diff --git a/client/src/main/proto/services/LoginRequests.proto b/client/src/main/proto/services/LoginRequests.proto
new file mode 100644
index 0000000..c99f822
--- /dev/null
+++ b/client/src/main/proto/services/LoginRequests.proto
@@ -0,0 +1,158 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package greenbus.client;
+
+option java_package = "io.greenbus.client.service.proto";
+option java_outer_classname = "LoginRequests";
+option java_generic_services = false;
+
+import "google/protobuf/descriptor.proto";
+import "CompilerExtensions.proto";
+
+import "Auth.proto";
+
+/*
+ Request for an auth token, specifying name and password, expiration time, and contextual information.
+*/
+message LoginRequest {
+
+ /*
+ Name associated with credentials. Will be the name of an Agent modeled in the system.
+
+ @required
+ */
+ optional string name = 2;
+
+ /*
+ Password associated with credentials.
+
+ @required
+ */
+ optional string password = 3;
+
+ /*
+ Provides contextual information about the user or process obtaining the auth token.
+
+ @optional
+ */
+ optional string login_location = 4;
+
+ /*
+ Time for the resulting auth token. If not specified, a default value will be provided.
+
+ Time is measured in milliseconds since midnight, January 1, 1970 UTC.
+
+ @optional
+ */
+ optional uint64 expiration_time = 5;
+
+ /*
+ Provides contextual information about the client software version.
+
+ @optional
+ */
+ optional string client_version = 6;
+}
+
+/*
+ Provides an auth token, expiration time, and contextual information as a result of a successful login.
+*/
+message LoginResponse {
+
+ /*
+ Auth token resulting from a successful login. Can be included in subsequent service requests in order to
+ authorize them.
+ */
+ optional string token = 2;
+
+ /*
+ Expiration time for this auth token. After this time the auth token will be rejected and a new token
+ will need to be acquired.
+
+ Time is measured in milliseconds since midnight, January 1, 1970 UTC.
+ */
+ optional uint64 expiration_time = 3;
+
+ /*
+ Contextual information about the server software version.
+ */
+ optional string server_version = 4;
+}
+
+/*
+ Request to invalidate and remove an auth token no longer being used.
+*/
+message LogoutRequest {
+
+ /*
+ Auth token to be invalidated.
+
+ @required
+ */
+ optional string token = 2;
+}
+
+
+
+message PostLoginRequest {
+ optional LoginRequest request = 1;
+}
+message PostLoginResponse {
+ optional LoginResponse response = 1;
+}
+
+message PostLogoutRequest {
+ optional LogoutRequest request = 1;
+}
+message PostLogoutResponse {
+
+}
+
+message ValidateAuthTokenRequest {
+ optional string token = 1;
+}
+message ValidateAuthTokenResponse {
+}
+
+
+service LoginService {
+ option (scala_package) = "io.greenbus.client.service";
+ option (java_package) = "io.greenbus.japi.client.service";
+
+ /*
+ Provides user credentials in exchange for an auth token.
+
+ Request does not require authorization.
+ */
+ rpc login(PostLoginRequest) returns (PostLoginResponse);
+
+ /*
+ Logs out an auth token, causing subsequent authorizations using it to fail.
+ */
+ rpc logout(PostLogoutRequest) returns (PostLogoutResponse);
+
+ /*
+ Verifies an auth token is valid. Fails if the auth token is not authorized.
+
+ Used in situations where a process is managing requests on behalf of a client that has provided it
+ a pre-obtained auth token.
+ */
+ rpc validate(ValidateAuthTokenRequest) returns (ValidateAuthTokenResponse);
+
+}
diff --git a/client/src/main/proto/services/MeasurementRequests.proto b/client/src/main/proto/services/MeasurementRequests.proto
new file mode 100644
index 0000000..8c70492
--- /dev/null
+++ b/client/src/main/proto/services/MeasurementRequests.proto
@@ -0,0 +1,184 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package greenbus.client;
+
+option java_package = "io.greenbus.client.service.proto";
+option java_outer_classname = "MeasurementRequests";
+option java_generic_services = false;
+
+import "google/protobuf/descriptor.proto";
+import "CompilerExtensions.proto";
+
+import "Measurements.proto";
+import "Model.proto";
+import "services/ModelRequests.proto";
+
+
+/*
+ Specifies a sequence of historical Measurement values for a Point by time-order, with constraints.
+
+ The resulting sequence of Measurements is always returned in ascending time-order (oldest to newest).
+*/
+message MeasurementHistoryQuery {
+
+ /*
+ UUID of Point to find Measurements for.
+
+ @required
+ */
+ optional ModelUUID point_uuid = 1;
+
+ /*
+ Boundary of earliest time, exclusive, in the window of Measurements to return.
+
+ If not included, the beginning of the time window is unbounded, or practically, bounded to the
+ first Measurement published.
+
+ Time is measured in milliseconds since midnight, January 1, 1970 UTC.
+
+ @optional
+ */
+ optional uint64 time_from = 2;
+
+ /*
+ Boundary of latest time, inclusive, in the window of Measurements to return.
+
+ If not included, the end of the time window is unbounded, or practically, bounded to the current time.
+
+ Time is measured in milliseconds since midnight, January 1, 1970 UTC.
+
+ @optional
+ */
+ optional uint64 time_to = 3;
+
+ /*
+ Maximum number of Measurement values to return.
+
+ If not specified, the server provides a default value.
+
+ @optional
+ */
+ optional uint32 limit = 4;
+
+ /*
+ If the number of Measurements in the time window exceeds the limit on the number of Measurements to be
+ returned by the query, the sequence of Measurements will be justified to either the lower or higher
+ boundary of the window.
+
+ - If `false`, the returned Measurements will begin with the first in the time window and proceed until the time window
+ ends or the limit is met. Paging using this method would move forward in time.
+ - If `true`, the sequence of returned Measurements begins at the `limit`th value below the high boundary of the time window,
+ and proceeds to the high boundary. Paging using this method would move backward in time.
+
+ In both cases, the sequence of Measurements is returned in ascending time-order (oldest to newest).
+
+ The default value is `false`.
+
+ @optional
+ */
+ optional bool latest = 5;
+}
+
+/*
+ Selects Measurement batches to subscribe to. Can be narrowed by the UUIDs of Endpoints. If no Endpoints are
+ specified, subscribes to all notifications.
+*/
+message MeasurementBatchSubscriptionQuery {
+
+ /*
+ UUIDs of Endpoint to receive batch notifications for.
+
+ @required
+ */
+ repeated ModelUUID endpoint_uuids = 1;
+}
+
+
+message GetCurrentValuesRequest {
+ repeated ModelUUID point_uuids = 1;
+}
+message GetCurrentValuesResponse {
+ repeated PointMeasurementValue results = 1;
+}
+
+message GetMeasurementHistoryRequest {
+ optional MeasurementHistoryQuery request = 1;
+}
+message GetMeasurementHistoryResponse {
+ optional PointMeasurementValues result = 2;
+}
+
+message GetCurrentValuesAndSubscribeRequest {
+ repeated ModelUUID point_uuids = 1;
+}
+message GetCurrentValuesAndSubscribeResponse {
+ repeated PointMeasurementValue results = 1;
+}
+
+message SubscribeToBatchesRequest {
+ optional MeasurementBatchSubscriptionQuery query = 1;
+}
+message SubscribeToBatchesResponse {
+}
+
+message PostMeasurementsRequest {
+ optional MeasurementBatch batch = 1;
+}
+message PostMeasurementsResponse {
+}
+
+service MeasurementService {
+ option (scala_package) = "io.greenbus.client.service";
+ option (java_package) = "io.greenbus.japi.client.service";
+
+ /*
+ Gets current Measurement values for Points specified by UUID.
+ */
+ rpc get_current_values(GetCurrentValuesRequest) returns (GetCurrentValuesResponse);
+
+ /*
+ Queries for a time series of previous Measurement values.
+ */
+ rpc get_history(GetMeasurementHistoryRequest) returns (GetMeasurementHistoryResponse);
+
+ /*
+ Gets current Measurement values for Points specified by UUID, and subscribes to subsequent Measurements for
+ those Points.
+ */
+ rpc get_current_values_and_subscribe(GetCurrentValuesAndSubscribeRequest) returns (GetCurrentValuesAndSubscribeResponse) {
+ option (subscription_type) = "greenbus.client.MeasurementNotification";
+ }
+
+ /*
+ Subscribes to notifications for batches of Measurements. Measurements are in the batches they were published
+ to the measurement processor.
+
+ The subscription can be filtered by the UUIDs of the Endpoints they are associated with.
+ */
+ rpc subscribe_to_batches(SubscribeToBatchesRequest) returns (SubscribeToBatchesResponse) {
+ option (subscription_type) = "greenbus.client.MeasurementBatchNotification";
+ }
+
+ /*
+ Posts a batch of Measurements to a measurement processor.
+ */
+ rpc post_measurements(PostMeasurementsRequest) returns (PostMeasurementsResponse) {
+ option (addressed) = ALWAYS;
+ }
+}
diff --git a/client/src/main/proto/services/ModelRequests.proto b/client/src/main/proto/services/ModelRequests.proto
new file mode 100644
index 0000000..9ea5731
--- /dev/null
+++ b/client/src/main/proto/services/ModelRequests.proto
@@ -0,0 +1,1288 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package greenbus.client;
+
+option java_package = "io.greenbus.client.service.proto";
+option java_outer_classname = "ModelRequests";
+option java_generic_services = false;
+
+import "google/protobuf/descriptor.proto";
+import "CompilerExtensions.proto";
+
+import "Model.proto";
+import "ServiceEnvelopes.proto";
+
+/*
+ Contains common parameters for specifying paging in model queries.
+*/
+message EntityPagingParams {
+
+ /*
+ If `true` results are sorted by name. If `false` results are sorted by UUID.
+
+ Default is `true`.
+
+ @optional
+ */
+ optional bool page_by_name = 12;
+
+ /*
+ Last UUID (in the returned sorted order) of a previous result set. This result set will be the
+ page beginning from the next UUID.
+
+ @optional
+ */
+ optional ModelUUID last_uuid = 14;
+
+ /*
+ Last name (in the returned sorted order) of a previous result set. This result set will be the
+ page beginning from the next name.
+
+ @optional
+ */
+ optional string last_name = 13;
+
+ /*
+ The number of elements to return. Used to limit the size of result sets in large models. If not set,
+ a server-determined page size will be used.
+
+ If the size of the result set equals the page size, there may be more data available.
+
+ @optional
+ */
+ optional uint32 page_size = 15;
+}
+
+/*
+ Description used to create or modify an Entity.
+
+ New Entities are created by using a template with no UUID, identified by name and with a set of types. The UUID for the Entity
+ will be generated on the server.
+
+ Including a UUID will either create a new Entity with a pre-defined UUID (e.g. imported), or modify the name and types
+ of an existing Entity.
+*/
+message EntityTemplate {
+
+ /*
+ Identifies a pre-existing Entity, or provides a pre-determined UUID for a new Entity.
+
+ @optional
+ */
+ optional ModelUUID uuid = 1;
+
+ /*
+ Identifies a pre-existing Entity, provides a name for a created Entity, or a new name for an existing Entity.
+
+ - If a UUID is not provided and the Entity does not exist an Entity will be created with this name.
+ - If a UUID is not provided and the Entity exists, the Entity will be identified and only the types will be (potentially) modified.
+ - If a UUID is provided and the Entity does not exist, an Entity will be created with this name.
+ - If a UUID is provided and the Entity already exists, the Entity will be renamed to this name.
+
+ @required
+ */
+ optional string name = 2;
+
+ /*
+ The set of types for the Entity.
+
+ - If the Entity is being created, all types will be added.
+ - If the Entity is being modified, types not in this field will be removed and types in this field and not previously in the Entity will be added.
+ - An empty set of types (including if the field is uninitialized) will result in the Entity containing no types.
+
+ @required
+ */
+ repeated string types = 3;
+}
+
+/*
+ Collection of keys that identify a set of Entities.
+*/
+message EntityKeySet {
+
+ /*
+ UUIDs of Entities.
+
+ @optional
+ */
+ repeated ModelUUID uuids = 1;
+
+ /*
+ Names of Entities.
+
+ @optional
+ */
+ repeated string names = 2;
+}
+
+/*
+ Parameters to match Entities based on their types.
+*/
+message EntityTypeParams {
+
+ /*
+ Any Entity that has a type in this set will be included.
+
+ @optional
+ */
+ repeated string include_types = 2;
+
+ /*
+ If non-empty, an Entity must have all types in this set to be included.
+
+ @optional
+ */
+ repeated string match_types = 5;
+
+ /*
+ Entities that have a type in this set will be excluded.
+
+ @optional
+ */
+ repeated string filter_out_types = 6;
+}
+
+/*
+ Query parameters to select a set of Entities.
+
+ Including no search parameters will return all Entities in the system.
+
+ Uses paging to handle large result sets.
+*/
+message EntityQuery {
+
+ /*
+ Parameters to match Entities based on their types.
+
+ @optional
+ @nested
+ */
+ optional EntityTypeParams type_params = 1;
+
+ /*
+ Parameters to narrow the result sets for queries.
+
+ @optional
+ @nested
+ */
+ optional EntityPagingParams paging_params = 14;
+}
+
+/*
+ Subscription parameters for Entities. Entity notifications can be filtered by key.
+
+ - Including key parameters will return the current state of those Entities and subscribe to notifications for them.
+ - Including no parameters will subscribe to all Entity notifications, but return no immediate results.
+
+ __Note__: Entity notifications are filtered by UUID, not name. Using a name parameter resolves
+ the UUID of the Entity associated with that name at the time of the request, and is
+ equivalent to the client resolving the UUID before making the request. As a result, if the
+ Entity a particular name refers to changes (due to a deletion/addition or two renames) notifications will be delivered
+ for only for the original Entity.
+
+*/
+message EntitySubscriptionQuery {
+
+ /*
+ UUIDs of Entities.
+
+ @optional
+ */
+ repeated ModelUUID uuids = 1;
+
+ /*
+ Names of Entities.
+
+ @optional
+ */
+ repeated string names = 2;
+}
+
+/*
+ Queries for a result set of Entities based on their relationship in the graph model to a "start" set.
+ The start set can include multiple Entities, including those only specified by reference to their
+ type, and the query can reach across derived edges to return Entities connected through transitive
+ relationships. However, the service returns a flat result set, so the mapping between Entities in the result
+ set and Entities in the start set is not preserved. To get information about the relationships between
+ parents and children in the graph model, see *ModelService.edge_query*.
+
+ The query may be directed in either direction; from parents to children or vice versa.
+
+ The cost of the query does not increase with depth.
+
+ Uses paging to handle large result sets.
+
+*/
+message EntityRelationshipFlatQuery {
+
+ /*
+ Specifies UUIDs of Entities to include in the start set.
+
+ One or more of start_uuids, start_names, or start_types must be specified.
+
+ @optional
+ */
+ repeated ModelUUID start_uuids = 1;
+
+ /*
+ Specifies names of Entities to include in the start set.
+
+ One or more of start_uuids, start_names, or start_types must be specified.
+
+ @optional
+ */
+ repeated string start_names = 2;
+
+ /*
+ Entities having one or more of the specified types will be included in the start set. Note that Entities
+ with the same type may appear at different levels in the same hierachy, and so may also be in the
+ result set.
+
+ Entities specified by UUID or name do not have to include these types; the start set is the union of
+ the result of all three sets of parameters.
+
+ One or more of start_uuids, start_names, or start_types must be specified.
+
+ @optional
+ */
+ repeated string start_types = 3;
+
+ /*
+ Relationship type between parents and children.
+
+ @required
+ */
+ optional string relationship = 10;
+
+ /*
+ Specifies the direction of the query.
+
+ - If true, the Entities in the start set are the parents and the result set are the children.
+ - If false, the Entities in the start set are the children and the result set are the parents.
+
+ @required
+ */
+ optional bool descendant_of = 11;
+
+ /*
+ Narrows the result set of Entities to only those with one of the specified types.
+
+ - If types are specified, only Entities with one or more of the types in the list will be included in the result set.
+ - If no types are specified, all Entities will be included in the result set.
+
+ @optional
+ */
+ repeated string end_types = 12;
+
+ /*
+ Provides an optional limit to the depth of the query. Relationships in the graph are transitive, and
+ by default the query will return all Entities with immediate and derived edges for the given relationship.
+ Setting a depth limit will only return Entities at a distance up to and including that depth.
+
+ Example:
+
+
+ Entity tree: A -> B -> C -> D
+ Start set: ( A )
+
+ No depth limit: ( B, C, D )
+ Depth limit = 1: ( B )
+ Depth limit = 2: ( B, C )
+
+
+ @optional
+ */
+ optional uint32 depth_limit = 13;
+
+ /*
+ Parameters to narrow the result sets for queries.
+
+ @optional
+ @nested
+ */
+ optional EntityPagingParams paging_params = 14;
+}
+
+/*
+ Queries for edges in the graph model. Can search according to parents, children, relationship types,
+ and depth.
+
+ Including no search parameters will return all edges in the graph model. Including only
+ `depth_limit = 1` will return all immediate edges, which is sufficient to reconstruct the model.
+
+ Enables querying for derived edges, but the intermediate path will not necessarily be included.
+
+ The cost of the query does not increase with depth.
+
+ Uses paging to handle large result sets.
+*/
+message EntityEdgeQuery {
+
+ /*
+ Filters results by parent UUIDs.
+
+ @optional
+ */
+ repeated ModelUUID parent_uuids = 1;
+
+ /*
+ Filters results by child UUIDs.
+
+ @optional
+ */
+ repeated ModelUUID child_uuids = 2;
+
+ /*
+ Filters results by relationships.
+
+ - If not specified, edges of any relationship will be returned.
+ - If specified, only edges with one of the listed relationships will be returned.
+
+ @optional
+ */
+ repeated string relationships = 3;
+
+ /*
+ Provides an optional limit to the depth of the query. By default, the query will return any and all
+ derived edges (`distance > 1`). Setting a depth limit will return only edges up to and including that
+ depth.
+
+ Example:
+
+
+ Entity tree: A -> B -> C -> D
+ Parent: ( A )
+
+ No depth limit: ( (A -> B), (A -> C), (A -> D) )
+ Depth limit = 1: ( (A -> B) )
+ Depth limit = 2: ( (A -> B), (A -> C) )
+
+
+ Note that intermediate edges are not necessarily provided (unless they are otherwise part of the result set).
+ In other words, in the example above, `(B -> C)`, `(B -> D)`, and `(C -> D)` are all unstated. The same result
+ would hold in an alternate scenario where `C` and `D` were immediate children of `B`.
+
+ @optional
+ */
+ optional uint32 depth_limit = 4;
+
+ /*
+ Last ID (in the returned sorted order) of a previous result set. This result set will be the
+ page beginning from the next ID.
+
+ @optional
+ */
+ optional ModelID last_id = 21;
+
+ /*
+ The number of elements to return. Used to limit the size of result sets in large models. If not set,
+ a server-determined page size will be used.
+
+ If the size of the result set equals the page size, there may be more data available.
+
+ @optional
+ */
+ optional uint32 page_size = 22;
+}
+
+/*
+ Describes a filter for EntityEdges.
+
+ An EntityEdge that is added or removed must match all specified fields to be
+ included by this filter.
+*/
+message EntityEdgeFilter {
+
+ /*
+ Filter EntityEdges with this parent UUID.
+
+ @optional
+ */
+ optional ModelUUID parent_uuid = 1;
+
+ /*
+ Filter EntityEdges with this child UUID.
+
+ @optional
+ */
+ optional ModelUUID child_uuid = 2;
+
+ /*
+ Filter EntityEdges with this relationship.
+
+ @optional
+ */
+ optional string relationship = 3;
+
+ /*
+ Filter EntityEdges at this distance.
+
+ Most likely to be useful for filtering for immediate edges, or `distance = 1`.
+
+ @optional
+ */
+ optional uint32 distance = 4;
+}
+
+/*
+ Query for subscribing to notifications for a set of EntityEdges.
+
+ Including no parameters will subscribe to all EntityEdge notifications.
+*/
+message EntityEdgeSubscriptionQuery {
+
+ /*
+ A set of filters for EntityEdges for which to be notified of changes.
+
+ Notifications will be sent if an EntityEdge that is added or removed matches one or
+ more of these filters.
+
+ @optional
+ @nested
+ */
+ repeated EntityEdgeFilter filters = 2;
+}
+
+/*
+ Describes an immediate edge in the graph model. All immediate edges are unique for the combined values of these
+ fields.
+*/
+message EntityEdgeDescriptor {
+
+ /*
+ UUID of parent Entity for this edge.
+
+ @required
+ */
+ optional ModelUUID parent_uuid = 1;
+
+ /*
+ UUID of child Entity for this edge.
+
+ @required
+ */
+ optional ModelUUID child_uuid = 2;
+
+ /*
+ Relationship type for this edge.
+
+ @required
+ */
+ optional string relationship = 3;
+}
+
+
+/*
+ Description used to create or modify a Point.
+*/
+message PointTemplate {
+
+ /*
+ Description of the Entity information for this Point.
+
+ @required
+ @nested
+ */
+ optional EntityTemplate entity_template = 1;
+
+ /*
+ Specifies a high-level category for the data of Measurements associated with this Point.
+
+ @required
+ */
+ optional PointCategory point_category = 5;
+
+ /*
+ Specifies the unit of Measurements associated with this Point.
+
+ @required
+ */
+ optional string unit = 6;
+}
+
+/*
+ Query parameters to select a set of Points.
+
+ Points can be matched by their Entity types or Point-specific fields.
+ Including no search parameters will return all Points in the system.
+
+ Uses paging to handle large result sets.
+*/
+message PointQuery {
+
+ /*
+ Parameters to match Points based on their Entity types.
+
+ @optional
+ @nested
+ */
+ optional EntityTypeParams type_params = 1;
+
+ /*
+ Parameters to match Points based on PointCategory. Points with one of any of the specified PointCategories will be matched.
+
+ @optional
+ */
+ repeated PointCategory point_categories = 5;
+
+ /*
+ Parameters to match Points based on unit. Points with one of any of the specified units will be matched.
+
+ @optional
+ */
+ repeated string units = 6;
+
+ /*
+ Parameters to narrow the result sets for queries.
+
+ @optional
+ @nested
+ */
+ optional EntityPagingParams paging_params = 14;
+}
+
+
+/*
+ Subscription parameters for Points. Notifications can be filtered by key or by Endpoint associations.
+
+ - Including key parameters (and no Endpoint UUIDs) will return the current state of those Points
+ and subscribe to notifications for them.
+ - Including Endpoint UUIDs will subscribe to notifications for Points associated with those Endpoints,
+ but return no immediate results.
+ - Including no parameters will subscribe to notifications for all Points, but return no immediate results.
+
+ __Note__: Point notifications are filtered by UUID, not name. Using a name parameter resolves
+ the UUID of the Point associated with that name at the time of the request, and is
+ equivalent to the client resolving the UUID before making the request. As a result, if the
+ Point a particular name refers to changes (due to a deletion/addition or two renames) notifications will be delivered
+ for only for the original Point.
+
+ Endpoint UUIDs are _not_ resolved to a set of Points at request time, and updates in the system model to associations between Points
+ and Endpoints will affect what notifications are delivered to the subscription.
+*/
+message PointSubscriptionQuery {
+
+ /*
+ UUIDs of Points to subscribe to notifications for.
+
+ @optional
+ */
+ repeated ModelUUID uuids = 1;
+
+ /*
+ Names of Points to subscribe to notifications for.
+
+ @optional
+ */
+ repeated string names = 2;
+
+ /*
+ UUIDs of Endpoints whose associated Points should be subscribed to notifications for.
+
+ @optional
+ */
+ repeated ModelUUID endpoint_uuids = 3;
+}
+
+/*
+ Description used to create or modify a Command.
+*/
+message CommandTemplate {
+
+ /*
+ Description of the Entity information for this Command.
+
+ @required
+ @nested
+ */
+ optional EntityTemplate entity_template = 1;
+
+ /*
+ Specifies an explicative name that does not need to be globally unique.
+
+ @required
+ */
+ optional string display_name = 3;
+
+ /*
+ Specifies whether CommandRequests associated with a Command contain a value, and what data type the value is.
+
+ @required
+ */
+ optional CommandCategory category = 7;
+}
+
+/*
+ Query parameters to select a set of Commands.
+
+ Commands can be matched by their Entity types or Command-specific fields.
+ Including no search parameters will return all Commands in the system.
+
+ Uses paging to handle large result sets.
+*/
+message CommandQuery {
+
+ /*
+ Parameters to match Commands based on their Entity types.
+
+ @optional
+ @nested
+ */
+ optional EntityTypeParams type_params = 1;
+
+ /*
+ Parameters to match Commands based on CommandCategory. Commands with one of any of the specified CommandCategorys will be matched.
+
+ @optional
+ */
+ repeated CommandCategory command_categories = 5;
+
+ /*
+ Parameters to narrow the result sets for queries.
+
+ @optional
+ @nested
+ */
+ optional EntityPagingParams paging_params = 14;
+}
+
+/*
+ Subscription parameters for Commands. Notifications can be filtered by key or by Endpoint associations.
+
+ - Including key parameters (and no Endpoint UUIDs) will return the current state of those Commands
+ and subscribe to notifications for them.
+ - Including Endpoint UUIDs will subscribe to notifications for Commands associated with those Endpoints,
+ but return no immediate results.
+ - Including no parameters will subscribe to notifications for all Commands, but return no immediate results.
+
+ __Note__: Command notifications are filtered by UUID, not name. Using a name parameter resolves
+ the UUID of the Command associated with that name at the time of the request, and is
+ equivalent to the client resolving the UUID before making the request. As a result, if the
+ Command a particular name refers to changes (due to a deletion/addition or two renames) notifications will be delivered
+ for only for the original Command.
+
+ Endpoint UUIDs are _not_ resolved to a set of Commands at request time, and updates in the system model to associations between Commands
+ and Endpoints will affect what notifications are delivered to the subscription.
+*/
+message CommandSubscriptionQuery {
+
+ /*
+ UUIDs of Commands to subscribe to notifications for.
+
+ @optional
+ */
+ repeated ModelUUID uuids = 1;
+
+ /*
+ Names of Commands to subscribe to notifications for.
+
+ @optional
+ */
+ repeated string names = 2;
+
+ /*
+ UUIDs of Endpoints whose associated Commands should be subscribed to notifications for.
+
+ @optional
+ */
+ repeated ModelUUID endpoint_uuids = 3;
+}
+
+/*
+ Description used to create or modify an Endpoint.
+*/
+message EndpointTemplate {
+
+ /*
+ Description of the Entity information for this Endpoint.
+
+ @required
+ @nested
+ */
+ optional EntityTemplate entity_template = 1;
+
+ /*
+ Identifies the type of front-end that is intended to service the Points and Commands
+ associated with this Endpoint.
+
+ @required
+ */
+ optional string protocol = 3;
+
+ /*
+ Specifies whether an Endpoint has been disabled, so that Measurements will not be published and Commands
+ will not be processed for it.
+
+ The default value for new Endpoints is `false`. If not specified, modifications to existing Endpoints will not change the
+ disabled value.
+
+ @optional
+ */
+ optional bool disabled = 4;
+}
+
+/*
+ Query parameters to select a set of Endpoints.
+
+ Endpoints can be matched by their Entity types or Endpoint-specific fields.
+ Including no search parameters will return all Endpoints in the system.
+
+ Uses paging to handle large result sets.
+*/
+message EndpointQuery {
+
+ /*
+ Parameters to match Endpoints based on their Entity types.
+
+ @optional
+ @nested
+ */
+ optional EntityTypeParams type_params = 1;
+
+ /*
+ Parameters to match Endpoints based on protocol. Endpoints with one of any of the specified protocols will be matched.
+
+ @optional
+ */
+ repeated string protocols = 4;
+
+ /*
+ Parameter to filter Endpoints by the state of their `disabled` field. If set to `true`, only matches Endpoints marked as disabled.
+ If set to `false`, only matches Endpoints not marked as disabled.
+
+ @optional
+ */
+ optional bool disabled = 5;
+
+ /*
+ Parameters to narrow the result sets for queries.
+
+ @optional
+ @nested
+ */
+ optional EntityPagingParams paging_params = 14;
+}
+
+/*
+ Subscription parameters for Endpoints. Notifications can be filtered by key or by protocol.
+
+ - Including key parameters will return the current state of those Endpoints
+ and subscribe to notifications for them.
+ - Including protocol parameters will subscribe to notifications for Endpoints associated with those Endpoints,
+ but return no immediate results.
+ - Including no parameters will subscribe to notifications for all Endpoints, but return no immediate results.
+
+ __Note__: Endpoint notifications are filtered by UUID, not name. Using a name parameter resolves
+ the UUID of the Endpoint associated with that name at the time of the request, and is
+ equivalent to the client resolving the UUID before making the request. As a result, if the
+ Endpoint a particular name refers to changes (due to a deletion/addition or two renames) notifications will be delivered
+ for only for the original Endpoint.
+*/
+message EndpointSubscriptionQuery {
+
+ /*
+ UUIDs of Endpoints to subscribe to notifications for.
+
+ @optional
+ */
+ repeated ModelUUID uuids = 1;
+
+ /*
+ Names of Endpoints to subscribe to notifications for.
+
+ @optional
+ */
+ repeated string names = 2;
+
+ /*
+ Parameters for subscribing to notifications by Endpoints' protocols.
+
+ @optional
+ */
+ repeated string protocols = 3;
+}
+
+/*
+ Represents a modification to an Endpoint's enabled/disabled state.
+*/
+message EndpointDisabledUpdate {
+
+ /*
+ UUID of the Endpoint to be modified.
+
+ @required
+ */
+ optional ModelUUID endpoint_uuid = 1;
+
+ /*
+ Whether the Endpoint's state should be disabled or not.
+
+ - If `true`, the Endpoint will be disabled.
+ - If `false`, the Endpoint will be enabled.
+
+ @required
+ */
+ optional bool disabled = 2;
+}
+
+/*
+ Represents the identify of an EntityKeyValue, the combination of the model object's UUID and the key.
+*/
+message EntityKeyPair {
+
+ /*
+ UUID of the model object the key-value is associated with.
+
+ @required
+ */
+ optional ModelUUID uuid = 1;
+
+ /*
+ Key string for the key-value.
+
+ @required
+ */
+ optional string key = 2;
+}
+
+/*
+ Subscription parameters for Endpoints. Notifications can be filtered by UUIDs, UUID/key pairs, or the UUIDs of the Endpoints associated
+ with the model objects.
+*/
+message EntityKeyValueSubscriptionQuery {
+
+ /*
+ UUIDs of EntityKeyValues to subscribe to notifications for.
+
+ @optional
+ */
+ repeated ModelUUID uuids = 1;
+
+ /*
+ UUID and key combinations to subscribe to notifications for.
+
+ @optional
+ */
+ repeated EntityKeyPair key_pairs = 2;
+
+ /*
+ UUIDs of Endpoints whose associated EntityKeyValues should be subscribed to notifications for.
+
+ @optional
+ */
+ repeated ModelUUID endpoint_uuids = 3;
+}
+
+message GetEntitiesRequest {
+ optional EntityKeySet request = 1;
+}
+message GetEntitiesResponse {
+ repeated Entity results = 1;
+}
+message EntityQueryRequest {
+ optional EntityQuery query = 1;
+}
+message EntityQueryResponse {
+ repeated Entity results = 1;
+}
+
+message SubscribeEntitiesRequest {
+ optional EntitySubscriptionQuery query = 1;
+}
+message SubscribeEntitiesResponse {
+ repeated Entity results = 1;
+}
+
+
+message EntityRelationshipFlatQueryRequest {
+ optional EntityRelationshipFlatQuery query = 1;
+}
+message EntityRelationshipFlatQueryResponse {
+ repeated Entity results = 1;
+}
+
+message PutEntitiesRequest {
+ repeated EntityTemplate entities = 1;
+}
+message PutEntitiesResponse {
+ repeated Entity results = 1;
+}
+message DeleteEntitiesRequest {
+ repeated ModelUUID entity_uuids = 1;
+}
+message DeleteEntitiesResponse {
+ repeated Entity results = 1;
+}
+
+
+message GetEntityKeyValuesRequest {
+ repeated EntityKeyPair request = 1;
+}
+message GetEntityKeyValuesResponse {
+ repeated EntityKeyValue results = 1;
+}
+
+message GetKeysForEntitiesRequest {
+ repeated ModelUUID request = 1;
+}
+message GetKeysForEntitiesResponse {
+ repeated EntityKeyPair results = 1;
+}
+
+message PutEntityKeyValuesRequest {
+ repeated EntityKeyValue key_values = 1;
+}
+message PutEntityKeyValuesResponse {
+ repeated EntityKeyValue results = 1;
+}
+
+message DeleteEntityKeyValuesRequest {
+ repeated EntityKeyPair request = 1;
+}
+message DeleteEntityKeyValuesResponse {
+ repeated EntityKeyValue results = 1;
+}
+
+message SubscribeEntityKeyValuesRequest {
+ optional EntityKeyValueSubscriptionQuery query = 1;
+}
+message SubscribeEntityKeyValuesResponse {
+ repeated EntityKeyValue results = 1;
+}
+
+message EntityEdgeQueryRequest {
+ optional EntityEdgeQuery query = 1;
+}
+message EntityEdgeQueryResponse {
+ repeated EntityEdge results = 1;
+}
+
+message PutEntityEdgesRequest {
+ repeated EntityEdgeDescriptor descriptors = 1;
+}
+message PutEntityEdgesResponse {
+ repeated EntityEdge results = 1;
+}
+message DeleteEntityEdgesRequest {
+ repeated EntityEdgeDescriptor descriptors = 1;
+}
+message DeleteEntityEdgesResponse {
+ repeated EntityEdge results = 1;
+}
+message SubscribeEntityEdgesRequest {
+ optional EntityEdgeSubscriptionQuery query = 1;
+}
+message SubscribeEntityEdgesResponse {
+ repeated EntityEdge results = 1;
+}
+
+message GetPointsRequest {
+ optional EntityKeySet request = 1;
+}
+message GetPointsResponse {
+ repeated Point results = 1;
+}
+message PointQueryRequest {
+ optional PointQuery request = 1;
+}
+message PointQueryResponse {
+ repeated Point results = 1;
+}
+message PutPointsRequest {
+ repeated PointTemplate templates = 1;
+}
+message PutPointsResponse {
+ repeated Point results = 1;
+}
+message DeletePointsRequest {
+ repeated ModelUUID point_uuids = 1;
+}
+message DeletePointsResponse {
+ repeated Point results = 1;
+}
+message SubscribePointsRequest {
+ optional PointSubscriptionQuery request = 1;
+}
+message SubscribePointsResponse {
+ repeated Point results = 1;
+}
+
+message GetCommandsRequest {
+ optional EntityKeySet request = 1;
+}
+message GetCommandsResponse {
+ repeated Command results = 1;
+}
+message CommandQueryRequest {
+ optional CommandQuery request = 1;
+}
+message CommandQueryResponse {
+ repeated Command results = 1;
+}
+message PutCommandsRequest {
+ repeated CommandTemplate templates = 1;
+}
+message PutCommandsResponse {
+ repeated Command results = 1;
+}
+message DeleteCommandsRequest {
+ repeated ModelUUID command_uuids = 1;
+}
+message DeleteCommandsResponse {
+ repeated Command results = 1;
+}
+message SubscribeCommandsRequest {
+ optional CommandSubscriptionQuery request = 1;
+}
+message SubscribeCommandsResponse {
+ repeated Command results = 1;
+}
+
+message GetEndpointsRequest {
+ optional EntityKeySet request = 1;
+}
+message GetEndpointsResponse {
+ repeated Endpoint results = 1;
+}
+message EndpointQueryRequest {
+ optional EndpointQuery request = 1;
+}
+message EndpointQueryResponse {
+ repeated Endpoint results = 1;
+}
+message PutEndpointsRequest {
+ repeated EndpointTemplate templates = 1;
+}
+message PutEndpointsResponse {
+ repeated Endpoint results = 1;
+}
+message DeleteEndpointsRequest {
+ repeated ModelUUID endpoint_uuids = 1;
+}
+message DeleteEndpointsResponse {
+ repeated Endpoint results = 1;
+}
+message SubscribeEndpointsRequest {
+ optional EndpointSubscriptionQuery request = 1;
+}
+message SubscribeEndpointsResponse {
+ repeated Endpoint results = 1;
+}
+message PutEndpointDisabledRequest {
+ repeated EndpointDisabledUpdate updates = 1;
+}
+message PutEndpointDisabledResponse {
+ repeated Endpoint results = 1;
+}
+
+/*
+
+
+*/
+service ModelService {
+ option (scala_package) = "io.greenbus.client.service";
+ option (java_package) = "io.greenbus.japi.client.service";
+
+ /*
+ Returns a set of Entities identified specifically by UUID or name.
+ */
+ rpc get(GetEntitiesRequest) returns (GetEntitiesResponse);
+
+ /*
+ Returns all Entities or those matching query parameters. Uses paging to
+ handle large result sets.
+ */
+ rpc entity_query(EntityQueryRequest) returns (EntityQueryResponse);
+
+ /*
+ Subscribes to Entity notifications, and returns the current state of the Entities.
+ */
+ rpc subscribe(SubscribeEntitiesRequest) returns (SubscribeEntitiesResponse) {
+ option (subscription_type) = "greenbus.client.EntityNotification";
+ }
+
+ /*
+ Uses the graph model to return a set of Entities specified by their relationships.
+ */
+ rpc relationship_flat_query(EntityRelationshipFlatQueryRequest) returns (EntityRelationshipFlatQueryResponse);
+
+ /*
+ Creates or modifies a set of Entities.
+
+ May not be used to modify Entities that have a more specific service type (Agent, Point, etc.). See the associated
+ service for those types.
+ */
+ rpc put(PutEntitiesRequest) returns (PutEntitiesResponse);
+
+ /*
+ Deletes a set of Entities specified by UUID. Returns the final state of the deleted Entities.
+ */
+ rpc delete(DeleteEntitiesRequest) returns (DeleteEntitiesResponse);
+
+ /*
+ Returns a set of EntityEdges that represents some subset of the graph model.
+ */
+ rpc edge_query(EntityEdgeQueryRequest) returns (EntityEdgeQueryResponse);
+
+ /*
+ Creates a set of EntityEdges. EntityEdges are value types and cannot be modified, only deleted.
+ Putting already existing EntityEdges will succeed with no effect.
+
+ Only immediate edges (`distance = 1`) can be added by clients. Derived edges are maintained
+ automatically.
+ */
+ rpc put_edges(PutEntityEdgesRequest) returns (PutEntityEdgesResponse);
+
+ /*
+ Deletes a set of EntityEdges.
+
+ Only immediate edges (`distance = 1`) can be removed by clients. Derived edges are removed
+ automatically.
+ */
+ rpc delete_edges(DeleteEntityEdgesRequest) returns (DeleteEntityEdgesResponse);
+
+ /*
+ Subscribes to notifications for a set of EntityEdges.
+
+ Because the result set cannot be constrained, no current values of EntityEdges are returned.
+ */
+ rpc subscribe_to_edges(SubscribeEntityEdgesRequest) returns (SubscribeEntityEdgesResponse) {
+ option (subscription_type) = "greenbus.client.EntityEdgeNotification";
+ }
+
+
+ /*
+ Returns a set of Points identified specifically by UUID or name.
+ */
+ rpc get_points(GetPointsRequest) returns (GetPointsResponse);
+
+ /*
+ Returns all Points or those matching query parameters. Uses paging to
+ handle large result sets.
+ */
+ rpc point_query(PointQueryRequest) returns (PointQueryResponse);
+
+ /*
+ Creates or modifies a set of Points.
+ */
+ rpc put_points(PutPointsRequest) returns (PutPointsResponse);
+
+ /*
+ Deletes a set of Points identified by UUID. Returns the final state of the deleted Points.
+ */
+ rpc delete_points(DeletePointsRequest) returns (DeletePointsResponse);
+
+ /*
+ Subscribes to Point notifications, and returns the current state of the Points.
+ */
+ rpc subscribe_to_points(SubscribePointsRequest) returns (SubscribePointsResponse) {
+ option (subscription_type) = "greenbus.client.PointNotification";
+ }
+
+
+ /*
+ Returns a set of Commands identified specifically by UUID or name.
+ */
+ rpc get_commands(GetCommandsRequest) returns (GetCommandsResponse);
+
+ /*
+ Returns all Commands or those matching query parameters. Uses paging to
+ handle large result sets.
+ */
+ rpc command_query(CommandQueryRequest) returns (CommandQueryResponse);
+
+ /*
+ Creates or modifies a set of Commands.
+ */
+ rpc put_commands(PutCommandsRequest) returns (PutCommandsResponse);
+
+ /*
+ Deletes a set of Commands identified by UUID. Returns the final state of the deleted Commands.
+ */
+ rpc delete_commands(DeleteCommandsRequest) returns (DeleteCommandsResponse);
+
+ /*
+ Subscribes to Command notifications, and returns the current state of the Commands.
+ */
+ rpc subscribe_to_commands(SubscribeCommandsRequest) returns (SubscribeCommandsResponse) {
+ option (subscription_type) = "greenbus.client.CommandNotification";
+ }
+
+
+ /*
+ Returns a set of Endpoints identified specifically by UUID or name.
+ */
+ rpc get_endpoints(GetEndpointsRequest) returns (GetEndpointsResponse);
+
+ /*
+ Returns all Endpoints or those matching query parameters. Uses paging to
+ handle large result sets.
+ */
+ rpc endpoint_query(EndpointQueryRequest) returns (EndpointQueryResponse);
+
+ /*
+ Creates or modifies a set of Endpoints.
+ */
+ rpc put_endpoints(PutEndpointsRequest) returns (PutEndpointsResponse);
+
+ /*
+ Deletes a set of Endpoints identified by UUID. Returns the final state of the deleted Endpoints.
+ */
+ rpc delete_endpoints(DeleteEndpointsRequest) returns (DeleteEndpointsResponse);
+
+ /*
+ Subscribes to Endpoint notifications, and returns the current state of the Endpoints.
+ */
+ rpc subscribe_to_endpoints(SubscribeEndpointsRequest) returns (SubscribeEndpointsResponse) {
+ option (subscription_type) = "greenbus.client.EndpointNotification";
+ }
+
+ /*
+ Sets whether a set of Endpoints are enabled or disabled.
+ */
+ rpc put_endpoint_disabled(PutEndpointDisabledRequest) returns (PutEndpointDisabledResponse);
+
+ /*
+ Returns a set of Entity key values identified by UUID and key.
+ */
+ rpc get_entity_key_values(GetEntityKeyValuesRequest) returns (GetEntityKeyValuesResponse);
+
+ /*
+ Returns a set of Entity keys associated with the specified UUIDs.
+ */
+ rpc get_entity_keys(GetKeysForEntitiesRequest) returns (GetKeysForEntitiesResponse);
+
+ /*
+ Creates or modifies a set of Entity key values.
+ */
+ rpc put_entity_key_values(PutEntityKeyValuesRequest) returns (PutEntityKeyValuesResponse);
+
+ /*
+ Deletes a set of Entity key values identified by UUID and key. Returns the final state of the deleted values.
+ */
+ rpc delete_entity_key_values(DeleteEntityKeyValuesRequest) returns (DeleteEntityKeyValuesResponse);
+
+ /*
+ Subscribes to Entity key value notifications, and returns the current state of the Entity key values.
+ */
+ rpc subscribe_to_entity_key_values(SubscribeEntityKeyValuesRequest) returns (SubscribeEntityKeyValuesResponse) {
+ option (subscription_type) = "greenbus.client.EntityKeyValueNotification";
+ }
+
+}
diff --git a/client/src/main/proto/services/ProcessingRequests.proto b/client/src/main/proto/services/ProcessingRequests.proto
new file mode 100644
index 0000000..6f946cf
--- /dev/null
+++ b/client/src/main/proto/services/ProcessingRequests.proto
@@ -0,0 +1,142 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package greenbus.client;
+
+option java_package = "io.greenbus.client.service.proto";
+option java_outer_classname = "ProcessingRequests";
+option java_generic_services = false;
+
+import "google/protobuf/descriptor.proto";
+import "CompilerExtensions.proto";
+
+import "Processing.proto";
+import "Model.proto";
+import "ServiceEnvelopes.proto";
+import "services/ModelRequests.proto";
+
+/*
+ Subscription parameters for MeasOverrides. Notifications can be filtered by Point or Endpoint.
+
+ - Including key parameters (and no Endpoint UUIDs) will return the current state of the MeasOverrides for those Points
+ and subscribe to notifications for them.
+ - Including Endpoint UUIDs will subscribe to MeasOverride notifications for Points associated with those Endpoints,
+ but return no immediate results.
+ - Including no parameters will subscribe to MeasOverride notifications for all Points, but return no immediate results.
+
+ __Note__: MeasOverride notifications are filtered by Point UUID, not Point name. Using a name parameter resolves
+ the UUID of the Point associated with that name at the time of the request, and is
+ equivalent to the client resolving the UUID before making the request. As a result, if the
+ Point a particular name refers to changes (due to a deletion/addition or two renames) notifications will be delivered
+ for only for the original Point.
+
+ Endpoint UUIDs are _not_ resolved to a set of Points at request time, and updates in the system model to associations between Points
+ and Endpoints will affect what notifications are delivered to the subscription.
+*/
+message OverrideSubscriptionQuery {
+
+ /*
+ UUIDs of Points to subscribe to MeasOverrides for.
+
+ @optional
+ @optional
+ */
+ repeated ModelUUID point_uuids = 1;
+
+ /*
+ Names of Points to subscribe to MeasOverrides for.
+
+ @optional
+ */
+ repeated string point_names = 2;
+
+
+ /*
+ UUIDs of Endpoints to subscribe to MeasOverrides for.
+
+ @optional
+ */
+ repeated ModelUUID endpoint_uuids = 3;
+}
+
+
+message GetOverridesRequest {
+ optional EntityKeySet request = 1;
+}
+message GetOverridesResponse {
+ repeated MeasOverride results = 1;
+}
+message SubscribeOverridesRequest {
+ optional OverrideSubscriptionQuery query = 1;
+}
+message SubscribeOverridesResponse {
+ repeated MeasOverride results = 1;
+}
+message PutOverridesRequest {
+ repeated MeasOverride overrides = 1;
+}
+message PutOverridesResponse {
+ repeated MeasOverride results = 1;
+}
+message DeleteOverridesRequest {
+ repeated ModelUUID point_uuids = 1;
+}
+message DeleteOverridesResponse {
+ repeated MeasOverride results = 1;
+}
+
+
+/*
+ Service for configuring the behavior of the measurement processor.
+
+ MeasOverride objects provide the ability to block new Measurements from being published, and to optionally
+ replace the latest value provided by the front-end with a user-specified override value. Blocked (not in service)
+ Measurements will not be stored in the current value and historian stores, and will not be published to the
+ Measurement notification stream.
+
+ TriggerSet objects describe the transformations and event generation to be performed on Measurements in the
+ measurement processor before they are published to the stores and notification stream.
+*/
+service ProcessingService {
+ option (scala_package) = "io.greenbus.client.service";
+ option (java_package) = "io.greenbus.japi.client.service";
+
+ /*
+ Returns a set of MeasOverrides associated with Points specified by an EntityKeySet.
+ */
+ rpc get_overrides(GetOverridesRequest) returns (GetOverridesResponse);
+
+ /*
+ Creates or modifies the MeasOverrides associated with Points.
+
+ Only one MeasOverride can exist for any given Point. Putting a new value will replace a previous one.
+ */
+ rpc put_overrides(PutOverridesRequest) returns (PutOverridesResponse);
+
+ /*
+ Delete MeasOverrides associated with Points by specifying the Point UUIDs.
+ */
+ rpc delete_overrides(DeleteOverridesRequest) returns (DeleteOverridesResponse);
+
+ /*
+ Subscribes to notifications for changes to MeasOverrides by either Point or Endpoint.
+ */
+ rpc subscribe_to_overrides(SubscribeOverridesRequest) returns (SubscribeOverridesResponse) {
+ option (subscription_type) = "greenbus.client.OverrideNotification";
+ }
+}
diff --git a/client/src/main/resources/Version.scala.template b/client/src/main/resources/Version.scala.template
new file mode 100755
index 0000000..615beca
--- /dev/null
+++ b/client/src/main/resources/Version.scala.template
@@ -0,0 +1,31 @@
+/**
+ * Copyright 2011 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.client.version
+
+object Version {
+
+ def projectVersion = "PROJECT_VERSION"
+
+ def gitCommitId = "GIT_COMMIT_ID"
+
+ def isSnapshot = projectVersion.contains("SNAPSHOT")
+
+ def clientVersion = if(isSnapshot) projectVersion + "-" + gitCommitId.slice(0,8) else projectVersion
+
+}
diff --git a/client/src/main/scala/io/greenbus/client/ServiceConnection.scala b/client/src/main/scala/io/greenbus/client/ServiceConnection.scala
new file mode 100644
index 0000000..bdfaee8
--- /dev/null
+++ b/client/src/main/scala/io/greenbus/client/ServiceConnection.scala
@@ -0,0 +1,98 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.client
+
+import io.greenbus.msg.amqp._
+import io.greenbus.msg.Session
+import scala.concurrent.{ ExecutionContext, Await, Future }
+import java.util.concurrent.{ ExecutorService, Executors }
+import io.greenbus.msg.util.Scheduler
+import scala.concurrent.duration.Duration
+import io.greenbus.client.service.proto.LoginRequests.LoginRequest
+import io.greenbus.client.service.LoginService
+
+object ServiceHeaders {
+ def tokenHeader() = "AUTH_TOKEN"
+}
+
+object ServiceConnection {
+
+ def connect(settings: AmqpSettings, broker: AmqpBroker, timeoutMs: Long): ServiceConnection = {
+ val exe = Executors.newScheduledThreadPool(5)
+ val factory = new AmqpConnectionFactory(settings, broker, timeoutMs, Scheduler(exe))
+ new DefaultConnection(factory.connect, exe)
+ }
+
+ private class DefaultConnection(conn: AmqpConnection, exe: ExecutorService) extends ServiceConnection {
+
+ def login(user: String, password: String): Future[Session] = {
+ import ExecutionContext.Implicits.global
+ val sess = session
+ val loginClient = LoginService.client(sess)
+
+ val loginRequest = LoginRequest.newBuilder()
+ .setName(user)
+ .setPassword(password)
+ .setLoginLocation("")
+ .setClientVersion(version.Version.clientVersion)
+ .build()
+
+ loginClient.login(loginRequest).map { result =>
+ sess.addHeader(ServiceHeaders.tokenHeader(), result.getToken)
+ sess
+ }
+ }
+ def login(user: String, password: String, timeout: Duration): Session = {
+ Await.result(login(user, password), timeout)
+ }
+
+ def session: Session = conn.createSession(ServiceMessagingCodec.codec)
+
+ def serviceOperations: AmqpServiceOperations = conn.serviceOperations
+
+ def disconnect() {
+ conn.disconnect()
+ exe.shutdown()
+ }
+
+ def addConnectionListener(listener: (Boolean) => Unit) {
+ conn.addConnectionListener(listener)
+ }
+
+ def removeConnectionListener(listener: (Boolean) => Unit) {
+ conn.removeConnectionListener(listener)
+ }
+ }
+
+}
+
+trait ServiceConnection {
+
+ def addConnectionListener(listener: Boolean => Unit)
+ def removeConnectionListener(listener: Boolean => Unit)
+
+ def login(user: String, password: String): Future[Session]
+ def login(user: String, password: String, duration: Duration): Session
+
+ def session: Session
+
+ def serviceOperations: AmqpServiceOperations
+
+ def disconnect()
+}
diff --git a/client/src/main/scala/io/greenbus/client/ServiceMessagingCodec.scala b/client/src/main/scala/io/greenbus/client/ServiceMessagingCodec.scala
new file mode 100644
index 0000000..559d406
--- /dev/null
+++ b/client/src/main/scala/io/greenbus/client/ServiceMessagingCodec.scala
@@ -0,0 +1,77 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.client
+
+import com.google.protobuf.ByteString
+import scala.util.{ Success, Failure, Try }
+import io.greenbus.msg.RequestMessagingCodec
+import io.greenbus.client.proto.Envelope.{ ServiceResponse, RequestHeader, ServiceRequest }
+import io.greenbus.client.exception.{ RequestException, MalformedResponseException, StatusCodes }
+
+object ServiceMessagingCodec {
+
+ def codec: RequestMessagingCodec = FullCodec
+
+ private object FullCodec extends RequestMessagingCodec {
+
+ private def requestBuilder(requestId: String, headers: Map[String, String], payload: Array[Byte]): ServiceRequest.Builder = {
+ val builder = ServiceRequest.newBuilder
+ .setPayload(ByteString.copyFrom(payload))
+
+ headers.foreach { case (key, v) => builder.addHeaders(RequestHeader.newBuilder.setKey(key).setValue(v)) }
+
+ builder
+ }
+
+ def encode(requestId: String, headers: Map[String, String], payload: Array[Byte]): Array[Byte] = {
+ val requestEnv = requestBuilder(requestId, headers, payload)
+ requestEnv.build().toByteArray
+ }
+
+ def encodeSubscription(requestId: String, headers: Map[String, String], payload: Array[Byte], subscriptionId: String): Array[Byte] = {
+ val requestEnv = requestBuilder(requestId, headers, payload)
+ requestEnv.addHeaders(
+ RequestHeader.newBuilder
+ .setKey("__subscription")
+ .setValue(subscriptionId))
+
+ requestEnv.build().toByteArray
+ }
+
+ def decodeAndProcess(bytes: Array[Byte]): Try[Array[Byte]] = {
+ try {
+ val response = ServiceResponse.parseFrom(bytes)
+
+ if (!response.hasStatus) {
+ Failure(new MalformedResponseException("Service response did not include status"))
+ } else {
+ if (StatusCodes.isSuccess(response.getStatus)) {
+ Success(response.getPayload.toByteArray)
+ } else {
+ Failure(StatusCodes.toException(response.getStatus, response.getErrorMessage))
+ }
+ }
+ } catch {
+ case ex: Throwable =>
+ Failure(new RequestException("Couldn't parse response envelope", ex))
+ }
+ }
+
+ }
+}
diff --git a/client/src/main/scala/io/greenbus/client/exception/ServiceException.scala b/client/src/main/scala/io/greenbus/client/exception/ServiceException.scala
new file mode 100644
index 0000000..5f6e106
--- /dev/null
+++ b/client/src/main/scala/io/greenbus/client/exception/ServiceException.scala
@@ -0,0 +1,80 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.client.exception
+
+import io.greenbus.client.proto.Envelope
+
+class RequestException(message: String, cause: Throwable) extends Exception(message, cause) {
+ def this(message: String) {
+ this(message, null)
+ }
+}
+
+class MalformedResponseException(message: String) extends RequestException(message)
+
+class ServiceException(message: String, status: Envelope.Status, cause: Throwable) extends RequestException(message, cause) {
+ def this(message: String, status: Envelope.Status) = {
+ this(message, status, null)
+ }
+
+ def getStatus: Envelope.Status = status
+}
+
+class ReplyException(message: String, status: Envelope.Status, cause: Throwable) extends ServiceException(message, status, cause) {
+ def this(message: String, status: Envelope.Status) = {
+ this(message, status, null)
+ }
+}
+
+class BadRequestException(message: String) extends ReplyException(message, Envelope.Status.BAD_REQUEST)
+
+class InternalServiceException(message: String) extends ServiceException(message, Envelope.Status.INTERNAL_ERROR)
+
+class UnauthorizedException(message: String) extends ServiceException(message, Envelope.Status.UNAUTHORIZED)
+
+class ForbiddenException(message: String) extends ServiceException(message, Envelope.Status.FORBIDDEN)
+
+class LockedException(message: String) extends ServiceException(message, Envelope.Status.LOCKED)
+
+class BusUnavailableException(message: String) extends ServiceException(message, Envelope.Status.BUS_UNAVAILABLE)
+
+object StatusCodes {
+
+ def isSuccess(status: Envelope.Status): Boolean = {
+ status match {
+ case Envelope.Status.OK => true
+ case Envelope.Status.CREATED => true
+ case Envelope.Status.UPDATED => true
+ case Envelope.Status.DELETED => true
+ case Envelope.Status.NOT_MODIFIED => true
+ case _ => false
+ }
+ }
+
+ def toException(status: Envelope.Status, error: String): ServiceException = status match {
+ case Envelope.Status.BAD_REQUEST => new BadRequestException(error)
+ case Envelope.Status.UNAUTHORIZED => new UnauthorizedException(error)
+ case Envelope.Status.FORBIDDEN => new ForbiddenException(error)
+ case Envelope.Status.INTERNAL_ERROR => new InternalServiceException(error)
+ case Envelope.Status.LOCKED => new LockedException(error)
+ case Envelope.Status.BUS_UNAVAILABLE => new BusUnavailableException(error)
+ case _ => new ServiceException(error, status)
+ }
+
+}
\ No newline at end of file
diff --git a/init_postgres.sql b/init_postgres.sql
new file mode 100644
index 0000000..9e35db3
--- /dev/null
+++ b/init_postgres.sql
@@ -0,0 +1,6 @@
+CREATE USER core WITH PASSWORD 'core';
+CREATE DATABASE greenbus3_d;
+CREATE DATABASE greenbus3_t;
+GRANT ALL PRIVILEGES ON DATABASE greenbus3_d TO core;
+GRANT ALL PRIVILEGES ON DATABASE greenbus3_t TO core;
+
diff --git a/integration/pom.xml b/integration/pom.xml
new file mode 100755
index 0000000..bdc1719
--- /dev/null
+++ b/integration/pom.xml
@@ -0,0 +1,81 @@
+
+ 4.0.0
+
+
+ io.greenbus
+ greenbus-scala-base
+ 3.0.0
+ ../scala-base
+
+
+ greenbus-integration
+ jar
+
+
+
+ AGPLv3
+ http://www.gnu.org/licenses/agpl-3.0.txt
+
+
+
+
+
+
+
+ com.mycila.maven-license-plugin
+ maven-license-plugin
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ ../
+
+
+
+
+
+
+
+ io.greenbus
+ greenbus-services
+ 3.0.0
+
+
+ io.greenbus
+ greenbus-processing
+ 3.0.0
+
+
+ io.greenbus
+ greenbus-loader-xml
+ 3.0.0
+
+
+ io.greenbus
+ greenbus-client
+ 3.0.0
+
+
+ io.greenbus
+ greenbus-util
+ 3.0.0
+
+
+ io.greenbus.msg
+ greenbus-msg-qpid
+ 1.0.0
+
+
+ com.typesafe.akka
+ akka-actor_2.10
+ 2.2.0
+
+
+
+
+
diff --git a/integration/src/main/scala/io/greenbus/integration/tools/EventWatcher.scala b/integration/src/main/scala/io/greenbus/integration/tools/EventWatcher.scala
new file mode 100644
index 0000000..197030f
--- /dev/null
+++ b/integration/src/main/scala/io/greenbus/integration/tools/EventWatcher.scala
@@ -0,0 +1,61 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.integration.tools
+
+import scala.concurrent.{ Future, Promise }
+
+class EventWatcher[A] {
+ private val mutex = new Object
+ private var updates = List.empty[A]
+ private var check = Option.empty[(List[A] => Boolean, Promise[List[A]])]
+
+ def reset() {
+ mutex.synchronized {
+ updates = Nil
+ }
+ }
+
+ def future(f: List[A] => Boolean): Future[List[A]] = {
+ mutex.synchronized {
+ val promise = Promise[List[A]]()
+ check = Some((f, promise))
+ promise.future
+ }
+ }
+
+ def currentUpdates(): List[A] = {
+ mutex.synchronized {
+ updates.reverse
+ }
+ }
+
+ def enqueue(a: A) {
+ mutex.synchronized {
+ updates ::= a
+ check.foreach {
+ case (fun, promise) =>
+ val reversed = updates.reverse
+ if (fun(reversed)) {
+ check = None
+ promise.success(reversed)
+ }
+ }
+ }
+ }
+}
diff --git a/integration/src/main/scala/io/greenbus/integration/tools/PollingUtils.scala b/integration/src/main/scala/io/greenbus/integration/tools/PollingUtils.scala
new file mode 100644
index 0000000..1aa3f56
--- /dev/null
+++ b/integration/src/main/scala/io/greenbus/integration/tools/PollingUtils.scala
@@ -0,0 +1,66 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.integration.tools
+
+import scala.annotation.tailrec
+import java.util.concurrent.TimeoutException
+import com.typesafe.scalalogging.slf4j.Logging
+
+object PollingUtils extends Logging {
+
+ def pollForSuccess[A](delayMs: Long, timeoutMs: Long)(f: => A): A = {
+ pollForSuccessWithIter(delayMs, timeoutMs) { _ => f }
+ }
+
+ def pollForSuccessWithIter[A](delayMs: Long, timeoutMs: Long)(f: Int => A): A = {
+ val start = System.currentTimeMillis()
+
+ @tailrec
+ def poll(i: Int): A = {
+ val now = System.currentTimeMillis()
+ if ((now - start) < timeoutMs) {
+
+ val resultOpt = try {
+ logger.info("Polling")
+ Some(f(i))
+ } catch {
+ case ex: Throwable =>
+ logger.info("Missed poll: " + ex)
+ None
+ }
+
+ resultOpt match {
+ case Some(result) => result
+ case None =>
+ val elapsedInPoll = System.currentTimeMillis() - now
+ if (elapsedInPoll >= delayMs) {
+ poll(i + 1)
+ } else {
+ Thread.sleep(delayMs - elapsedInPoll)
+ poll(i + 1)
+ }
+ }
+ } else {
+ throw new TimeoutException("Failed operation within time limit")
+ }
+ }
+
+ poll(1)
+ }
+}
diff --git a/integration/src/main/scala/io/greenbus/integration/tools/ServiceWatcher.scala b/integration/src/main/scala/io/greenbus/integration/tools/ServiceWatcher.scala
new file mode 100644
index 0000000..c194ff8
--- /dev/null
+++ b/integration/src/main/scala/io/greenbus/integration/tools/ServiceWatcher.scala
@@ -0,0 +1,80 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.integration.tools
+
+import io.greenbus.msg.{ Subscription, Session }
+import io.greenbus.client.service.proto.FrontEnd.{ FrontEndConnectionStatusNotification, FrontEndConnectionStatus }
+import io.greenbus.client.service.proto.Model.ModelUUID
+import io.greenbus.client.service.{ FrontEndService, MeasurementService, ModelService }
+import io.greenbus.client.service.proto.ModelRequests.EntityRelationshipFlatQuery
+import scala.concurrent.Await
+import scala.concurrent.duration._
+import io.greenbus.client.service.proto.Measurements.{ MeasurementNotification, PointMeasurementValue }
+import io.greenbus.client.service.proto.ModelRequests.EndpointSubscriptionQuery
+
+object ServiceWatcher {
+ def getOwnedPoints(session: Session, owner: String): Seq[ModelUUID] = {
+ val modelClient = ModelService.client(session)
+
+ val relationQuery =
+ EntityRelationshipFlatQuery.newBuilder()
+ .addStartNames(owner)
+ .setDescendantOf(true)
+ .setRelationship("owns")
+ .addEndTypes("Point")
+ .build()
+
+ val pointEntities = Await.result(modelClient.relationshipFlatQuery(relationQuery), 5000.milliseconds)
+
+ pointEntities.map(_.getUuid)
+ }
+
+ def measurementWatcherForOwner(session: Session, owner: String): (Seq[PointMeasurementValue], ServiceWatcher[MeasurementNotification]) = {
+ val pointUuids = getOwnedPoints(session, owner)
+ val measClient = MeasurementService.client(session)
+
+ val subFut = measClient.getCurrentValuesAndSubscribe(pointUuids)
+
+ val (originals, sub) = Await.result(subFut, 5000.milliseconds)
+
+ (originals, new ServiceWatcher(sub))
+ }
+
+ def statusWatcher(session: Session, endpointName: String): (Seq[FrontEndConnectionStatus], ServiceWatcher[FrontEndConnectionStatusNotification]) = {
+ val client = FrontEndService.client(session)
+
+ val (current, sub) = Await.result(client.subscribeToFrontEndConnectionStatuses(EndpointSubscriptionQuery.newBuilder().addNames(endpointName).build()), 5000.milliseconds)
+
+ (current, new ServiceWatcher(sub))
+ }
+
+}
+
+class ServiceWatcher[A](subscription: Subscription[A]) {
+
+ val watcher = new EventWatcher[A]
+
+ subscription.start { subNot => watcher.enqueue(subNot) }
+
+ def cancel() {
+ subscription.cancel()
+ }
+
+}
+
diff --git a/integration/src/test/scala/io/greenbus/integration/FailoverTests.scala b/integration/src/test/scala/io/greenbus/integration/FailoverTests.scala
new file mode 100644
index 0000000..e63f1c7
--- /dev/null
+++ b/integration/src/test/scala/io/greenbus/integration/FailoverTests.scala
@@ -0,0 +1,352 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.integration
+
+import java.util.concurrent.TimeoutException
+
+import akka.actor.{ ActorRef, ActorSystem, PoisonPill }
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.client.ServiceConnection
+import io.greenbus.client.exception.LockedException
+import io.greenbus.client.service.proto.FrontEnd.FrontEndConnectionStatus
+import io.greenbus.client.service.proto.FrontEndRequests.{ FrontEndRegistrationTemplate, FrontEndStatusUpdate }
+import io.greenbus.client.service.proto.Measurements.Quality
+import io.greenbus.client.service.proto.Model.{ Endpoint, ModelUUID }
+import io.greenbus.client.service.proto.ModelRequests.{ EndpointQuery, EntityKeySet }
+import io.greenbus.client.service.{ EventService, FrontEndService, MeasurementService, ModelService }
+import io.greenbus.integration.IntegrationConfig._
+import io.greenbus.integration.tools.PollingUtils._
+import io.greenbus.measproc.MeasurementProcessor
+import io.greenbus.msg.Session
+import io.greenbus.msg.amqp.AmqpSettings
+import io.greenbus.msg.qpid.QpidBroker
+import io.greenbus.services.{ ServiceManager, CoreServices, ResetDatabase }
+import io.greenbus.util.UserSettings
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.matchers.ShouldMatchers
+import org.scalatest.{ BeforeAndAfterAll, FunSuite }
+
+import scala.concurrent.Await
+import scala.concurrent.duration._
+
+@RunWith(classOf[JUnitRunner])
+class FailoverTests extends FunSuite with ShouldMatchers with Logging with BeforeAndAfterAll {
+ import IntegrationHelpers._
+
+ val testConfigPath = "io.greenbus.test.cfg"
+ val system = ActorSystem("failoverTest")
+ var services = Option.empty[ActorRef]
+ var processor = Option.empty[ActorRef]
+ var processor2 = Option.empty[ActorRef]
+ var conn = Option.empty[ServiceConnection]
+ var session = Option.empty[Session]
+
+ var pointAUuid = Option.empty[ModelUUID]
+ var endpoint1 = Option.empty[Endpoint]
+ var endpoint1Address1 = Option.empty[String]
+ var endpoint1Address2 = Option.empty[String]
+ var endpoint1Address3 = Option.empty[String]
+
+ override protected def beforeAll(): Unit = {
+ ResetDatabase.reset(testConfigPath)
+
+ logger.info("starting services")
+ services = Some(system.actorOf(ServiceManager.props(testConfigPath, testConfigPath, CoreServices.runServices)))
+
+ val amqpConfig = AmqpSettings.load(testConfigPath)
+ val conn = ServiceConnection.connect(amqpConfig, QpidBroker, 5000)
+ this.conn = Some(conn)
+
+ val userConfig = UserSettings.load(testConfigPath)
+
+ val session = pollForSuccess(500, 5000) {
+ Await.result(conn.login(userConfig.user, userConfig.password), 500.milliseconds)
+ }
+
+ this.session = Some(session)
+
+ IntegrationConfig.loadActions(buildConfig("Set1"), session)
+
+ logger.info("starting processor")
+ processor = Some(system.actorOf(MeasurementProcessor.buildProcessor(testConfigPath, testConfigPath, testConfigPath, 1000, "testNode",
+ standbyLockRetryPeriodMs = 500,
+ standbyLockExpiryDurationMs = 1000)))
+
+ val eventClient = EventService.client(session)
+
+ Await.result(eventClient.putEventConfigs(eventConfigs()), 5000.milliseconds)
+ }
+
+ override protected def afterAll(): Unit = {
+ system.shutdown()
+ system.awaitTermination()
+ this.conn.get.disconnect()
+ }
+
+ test("registration") {
+
+ val session = this.session.get
+
+ val modelClient = ModelService.client(session)
+ val frontEndClient = FrontEndService.client(session)
+
+ val points = Await.result(modelClient.getPoints(EntityKeySet.newBuilder().addNames("Set1PointA").addNames("Set1PointB").build()), 5000.milliseconds)
+ points.size should equal(2)
+ val pointA = points.find(_.getName == "Set1PointA").get
+ pointAUuid = Some(pointA.getUuid)
+ val pointB = points.find(_.getName == "Set1PointB").get
+
+ val endpoints = Await.result(modelClient.endpointQuery(EndpointQuery.newBuilder().addProtocols("test").build()), 5000.milliseconds)
+
+ endpoints.size should equal(1)
+ val endpoint = endpoints.head
+
+ // let the measproc do its config, just a chance to avoid 2s wasted in the next step
+ Thread.sleep(500)
+
+ val reg = FrontEndRegistrationTemplate.newBuilder()
+ .setEndpointUuid(endpoint.getUuid)
+ .build()
+
+ val registration = retryFutureUntilSuccess(3, 2000) {
+ frontEndClient.putFrontEndRegistration(reg, endpoint.getUuid.getValue)
+ }
+
+ // FIRST MEASPROC IS UP, START SECOND
+ processor2 = Some(system.actorOf(MeasurementProcessor.buildProcessor(testConfigPath, testConfigPath, testConfigPath, 1000, "testNode2",
+ standbyLockRetryPeriodMs = 500,
+ standbyLockExpiryDurationMs = 1000)))
+ // -- CONTINUE
+
+ registration.getEndpointUuid should equal(endpoint.getUuid)
+
+ val address = registration.getInputAddress
+ this.endpoint1 = Some(endpoint)
+ this.endpoint1Address1 = Some(address)
+
+ val measClient = MeasurementService.client(session)
+
+ val original = Await.result(measClient.getCurrentValues(List(pointA.getUuid)), 5000.milliseconds)
+ original.size should equal(0)
+
+ val postTime = System.currentTimeMillis()
+ postAndGet(session, pointA.getUuid, analogBatch(pointA.getUuid, 1.45, postTime), address)
+ }
+
+ test("second registration fails") {
+ val session = this.session.get
+ val endpointUuid = this.endpoint1.get.getUuid
+
+ val frontEndClient = FrontEndService.client(session)
+
+ val reg = FrontEndRegistrationTemplate.newBuilder()
+ .setEndpointUuid(endpointUuid)
+ .setFepNodeId("node2")
+ .build()
+
+ val regFut = frontEndClient.putFrontEndRegistration(reg, endpointUuid.getValue)
+
+ intercept[LockedException] {
+ Await.result(regFut, 5000.milliseconds)
+ }
+ }
+
+ private def checkMarkedOffline(v: Double): Unit = {
+ checkMarked(v, Quality.Validity.INVALID)
+ }
+ private def checkMarkedOnline(v: Double): Unit = {
+ checkMarked(v, Quality.Validity.GOOD)
+ }
+
+ private def checkMarked(v: Double, q: Quality.Validity): Unit = {
+ val session = this.session.get
+ val measClient = MeasurementService.client(session)
+
+ val markedOffline = Await.result(measClient.getCurrentValues(List(pointAUuid.get)), 5000.milliseconds)
+ markedOffline.size should equal(1)
+ checkAnalog(markedOffline.head.getValue, v, None, q)
+ }
+
+ test("meas marked offline") {
+ val session = this.session.get
+ val pointAUuid = this.pointAUuid.get
+
+ val measClient = MeasurementService.client(session)
+
+ retryUntilSuccess(10, 500) {
+ checkMarkedOffline(1.45)
+ }
+ }
+
+ test("meas publish brings endpoint back online") {
+ val session = this.session.get
+ val pointAUuid = this.pointAUuid.get
+ val endpoint1Address = this.endpoint1Address1.get
+
+ val postTime = System.currentTimeMillis()
+ postAndGet(session, pointAUuid, analogBatch(pointAUuid, 1.8, postTime), endpoint1Address)
+ }
+
+ test("go down again; status publish brings back online") {
+ val session = this.session.get
+ val endpointUuid = this.endpoint1.get.getUuid
+ val endpoint1Address = this.endpoint1Address1.get
+
+ val frontEndClient = FrontEndService.client(session)
+
+ retryUntilSuccess(10, 500) {
+ checkMarkedOffline(1.8)
+ }
+
+ val status = FrontEndStatusUpdate.newBuilder().setEndpointUuid(endpointUuid).setStatus(FrontEndConnectionStatus.Status.COMMS_UP).build()
+ val updateFut = frontEndClient.putFrontEndConnectionStatuses(Seq(status), endpoint1Address)
+ Await.result(updateFut, 5000.milliseconds)
+
+ checkMarkedOnline(1.8)
+ }
+
+ test("second endpoint logs in") {
+ val session = this.session.get
+ val endpointUuid = this.endpoint1.get.getUuid
+
+ val measClient = MeasurementService.client(session)
+ val frontEndClient = FrontEndService.client(session)
+
+ retryUntilSuccess(10, 500) {
+ checkMarkedOffline(1.8)
+ }
+
+ val reg = FrontEndRegistrationTemplate.newBuilder()
+ .setEndpointUuid(endpointUuid)
+ .setFepNodeId("node2")
+ .build()
+
+ val regFut = frontEndClient.putFrontEndRegistration(reg, endpointUuid.getValue)
+
+ val registration = Await.result(regFut, 5000.milliseconds)
+
+ registration.getEndpointUuid should equal(endpointUuid)
+
+ val address = registration.getInputAddress
+ this.endpoint1Address2 = Some(address)
+ }
+
+ test("original address is dead, can't re-register") {
+ val session = this.session.get
+ val endpointUuid = this.endpoint1.get.getUuid
+ val frontEndClient = FrontEndService.client(session)
+ val measClient = MeasurementService.client(session)
+
+ val status = FrontEndStatusUpdate.newBuilder().setEndpointUuid(endpointUuid).setStatus(FrontEndConnectionStatus.Status.COMMS_UP).build()
+ val updateFut = frontEndClient.putFrontEndConnectionStatuses(Seq(status), endpoint1Address1.get)
+
+ intercept[TimeoutException] {
+ Await.result(updateFut, 200.milliseconds)
+ }
+
+ val postTime = System.currentTimeMillis()
+ val measFut = measClient.postMeasurements(analogBatch(this.pointAUuid.get, 1.85, postTime), endpoint1Address1.get)
+ intercept[TimeoutException] {
+ Await.result(measFut, 200.milliseconds)
+ }
+
+ val reg = FrontEndRegistrationTemplate.newBuilder()
+ .setEndpointUuid(endpointUuid)
+ .build()
+
+ val regFut = frontEndClient.putFrontEndRegistration(reg, endpointUuid.getValue)
+
+ intercept[LockedException] {
+ Await.result(regFut, 5000.milliseconds)
+ }
+ }
+
+ test("id'ed re-registration succeeds immediately") {
+ val session = this.session.get
+ val endpointUuid = this.endpoint1.get.getUuid
+
+ // keep alive
+ val postTime = System.currentTimeMillis()
+ postAndGet(session, this.pointAUuid.get, analogBatch(this.pointAUuid.get, 1.2, postTime), this.endpoint1Address2.get)
+
+ val frontEndClient = FrontEndService.client(session)
+
+ val reg = FrontEndRegistrationTemplate.newBuilder()
+ .setEndpointUuid(endpointUuid)
+ .setFepNodeId("node2")
+ .build()
+
+ val regFut = frontEndClient.putFrontEndRegistration(reg, endpointUuid.getValue)
+
+ val registration = Await.result(regFut, 5000.milliseconds)
+
+ registration.getEndpointUuid should equal(endpointUuid)
+
+ postAndGet(session, this.pointAUuid.get, analogBatch(this.pointAUuid.get, 1.25, postTime), registration.getInputAddress)
+ this.endpoint1Address2 = Some(registration.getInputAddress)
+ }
+
+ test("second measproc") {
+ val session = this.session.get
+ val endpointUuid = this.endpoint1.get.getUuid
+ val frontEndClient = FrontEndService.client(session)
+
+ this.processor.get ! PoisonPill
+
+ var i = 0
+ var timedOut = false
+ while (i < 10 && !timedOut) {
+
+ val status = FrontEndStatusUpdate.newBuilder().setEndpointUuid(endpointUuid).setStatus(FrontEndConnectionStatus.Status.COMMS_UP).build()
+ val updateFut = frontEndClient.putFrontEndConnectionStatuses(Seq(status), this.endpoint1Address2.get)
+
+ try {
+ Await.result(updateFut, 300.milliseconds)
+ Thread.sleep(1000)
+ i += 1
+ } catch {
+ case ex: TimeoutException =>
+ timedOut = true
+ }
+ }
+
+ timedOut should equal(true)
+
+ val reg = FrontEndRegistrationTemplate.newBuilder()
+ .setEndpointUuid(endpointUuid)
+ .setFepNodeId("node2")
+ .build()
+
+ val registration = retryFutureUntilSuccess(3, 2000) {
+ frontEndClient.putFrontEndRegistration(reg, endpointUuid.getValue)
+ }
+
+ this.endpoint1Address3 = Some(registration.getInputAddress)
+
+ retryUntilSuccess(10, 500) {
+ checkMarked(1.25, Quality.Validity.INVALID)
+ }
+
+ val postTime = System.currentTimeMillis()
+ postAndGet(session, this.pointAUuid.get, analogBatch(this.pointAUuid.get, 1.11, postTime), endpoint1Address3.get)
+
+ }
+}
+
diff --git a/integration/src/test/scala/io/greenbus/integration/IntegrationConfig.scala b/integration/src/test/scala/io/greenbus/integration/IntegrationConfig.scala
new file mode 100644
index 0000000..15cb755
--- /dev/null
+++ b/integration/src/test/scala/io/greenbus/integration/IntegrationConfig.scala
@@ -0,0 +1,170 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.integration
+
+import io.greenbus.client.service.proto.EventRequests.EventConfigTemplate
+import io.greenbus.client.service.proto.Events.EventConfig
+import io.greenbus.client.service.proto.Measurements.Quality
+import io.greenbus.client.service.proto.Processing._
+import io.greenbus.client.service.proto.{ Model, Processing }
+import io.greenbus.loader.set.Actions._
+import io.greenbus.loader.set.Mdl.EdgeDesc
+import io.greenbus.loader.set.{ NamedEntId, ByteArrayValue, Upload }
+import io.greenbus.msg.Session
+
+object IntegrationConfig {
+
+ def buildAnalogPoint(prefix: String, cache: ActionCache): String = {
+
+ val name = prefix + "PointA"
+ cache.pointPuts += PutPoint(None, name, Set("TypeA"), Model.PointCategory.ANALOG, "unit1")
+
+ val rangeTrigger = Trigger.newBuilder()
+ .setAnalogLimit(AnalogLimit.newBuilder()
+ .setUpperLimit(2.0d)
+ .setLowerLimit(1.0d)
+ .build())
+ .addActions(Action.newBuilder()
+ .setActionName("WentOutOfRange")
+ .setType(Processing.ActivationType.RISING)
+ .setEvent(EventGeneration.newBuilder()
+ .setEventType("OutOfRangeEvent")))
+ .addActions(Action.newBuilder()
+ .setActionName("MakeItQuestionable")
+ .setType(Processing.ActivationType.RISING)
+ .setQualityAnnotation(Quality.newBuilder()
+ .setValidity(Quality.Validity.QUESTIONABLE)))
+ .addActions(Action.newBuilder()
+ .setActionName("WentBackInRange")
+ .setType(Processing.ActivationType.FALLING)
+ .setEvent(EventGeneration.newBuilder()
+ .setEventType("BackInRangeEvent")))
+ .build()
+
+ val ts = TriggerSet.newBuilder().addTriggers(rangeTrigger).build()
+
+ cache.keyValuePutByNames += PutKeyValueByName(name, "triggerSet", ByteArrayValue(ts.toByteArray))
+
+ name
+ }
+
+ def buildStatusPoint(prefix: String, cache: ActionCache): String = {
+
+ val name = prefix + "PointB"
+ cache.pointPuts += PutPoint(None, name, Set("TypeA"), Model.PointCategory.STATUS, "unit1")
+
+ val rangeTrigger = Trigger.newBuilder()
+ .setBoolValue(false)
+ .addActions(Action.newBuilder()
+ .setActionName("WentAbnormal")
+ .setType(Processing.ActivationType.RISING)
+ .setEvent(EventGeneration.newBuilder()
+ .setEventType("AbnormalEvent")))
+ .build()
+
+ val ts = TriggerSet.newBuilder().addTriggers(rangeTrigger).build()
+
+ cache.keyValuePutByNames += PutKeyValueByName(name, "triggerSet", ByteArrayValue(ts.toByteArray))
+
+ name
+ }
+
+ def eventConfigs(): Seq[EventConfigTemplate] = {
+
+ List(EventConfigTemplate.newBuilder()
+ .setEventType("OutOfRangeEvent")
+ .setDesignation(EventConfig.Designation.EVENT)
+ .setSeverity(6)
+ .setResource("out of range")
+ .build(),
+ EventConfigTemplate.newBuilder()
+ .setEventType("AbnormalEvent")
+ .setDesignation(EventConfig.Designation.EVENT)
+ .setSeverity(7)
+ .setResource("abnormal")
+ .build())
+ }
+
+ def loadActions(actionSet: ActionsList, session: Session): Unit = {
+ Upload.push(session, actionSet, Seq())
+ }
+
+ def buildConfig(prefix: String): ActionsList = {
+
+ val cache = new ActionCache
+
+ val pointAName = buildAnalogPoint(prefix, cache)
+ val pointBName = buildStatusPoint(prefix, cache)
+
+ val endpointName = prefix + "Endpoint"
+ cache.endpointPuts += PutEndpoint(None, endpointName, Set(), "test")
+
+ cache.edgePuts += PutEdge(EdgeDesc(NamedEntId(endpointName), "source", NamedEntId(pointAName)))
+ cache.edgePuts += PutEdge(EdgeDesc(NamedEntId(endpointName), "source", NamedEntId(pointBName)))
+
+ cache.result()
+ }
+
+ def buildFilterTrigger(): TriggerSet = {
+ TriggerSet.newBuilder()
+ .addTriggers(Trigger.newBuilder()
+ .setFilter(Filter.newBuilder().setType(Filter.FilterType.DUPLICATES_ONLY))
+ .addActions(Action.newBuilder()
+ .setActionName("suppress")
+ .setSuppress(true)
+ .setType(Processing.ActivationType.LOW)
+ .build()))
+ .build()
+ }
+
+ def buildOffsetTrigger(offset: Double): TriggerSet = {
+ TriggerSet.newBuilder()
+ .addTriggers(Trigger.newBuilder()
+ .setAnalogLimit(AnalogLimit.newBuilder()
+ .setLowerLimit(1.2d)
+ .build())
+ .addActions(Action.newBuilder()
+ .setActionName("offsetaction")
+ .setType(Processing.ActivationType.HIGH)
+ .setLinearTransform(
+ LinearTransform.newBuilder()
+ .setScale(1.0)
+ .setOffset(offset)
+ .build())
+ .build())
+ .build())
+ .build()
+ }
+
+ def buildUnconditionalOffsetTrigger(offset: Double): TriggerSet = {
+ TriggerSet.newBuilder()
+ .addTriggers(Trigger.newBuilder()
+ .addActions(Action.newBuilder()
+ .setActionName("offsetaction2")
+ .setType(Processing.ActivationType.HIGH)
+ .setLinearTransform(
+ LinearTransform.newBuilder()
+ .setScale(1.0)
+ .setOffset(offset)
+ .build())
+ .build())
+ .build())
+ .build()
+ }
+}
diff --git a/integration/src/test/scala/io/greenbus/integration/IntegrationHelpers.scala b/integration/src/test/scala/io/greenbus/integration/IntegrationHelpers.scala
new file mode 100644
index 0000000..745d905
--- /dev/null
+++ b/integration/src/test/scala/io/greenbus/integration/IntegrationHelpers.scala
@@ -0,0 +1,137 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.integration
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.client.service.MeasurementService
+import io.greenbus.client.service.proto.Measurements.{ Measurement, MeasurementBatch, PointMeasurementValue, Quality }
+import io.greenbus.client.service.proto.Model.ModelUUID
+import io.greenbus.client.service.proto.ModelRequests.EndpointDisabledUpdate
+import io.greenbus.client.service.proto.Processing
+import io.greenbus.client.service.proto.Processing._
+import io.greenbus.msg.Session
+import org.scalatest.matchers.ShouldMatchers
+
+import scala.annotation.tailrec
+import scala.concurrent._
+import scala.concurrent.duration._
+
+object IntegrationHelpers extends ShouldMatchers with Logging {
+
+ def analogBatch(uuid: ModelUUID, v: Double, time: Long): MeasurementBatch = {
+ MeasurementBatch.newBuilder()
+ .addPointMeasurements(PointMeasurementValue.newBuilder()
+ .setPointUuid(uuid)
+ .setValue(analogMeas(v, time)))
+ .build()
+ }
+
+ def analogMeas(v: Double, time: Long): Measurement = {
+ Measurement.newBuilder()
+ .setDoubleVal(v)
+ .setType(Measurement.Type.DOUBLE)
+ .setTime(time)
+ .build()
+ }
+
+ def checkAnalog(m: Measurement, v: Double, time: Option[Long], qual: Quality.Validity = Quality.Validity.GOOD) {
+ m.hasDoubleVal should equal(true)
+ m.getDoubleVal should equal(v)
+ m.getType should equal(Measurement.Type.DOUBLE)
+ time.foreach(t => m.getTime should equal(t))
+ m.getQuality.getValidity should equal(qual)
+ }
+
+ def postAndGet(session: Session, point: ModelUUID, batch: MeasurementBatch, address: String, time: Long = 5000): Measurement = {
+ val measClient = MeasurementService.client(session)
+ val postResult = Await.result(measClient.postMeasurements(batch, address), time.milliseconds)
+ postResult should equal(true)
+
+ val getResult = Await.result(measClient.getCurrentValues(List(point)), time.milliseconds)
+ getResult.size should equal(1)
+
+ getResult.head.getValue
+ }
+
+ def endUpdate(uuid: ModelUUID, disabled: Boolean) = EndpointDisabledUpdate.newBuilder().setEndpointUuid(uuid).setDisabled(disabled).build()
+
+ @tailrec
+ def retryFutureUntilSuccess[A](times: Int, wait: Long)(f: => Future[A]): A = {
+ if (times == 0) {
+ throw new RuntimeException("Failed too many times")
+ }
+ try {
+ Await.result(f, wait.milliseconds)
+ } catch {
+ case ex: Throwable =>
+ logger.warn("Request failure: " + ex)
+ retryFutureUntilSuccess(times - 1, wait)(f)
+ }
+ }
+
+ @tailrec
+ def retryUntilSuccess[A](times: Int, wait: Long)(f: => A): A = {
+ if (times == 0) {
+ throw new RuntimeException("Failed too many times")
+ }
+ try {
+ f
+ } catch {
+ case ex: Throwable =>
+ logger.warn("Attempt failure: " + ex)
+ Thread.sleep(wait)
+ retryUntilSuccess(times - 1, wait)(f)
+ }
+ }
+}
+
+class TypedEventQueue[A] {
+
+ private var queue = Seq.empty[A]
+ private var chk = Option.empty[(Promise[Seq[A]], Seq[A] => Boolean)]
+
+ private val mutex = new Object
+
+ def received(obj: A): Unit = {
+ mutex.synchronized {
+ queue = queue ++ Vector(obj)
+ chk.foreach {
+ case (prom, check) =>
+ if (check(queue)) {
+ prom.success(queue)
+ chk = None
+ }
+ }
+ }
+ }
+
+ def listen(check: Seq[A] => Boolean): Future[Seq[A]] = {
+ mutex.synchronized {
+ val prom = promise[Seq[A]]
+
+ if (check(queue)) {
+ prom.success(queue)
+ } else {
+ chk = Some((prom, check))
+ }
+ prom.future
+
+ }
+ }
+}
diff --git a/integration/src/test/scala/io/greenbus/integration/ServiceAndProcessorTest.scala b/integration/src/test/scala/io/greenbus/integration/ServiceAndProcessorTest.scala
new file mode 100644
index 0000000..5913f54
--- /dev/null
+++ b/integration/src/test/scala/io/greenbus/integration/ServiceAndProcessorTest.scala
@@ -0,0 +1,698 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.integration
+
+import java.util.concurrent.TimeoutException
+
+import akka.actor.{ ActorRef, ActorSystem }
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.client.ServiceConnection
+import io.greenbus.client.service._
+import io.greenbus.client.service.proto.EventRequests.{ EventQuery, EventQueryParams }
+import io.greenbus.client.service.proto.FrontEndRequests.FrontEndRegistrationTemplate
+import io.greenbus.client.service.proto.MeasurementRequests.MeasurementBatchSubscriptionQuery
+import io.greenbus.client.service.proto.Measurements._
+import io.greenbus.client.service.proto.Model
+import io.greenbus.client.service.proto.Model._
+import io.greenbus.client.service.proto.ModelRequests.{ EntityEdgeDescriptor, EntityKeySet, EntityTemplate, _ }
+import io.greenbus.client.service.proto.Processing.MeasOverride
+import io.greenbus.measproc.MeasurementProcessor
+import io.greenbus.msg.Session
+import io.greenbus.msg.amqp.AmqpSettings
+import io.greenbus.msg.qpid.QpidBroker
+import io.greenbus.services.{ CoreServices, ResetDatabase, ServiceManager }
+import io.greenbus.util.UserSettings
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.matchers.ShouldMatchers
+import org.scalatest.{ BeforeAndAfterAll, FunSuite }
+
+import scala.collection.JavaConversions._
+import scala.concurrent.Await
+import scala.concurrent.duration._
+
+@RunWith(classOf[JUnitRunner])
+class ServiceAndProcessorTest extends FunSuite with ShouldMatchers with Logging with BeforeAndAfterAll {
+ import IntegrationHelpers._
+ import io.greenbus.integration.IntegrationConfig._
+ import io.greenbus.integration.tools.PollingUtils._
+
+ val testConfigPath = "io.greenbus.test.cfg"
+ val system = ActorSystem("integrationTest")
+ var services = Option.empty[ActorRef]
+ var processor = Option.empty[ActorRef]
+ var conn = Option.empty[ServiceConnection]
+ var session = Option.empty[Session]
+
+ var set1PointA = Option.empty[Point]
+ var set1PointB = Option.empty[Point]
+ var endpoint1 = Option.empty[Endpoint]
+ var endpoint1Address = Option.empty[String]
+ var endpoint2 = Option.empty[Endpoint]
+ var endpoint2Address = Option.empty[String]
+ var dynamicPoint = Option.empty[Point]
+
+ val measQueue = new TypedEventQueue[MeasurementNotification]
+ val batchQueue = new TypedEventQueue[MeasurementBatchNotification]
+
+ override protected def beforeAll(): Unit = {
+ ResetDatabase.reset(testConfigPath)
+
+ logger.info("starting services")
+ services = Some(system.actorOf(ServiceManager.props(testConfigPath, testConfigPath, CoreServices.runServices)))
+
+ val amqpConfig = AmqpSettings.load(testConfigPath)
+ val conn = ServiceConnection.connect(amqpConfig, QpidBroker, 5000)
+ this.conn = Some(conn)
+
+ val userConfig = UserSettings.load(testConfigPath)
+
+ val session = pollForSuccess(500, 5000) {
+ Await.result(conn.login(userConfig.user, userConfig.password), 500.milliseconds)
+ }
+
+ this.session = Some(session)
+
+ IntegrationConfig.loadActions(buildConfig("Set1"), session)
+
+ logger.info("starting processor")
+ processor = Some(system.actorOf(MeasurementProcessor.buildProcessor(testConfigPath, testConfigPath, testConfigPath, 10000, "testNode")))
+
+ val eventClient = EventService.client(session)
+
+ Await.result(eventClient.putEventConfigs(eventConfigs()), 5000.milliseconds)
+
+ val modelClient = ModelService.client(session)
+
+ val points = Await.result(modelClient.getPoints(EntityKeySet.newBuilder().addNames("Set1PointA").addNames("Set1PointB").build()), 5000.milliseconds)
+ points.size should equal(2)
+ val pointA = points.find(_.getName == "Set1PointA").get
+ val pointB = points.find(_.getName == "Set1PointB").get
+ this.set1PointA = Some(pointA)
+ this.set1PointB = Some(pointB)
+
+ val endpoints = Await.result(modelClient.endpointQuery(EndpointQuery.newBuilder().addProtocols("test").build()), 5000.milliseconds)
+
+ endpoints.size should equal(1)
+ val endpoint = endpoints.head
+
+ this.endpoint1 = Some(endpoint)
+
+ val measClient = MeasurementService.client(session)
+
+ val measSubFut = measClient.getCurrentValuesAndSubscribe(Seq(pointA.getUuid, pointB.getUuid))
+ val (_, measSub) = Await.result(measSubFut, 5000.milliseconds)
+ measSub.start { not => measQueue.received(not) }
+
+ val batchSubFut = measClient.subscribeToBatches(MeasurementBatchSubscriptionQuery.newBuilder().build())
+ val (_, batchSub) = Await.result(batchSubFut, 5000.milliseconds)
+ batchSub.start { not => /*println("BATCH: " + not);*/ batchQueue.received(not) }
+ }
+
+ override protected def afterAll(): Unit = {
+ system.shutdown()
+ system.awaitTermination()
+ this.conn.get.disconnect()
+ }
+
+ test("Simple meas publish") {
+ val session = this.session.get
+
+ val modelClient = ModelService.client(session)
+ val frontEndClient = FrontEndService.client(session)
+
+ val endpoint = endpoint1.get
+
+ // let the measproc do its config, just a chance to avoid 2s wasted in the next step
+ Thread.sleep(500)
+
+ val reg = FrontEndRegistrationTemplate.newBuilder()
+ .setEndpointUuid(endpoint.getUuid)
+ .build()
+
+ val registration = retryFutureUntilSuccess(3, 2000) {
+ frontEndClient.putFrontEndRegistration(reg, endpoint.getUuid.getValue)
+ }
+
+ registration.getEndpointUuid should equal(endpoint.getUuid)
+
+ val address = registration.getInputAddress
+ this.endpoint1Address = Some(address)
+
+ val measClient = MeasurementService.client(session)
+
+ val original = Await.result(measClient.getCurrentValues(List(set1PointA.get.getUuid)), 5000.milliseconds)
+ original.size should equal(0)
+
+ val measEventFut = measQueue.listen(_.nonEmpty)
+ val measBatchFut = batchQueue.listen(_.nonEmpty)
+
+ val postTime = 5
+ val posted = pollForSuccess(500, 5000) {
+ Await.result(measClient.postMeasurements(analogBatch(set1PointA.get.getUuid, 1.45, postTime), address), 500.milliseconds)
+ }
+
+ posted should equal(true)
+
+ val after = Await.result(measClient.getCurrentValues(List(set1PointA.get.getUuid)), 5000.milliseconds)
+ after.size should equal(1)
+ checkAnalog(after.head.getValue, 1.45, Some(postTime))
+
+ val measEvents = Await.result(measEventFut, 5000.milliseconds)
+ measEvents.size should equal(1)
+ checkAnalog(after.head.getValue, 1.45, Some(postTime))
+
+ val batchEvents = Await.result(measBatchFut, 5000.milliseconds)
+ batchEvents.size should equal(1)
+ batchEvents.head.getEndpointUuid should equal(endpoint.getUuid)
+ batchEvents.head.getValuesList.size should equal(1)
+ batchEvents.head.getValuesList.head.getPointUuid should equal(set1PointA.get.getUuid)
+ batchEvents.head.getValuesList.head.getPointName should equal(set1PointA.get.getName)
+ checkAnalog(batchEvents.head.getValuesList.head.getValue, 1.45, Some(postTime))
+ }
+
+ test("Post with nonexistent point") {
+ val session = this.session.get
+ val address = this.endpoint1Address.get
+ val measClient = MeasurementService.client(session)
+
+ def partialBatchTest(v: Double, time: Long, batch: MeasurementBatch): Unit = {
+ val posted = pollForSuccess(500, 5000) {
+ Await.result(measClient.postMeasurements(batch, address), 500.milliseconds)
+ }
+
+ posted should equal(true)
+
+ val after = Await.result(measClient.getCurrentValues(List(set1PointA.get.getUuid)), 5000.milliseconds)
+ after.size should equal(1)
+ checkAnalog(after.head.getValue, v, Some(time))
+ }
+
+ val nameBatch = MeasurementBatch.newBuilder()
+ .addNamedMeasurements(
+ NamedMeasurementValue.newBuilder()
+ .setPointName(set1PointA.get.getName)
+ .setValue(analogMeas(1.59, 6))
+ .build())
+ .addNamedMeasurements(
+ NamedMeasurementValue.newBuilder()
+ .setPointName("erroneousPoint")
+ .setValue(analogMeas(3.33, 6))
+ .build())
+ .build()
+
+ partialBatchTest(1.59, 6, nameBatch)
+
+ val postTime2 = 6
+ val batch2 = MeasurementBatch.newBuilder()
+ .addPointMeasurements(
+ PointMeasurementValue.newBuilder()
+ .setPointUuid(set1PointA.get.getUuid)
+ .setValue(analogMeas(1.63, postTime2))
+ .build())
+ .addPointMeasurements(
+ PointMeasurementValue.newBuilder()
+ .setPointUuid(ModelUUID.newBuilder().setValue("erroneousUuid").build())
+ .setValue(analogMeas(3.33, postTime2))
+ .build())
+ .build()
+
+ partialBatchTest(1.63, postTime2, batch2)
+
+ }
+
+ test("Event triggered") {
+ val session = this.session.get
+ val address = this.endpoint1Address.get
+
+ val modelClient = ModelService.client(session)
+ val points = Await.result(modelClient.getPoints(EntityKeySet.newBuilder().addNames("Set1PointA").addNames("Set1PointB").build()), 5000.milliseconds)
+ points.size should equal(2)
+ val pointA = points.find(_.getName == "Set1PointA").get
+
+ val postTime = 10
+ val measClient = MeasurementService.client(session)
+ val posted = Await.result(measClient.postMeasurements(analogBatch(pointA.getUuid, 2.5, postTime), address), 5000.milliseconds)
+
+ posted should equal(true)
+
+ val after = Await.result(measClient.getCurrentValues(List(pointA.getUuid)), 5000.milliseconds)
+ after.size should equal(1)
+
+ checkAnalog(after.head.getValue, 2.5, Some(postTime), Quality.Validity.QUESTIONABLE)
+
+ val eventClient = EventService.client(session)
+
+ val events = pollForSuccess(500, 5000) {
+ val evs = Await.result(eventClient.eventQuery(EventQuery.newBuilder().setQueryParams(EventQueryParams.newBuilder.addEventType("OutOfRangeEvent")).setPageSize(100).build()), 5000.milliseconds)
+ if (evs.nonEmpty) evs else throw new Exception("poll miss")
+ }
+ events.size should equal(1)
+ }
+
+ test("Endpoint added") {
+ val session = this.session.get
+
+ val modelClient = ModelService.client(session)
+ val frontEndClient = FrontEndService.client(session)
+
+ val endpointTemplate = EndpointTemplate.newBuilder()
+ .setEntityTemplate(
+ EntityTemplate.newBuilder()
+ .setName("Set2Endpoint")
+ .build())
+ .setProtocol("test2")
+ .build()
+
+ val ends = Await.result(modelClient.putEndpoints(List(endpointTemplate)), 5000.milliseconds)
+ ends.size should equal(1)
+ val endpoint = ends.head
+
+ // let the measproc do its config, just a chance to avoid 2s wasted in the next step
+ Thread.sleep(500)
+
+ val regRequest = FrontEndRegistrationTemplate.newBuilder().setEndpointUuid(endpoint.getUuid).build()
+
+ val regResult = retryFutureUntilSuccess(3, 2000) {
+ frontEndClient.putFrontEndRegistration(regRequest, endpoint.getUuid.getValue)
+ }
+
+ val set2Address = regResult.getInputAddress
+ this.endpoint2 = Some(endpoint)
+ this.endpoint2Address = Some(set2Address)
+
+ val pointATemplate = PointTemplate.newBuilder()
+ .setEntityTemplate(
+ EntityTemplate.newBuilder()
+ .setName("Set2PointA")
+ .addTypes("Point"))
+ .setPointCategory(Model.PointCategory.ANALOG)
+ .setUnit("unit3")
+ .build()
+
+ val pointAResult = Await.result(modelClient.putPoints(List(pointATemplate)), 5000.milliseconds)
+
+ pointAResult.size should equal(1)
+ val pointA = pointAResult.head
+ this.dynamicPoint = Some(pointA)
+
+ val measClient = MeasurementService.client(session)
+ val pointABatch = analogBatch(pointA.getUuid, 1.2, 3)
+
+ logger.info("post meas")
+
+ val postResult = pollForSuccess(500, 5000) {
+ Await.result(measClient.postMeasurements(pointABatch, set2Address), 500.milliseconds)
+ }
+ postResult should equal(true)
+
+ logger.info("get meas")
+ val emptyMeasResult = Await.result(measClient.getCurrentValues(List(pointA.getUuid)), 5000.milliseconds)
+ emptyMeasResult.size should equal(0)
+
+ val edgeDesc = EntityEdgeDescriptor.newBuilder()
+ .setParentUuid(endpoint.getUuid)
+ .setChildUuid(pointA.getUuid)
+ .setRelationship("source")
+ .build()
+
+ logger.info("put edges")
+ val edgeResult = Await.result(modelClient.putEdges(List(edgeDesc)), 5000.milliseconds)
+ edgeResult.size should equal(1)
+
+ val measInDb = pollForSuccess(500, 5000) {
+ val postResult2 = Await.result(measClient.postMeasurements(pointABatch, set2Address), 5000.milliseconds)
+ postResult2 should equal(true)
+
+ val shouldExistResult = Await.result(measClient.getCurrentValues(List(pointA.getUuid)), 5000.milliseconds)
+ if (shouldExistResult.isEmpty) {
+ throw new Exception("Still empty")
+ } else {
+ shouldExistResult
+ }
+ }
+
+ checkAnalog(measInDb.head.getValue, 1.2, Some(3))
+
+ }
+
+ test("Trigger added") {
+ val session = this.session.get
+ val pointA = this.dynamicPoint.get
+ val set2Address = this.endpoint2Address.get
+
+ val auxTrigger = buildOffsetTrigger(0.33d)
+
+ val keyValue = EntityKeyValue.newBuilder()
+ .setUuid(pointA.getUuid)
+ .setKey("triggerSet")
+ .setValue(StoredValue.newBuilder()
+ .setByteArrayValue(auxTrigger.toByteString))
+ .build()
+
+ val modelClient = ModelService.client(session)
+
+ val setPutResult = Await.result(modelClient.putEntityKeyValues(Seq(keyValue)), 5000.milliseconds)
+ setPutResult.size should equal(1)
+
+ val unitWasSet = pollForSuccessWithIter(500, 5000) { i =>
+ val m = postAndGet(session, pointA.getUuid, analogBatch(pointA.getUuid, 1.1, 9 + i), set2Address)
+ if (m.getDoubleVal != 1.1) m else throw new Exception("poll miss")
+ }
+
+ checkAnalog(unitWasSet, 1.1 + 0.33, None)
+ }
+
+ test("Endpoint disable/enable") {
+ val session = this.session.get
+ val pointA = this.dynamicPoint.get
+ val endpoint2 = this.endpoint2.get
+ val set2Address = this.endpoint2Address.get
+
+ val modelClient = ModelService.client(session)
+ val frontEndClient = FrontEndService.client(session)
+ val measClient = MeasurementService.client(session)
+
+ val disabledResult = Await.result(modelClient.putEndpointDisabled(List(endUpdate(endpoint2.getUuid, true))), 5000.milliseconds)
+ disabledResult.size should equal(1)
+
+ pollForSuccessWithIter(500, 5000) { i =>
+ val time = 15 + i
+ try {
+ Await.ready(measClient.postMeasurements(analogBatch(pointA.getUuid, 1.7, time), set2Address), 100.milliseconds)
+ throw new Exception("poll miss")
+ } catch {
+ case ex: TimeoutException =>
+ }
+ }
+
+ Await.result(modelClient.putEndpointDisabled(List(endUpdate(endpoint2.getUuid, false))), 5000.milliseconds).size should equal(1)
+
+ // let the measproc do its config, just a chance to avoid 2s wasted in the next step
+ Thread.sleep(500)
+
+ val regRequest = FrontEndRegistrationTemplate.newBuilder().setEndpointUuid(endpoint2.getUuid).build()
+
+ val regResult = retryFutureUntilSuccess(3, 2000) {
+ frontEndClient.putFrontEndRegistration(regRequest, endpoint2.getUuid.getValue)
+ }
+
+ val nextEndpoint2Address = regResult.getInputAddress
+ this.endpoint2Address = Some(nextEndpoint2Address)
+
+ pollForSuccessWithIter(500, 5000) { i =>
+ val time = 18 + i
+ val m = postAndGet(session, pointA.getUuid, analogBatch(pointA.getUuid, 1.236, time), nextEndpoint2Address, 500)
+ if (m.getTime == time) m else throw new Exception("poll miss")
+ }
+ }
+
+ private var measTime = 20
+ def nextMeasTime(): Int = {
+ val r = measTime
+ measTime += 1
+ r
+ }
+
+ test("Override Added") {
+ val session = this.session.get
+ val pointA = this.dynamicPoint.get
+ val set2Address = this.endpoint2Address.get
+
+ val modelClient = ModelService.client(session)
+ val processingClient = ProcessingService.client(session)
+ val measClient = MeasurementService.client(session)
+
+ // Set up new value suppression
+ val auxTrigger = buildFilterTrigger()
+
+ val keyValue = EntityKeyValue.newBuilder()
+ .setUuid(pointA.getUuid)
+ .setKey("triggerSet")
+ .setValue(StoredValue.newBuilder()
+ .setByteArrayValue(auxTrigger.toByteString))
+ .build()
+
+ val setPutResult = Await.result(modelClient.putEntityKeyValues(Seq(keyValue)), 5000.milliseconds)
+ setPutResult.size should equal(1)
+
+ val offsetUsed = pollForSuccessWithIter(500, 5000) { i =>
+ val t = nextMeasTime()
+ val m = postAndGet(session, pointA.getUuid, analogBatch(pointA.getUuid, 20.56, t), set2Address)
+ if (m.getTime != t) m else throw new Exception("poll miss")
+ }
+
+ val startMeas = Await.result(measClient.getCurrentValues(List(pointA.getUuid)), 5000.milliseconds)
+ val startValue = startMeas.head.getValue.getDoubleVal
+
+ val measOver = MeasOverride.newBuilder()
+ .setPointUuid(pointA.getUuid)
+ .build()
+
+ val overResult = Await.result(processingClient.putOverrides(List(measOver)), 5000.milliseconds)
+ overResult.size should equal(1)
+
+ val nisMeas = pollForSuccess(500, 5000) {
+ val result = Await.result(measClient.getCurrentValues(List(pointA.getUuid)), 5000.milliseconds)
+ result.size should equal(1)
+ val m = result.head.getValue
+ val isBlocked = m.getQuality.hasOperatorBlocked && m.getQuality.getOperatorBlocked
+ if (isBlocked) m else throw new Exception("poll miss")
+ }
+
+ nisMeas.getType should equal(Measurement.Type.DOUBLE)
+ nisMeas.getDoubleVal should equal(startValue)
+ (nisMeas.getQuality.hasOperatorBlocked && nisMeas.getQuality.getOperatorBlocked) should equal(true)
+ (nisMeas.getQuality.getDetailQual.hasOldData && nisMeas.getQuality.getDetailQual.getOldData) should equal(true)
+
+ val postNisResult = Await.result(measClient.postMeasurements(analogBatch(pointA.getUuid, 1.9, nextMeasTime()), set2Address), 5000.milliseconds)
+ postNisResult should equal(true)
+
+ val stillNisResult = Await.result(measClient.getCurrentValues(List(pointA.getUuid)), 5000.milliseconds)
+ stillNisResult.size should equal(1)
+
+ val stillNisMeas = stillNisResult.head.getValue
+ stillNisMeas.getType should equal(Measurement.Type.DOUBLE)
+ stillNisMeas.getDoubleVal should equal(startValue)
+ (stillNisMeas.getQuality.hasOperatorBlocked && stillNisMeas.getQuality.getOperatorBlocked) should equal(true)
+ (stillNisMeas.getQuality.getDetailQual.hasOldData && stillNisMeas.getQuality.getDetailQual.getOldData) should equal(true)
+
+ val delResult = Await.result(processingClient.deleteOverrides(List(pointA.getUuid)), 5000.milliseconds)
+ delResult.size should equal(1)
+
+ val afterNisMeas = pollForSuccess(500, 5000) {
+ val result = Await.result(measClient.getCurrentValues(List(pointA.getUuid)), 5000.milliseconds)
+ result.size should equal(1)
+ val m = result.head.getValue
+ println(m)
+ val notBlocked = !(m.getQuality.hasOperatorBlocked && m.getQuality.getOperatorBlocked)
+ if (notBlocked) m else throw new Exception("poll miss")
+ }
+
+ afterNisMeas.getType should equal(Measurement.Type.DOUBLE)
+ afterNisMeas.getDoubleVal should equal(1.9)
+ }
+
+ test("Override add/remove with identical update inbetween") {
+ val session = this.session.get
+ val pointA = this.dynamicPoint.get
+ val set2Address = this.endpoint2Address.get
+
+ val processingClient = ProcessingService.client(session)
+ val measClient = MeasurementService.client(session)
+
+ val initialValue = Await.result(measClient.postMeasurements(analogBatch(pointA.getUuid, 2.35, nextMeasTime()), set2Address), 5000.milliseconds)
+ initialValue should equal(true)
+
+ val startMeas = Await.result(measClient.getCurrentValues(List(pointA.getUuid)), 5000.milliseconds)
+ println("startMeas: " + startMeas)
+ val startValue = startMeas.head.getValue.getDoubleVal
+
+ val overVal = 1.11
+
+ val measOver = MeasOverride.newBuilder()
+ .setPointUuid(pointA.getUuid)
+ .setMeasurement(Measurement.newBuilder()
+ .setDoubleVal(overVal)
+ .setType(Measurement.Type.DOUBLE)
+ .setTime(nextMeasTime()))
+ .build()
+
+ val overResult = Await.result(processingClient.putOverrides(List(measOver)), 5000.milliseconds)
+ overResult.size should equal(1)
+
+ val overMeas = pollForSuccess(500, 5000) {
+ val result = Await.result(measClient.getCurrentValues(List(pointA.getUuid)), 5000.milliseconds)
+ result.size should equal(1)
+ val m = result.head.getValue
+ val isBlocked = m.getQuality.hasOperatorBlocked && m.getQuality.getOperatorBlocked
+ if (isBlocked) m else throw new Exception("poll miss")
+ }
+
+ overMeas.getType should equal(Measurement.Type.DOUBLE)
+ overMeas.getDoubleVal should equal(overVal)
+ (overMeas.getQuality.hasOperatorBlocked && overMeas.getQuality.getOperatorBlocked) should equal(true)
+ (overMeas.getQuality.hasSource && overMeas.getQuality.getSource == Quality.Source.SUBSTITUTED) should equal(true)
+
+ val postNisResult = Await.result(measClient.postMeasurements(analogBatch(pointA.getUuid, 2.35, nextMeasTime()), set2Address), 5000.milliseconds)
+ postNisResult should equal(true)
+
+ val stillOverResult = Await.result(measClient.getCurrentValues(List(pointA.getUuid)), 5000.milliseconds)
+ stillOverResult.size should equal(1)
+
+ val stillOverMeas = stillOverResult.head.getValue
+ stillOverMeas.getType should equal(Measurement.Type.DOUBLE)
+ stillOverMeas.getDoubleVal should equal(overVal)
+ (stillOverMeas.getQuality.hasOperatorBlocked && stillOverMeas.getQuality.getOperatorBlocked) should equal(true)
+ (stillOverMeas.getQuality.hasSource && stillOverMeas.getQuality.getSource == Quality.Source.SUBSTITUTED) should equal(true)
+
+ val delResult = Await.result(processingClient.deleteOverrides(List(pointA.getUuid)), 5000.milliseconds)
+ delResult.size should equal(1)
+
+ val afterRemoveMeas = pollForSuccess(500, 5000) {
+ val result = Await.result(measClient.getCurrentValues(List(pointA.getUuid)), 5000.milliseconds)
+ result.size should equal(1)
+ val m = result.head.getValue
+ val notBlocked = !(m.getQuality.hasOperatorBlocked && m.getQuality.getOperatorBlocked)
+ if (notBlocked) m else throw new Exception("poll miss")
+ }
+
+ afterRemoveMeas.getType should equal(Measurement.Type.DOUBLE)
+ afterRemoveMeas.getDoubleVal should equal(2.35)
+ }
+
+ test("un-nissing same measurement doesn't run transform twice") {
+ val session = this.session.get
+ val pointA = this.dynamicPoint.get
+ val set2Address = this.endpoint2Address.get
+
+ val modelClient = ModelService.client(session)
+ val measClient = MeasurementService.client(session)
+ val processingClient = ProcessingService.client(session)
+
+ val auxTrigger = buildUnconditionalOffsetTrigger(0.01d)
+
+ val keyValue = EntityKeyValue.newBuilder()
+ .setUuid(pointA.getUuid)
+ .setKey("triggerSet")
+ .setValue(StoredValue.newBuilder()
+ .setByteArrayValue(auxTrigger.toByteString))
+ .build()
+
+ val setPutResult = Await.result(modelClient.putEntityKeyValues(Seq(keyValue)), 5000.milliseconds)
+ setPutResult.size should equal(1)
+
+ val offsetUsed = pollForSuccessWithIter(500, 5000) { i =>
+ val m = postAndGet(session, pointA.getUuid, analogBatch(pointA.getUuid, 1.55, nextMeasTime()), set2Address)
+ if (m.getDoubleVal == 1.56) m else throw new Exception("poll miss")
+ }
+
+ val measOver = MeasOverride.newBuilder()
+ .setPointUuid(pointA.getUuid)
+ .build()
+
+ val overResult = Await.result(processingClient.putOverrides(List(measOver)), 5000.milliseconds)
+ overResult.size should equal(1)
+
+ val nisMeas = pollForSuccess(500, 5000) {
+ val result = Await.result(measClient.getCurrentValues(List(pointA.getUuid)), 5000.milliseconds)
+ result.size should equal(1)
+ val m = result.head.getValue
+ val isBlocked = m.getQuality.hasOperatorBlocked && m.getQuality.getOperatorBlocked
+ if (isBlocked) m else throw new Exception("poll miss")
+ }
+
+ nisMeas.getType should equal(Measurement.Type.DOUBLE)
+ nisMeas.getDoubleVal should equal(1.56)
+ (nisMeas.getQuality.hasOperatorBlocked && nisMeas.getQuality.getOperatorBlocked) should equal(true)
+ (nisMeas.getQuality.getDetailQual.hasOldData && nisMeas.getQuality.getDetailQual.getOldData) should equal(true)
+
+ val delResult = Await.result(processingClient.deleteOverrides(List(pointA.getUuid)), 5000.milliseconds)
+ delResult.size should equal(1)
+
+ val afterNisMeas = pollForSuccess(500, 5000) {
+ val result = Await.result(measClient.getCurrentValues(List(pointA.getUuid)), 5000.milliseconds)
+ result.size should equal(1)
+ val m = result.head.getValue
+ val notBlocked = !(m.getQuality.hasOperatorBlocked && m.getQuality.getOperatorBlocked)
+ if (notBlocked) m else throw new Exception("poll miss")
+ }
+
+ afterNisMeas.getType should equal(Measurement.Type.DOUBLE)
+ afterNisMeas.getDoubleVal should equal(1.56)
+ }
+
+ test("Swap point between endpoints") {
+ val session = this.session.get
+ val pointA = this.dynamicPoint.get
+ val endpoint1 = this.endpoint1.get
+ val end1address = this.endpoint1Address.get
+ val endpoint2 = this.endpoint2.get
+ val end2address = this.endpoint2Address.get
+
+ val modelClient = ModelService.client(session)
+ val end2Edge = EntityEdgeDescriptor.newBuilder()
+ .setParentUuid(endpoint2.getUuid)
+ .setChildUuid(pointA.getUuid)
+ .setRelationship("source")
+ .build()
+
+ logger.info("put edges")
+ val edgeResult = Await.result(modelClient.deleteEdges(List(end2Edge)), 5000.milliseconds)
+ edgeResult.size should equal(1)
+
+ pollForSuccessWithIter(500, 5000) { i =>
+ val t = nextMeasTime()
+ val m = postAndGet(session, pointA.getUuid, analogBatch(pointA.getUuid, 1.7, t), end2address)
+ if (m.getTime != t) m else throw new Exception("poll miss")
+ }
+
+ val end1Edge = EntityEdgeDescriptor.newBuilder()
+ .setParentUuid(endpoint1.getUuid)
+ .setChildUuid(pointA.getUuid)
+ .setRelationship("source")
+ .build()
+
+ val end1Result = Await.result(modelClient.putEdges(List(end1Edge)), 5000.milliseconds)
+ end1Result.size should equal(1)
+
+ pollForSuccessWithIter(500, 5000) { i =>
+ val t = nextMeasTime()
+ val m = postAndGet(session, pointA.getUuid, analogBatch(pointA.getUuid, 1.74, t), end1address)
+ if (m.getTime == t) m else throw new Exception("poll miss")
+ }
+
+ val unitTrigger = buildOffsetTrigger(0.66d)
+ val keyValue = EntityKeyValue.newBuilder()
+ .setUuid(pointA.getUuid)
+ .setKey("triggerSet")
+ .setValue(StoredValue.newBuilder()
+ .setByteArrayValue(unitTrigger.toByteString))
+ .build()
+ val triggerResult = Await.result(modelClient.putEntityKeyValues(List(keyValue)), 5000.milliseconds)
+ triggerResult.size should equal(1)
+
+ pollForSuccessWithIter(500, 5000) { i =>
+ val t = nextMeasTime()
+ val m = postAndGet(session, pointA.getUuid, analogBatch(pointA.getUuid, 1.05, t), end1address)
+ if (m.getDoubleVal == (1.05 + 0.66)) m else throw new Exception("poll miss")
+ }
+ }
+}
+
diff --git a/integration/src/test/scala/io/greenbus/integration/ldr/LoaderTest.scala b/integration/src/test/scala/io/greenbus/integration/ldr/LoaderTest.scala
new file mode 100644
index 0000000..66cb60a
--- /dev/null
+++ b/integration/src/test/scala/io/greenbus/integration/ldr/LoaderTest.scala
@@ -0,0 +1,495 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.integration.ldr
+
+import java.io.File
+
+import akka.actor.{ ActorRef, ActorSystem }
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.client.ServiceConnection
+import io.greenbus.client.service.ModelService
+import io.greenbus.client.service.proto.Model.ModelUUID
+import io.greenbus.client.service.proto.ModelRequests.{ EndpointQuery, EntityKeySet, EntityEdgeQuery, EntityQuery }
+import io.greenbus.ldr.XmlImporter
+import io.greenbus.ldr.xml.Configuration
+import io.greenbus.loader.set.LoadingException
+import io.greenbus.msg.Session
+import io.greenbus.msg.amqp.AmqpSettings
+import io.greenbus.msg.qpid.QpidBroker
+import io.greenbus.services.{ CoreServices, ResetDatabase, ServiceManager }
+import io.greenbus.util.{ XmlHelper, UserSettings }
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.matchers.ShouldMatchers
+import org.scalatest.{ BeforeAndAfterAll, FunSuite }
+
+import scala.concurrent.Await
+import scala.concurrent.duration._
+import scala.collection.JavaConversions._
+
+@RunWith(classOf[JUnitRunner])
+class LoaderTest extends FunSuite with ShouldMatchers with Logging with BeforeAndAfterAll {
+ import io.greenbus.integration.tools.PollingUtils._
+
+ val testConfigPath = "io.greenbus.test.cfg"
+ val amqpConfig = AmqpSettings.load(testConfigPath)
+ val userConfig = UserSettings.load(testConfigPath)
+ val system = ActorSystem("integrationTest")
+ var services = Option.empty[ActorRef]
+ var conn = Option.empty[ServiceConnection]
+ var session = Option.empty[Session]
+ val parentDir = new File(".")
+
+ override protected def beforeAll(): Unit = {
+ ResetDatabase.reset(testConfigPath)
+
+ logger.info("starting services")
+ services = Some(system.actorOf(ServiceManager.props(testConfigPath, testConfigPath, CoreServices.runServices)))
+
+ val conn = ServiceConnection.connect(amqpConfig, QpidBroker, 5000)
+ this.conn = Some(conn)
+
+ val session = pollForSuccess(500, 5000) {
+ Await.result(conn.login(userConfig.user, userConfig.password), 500.milliseconds)
+ }
+
+ this.session = Some(session)
+ }
+
+ override protected def afterAll(): Unit = {
+ system.shutdown()
+ system.awaitTermination()
+ this.conn.get.disconnect()
+ }
+
+ test("initial import") {
+ val session = this.session.get
+
+ val xml = XmlHelper.read(TestXml.xmlSimple, classOf[Configuration])
+
+ XmlImporter.importFromXml(amqpConfig, userConfig, None, xml, parentDir, prompt = false)
+
+ val modelClient = ModelService.client(session)
+
+ val allEntities = Await.result(modelClient.entityQuery(EntityQuery.newBuilder().build()), 5000.milliseconds)
+
+ allEntities.size should equal(7)
+
+ val rootA = allEntities.find(_.getName == "RootA").get
+ val equipA = allEntities.find(_.getName == "EquipA").get
+ val equipB = allEntities.find(_.getName == "EquipB").get
+ val pointA = allEntities.find(_.getName == "PointA").get
+ val commandA = allEntities.find(_.getName == "CommandA").get
+ val endpointA = allEntities.find(_.getName == "EndpointA").get
+ val dangling = allEntities.find(_.getName == "DanglingEndpoint").get
+
+ rootA.getTypesList.toSet should equal(Set("Root"))
+ equipA.getTypesList.toSet should equal(Set("Equip"))
+ equipB.getTypesList.toSet should equal(Set("Equip", "TypeA"))
+ pointA.getTypesList.toSet should equal(Set("Point", "PointTypeA"))
+ commandA.getTypesList.toSet should equal(Set("Command", "CommandTypeA"))
+ endpointA.getTypesList.toSet should equal(Set("Endpoint", "EndpointTypeA"))
+
+ val allUuids = Seq(rootA, equipA, equipB, pointA, commandA, endpointA).map(_.getUuid)
+
+ val allKeys = Await.result(modelClient.getEntityKeys(allUuids), 5000.milliseconds)
+
+ val allKeyPairs = Await.result(modelClient.getEntityKeyValues(allKeys), 5000.milliseconds)
+
+ allKeyPairs.size should equal(1)
+ val key = allKeyPairs.find(_.getUuid == equipA.getUuid).get
+ key.getKey should equal("keyA")
+ key.getValue.hasStringValue should equal(true)
+ key.getValue.getStringValue should equal("valueA")
+
+ val edgeQuery = EntityEdgeQuery.newBuilder()
+ .addAllParentUuids(allUuids)
+ .addAllChildUuids(allUuids)
+ .setDepthLimit(1)
+ .setPageSize(Int.MaxValue)
+ .build()
+
+ val allEdges = Await.result(modelClient.edgeQuery(edgeQuery), 5000.milliseconds)
+
+ val edgeSet = Set(
+ (rootA.getUuid, "owns", equipA.getUuid),
+ (rootA.getUuid, "owns", equipB.getUuid),
+ (equipA.getUuid, "owns", pointA.getUuid),
+ (equipA.getUuid, "owns", commandA.getUuid),
+ (pointA.getUuid, "feedback", commandA.getUuid),
+ (endpointA.getUuid, "source", pointA.getUuid),
+ (endpointA.getUuid, "source", commandA.getUuid))
+
+ allEdges.map(e => (e.getParent, e.getRelationship, e.getChild)).toSet should equal(edgeSet)
+ }
+
+ test("dangling endpoint doesn't prevent re-upload") {
+ val session = this.session.get
+
+ val xml = XmlHelper.read(TestXml.xmlSimple, classOf[Configuration])
+
+ XmlImporter.importFromXml(amqpConfig, userConfig, None, xml, parentDir, prompt = false)
+
+ val modelClient = ModelService.client(session)
+
+ val endpoints = Await.result(modelClient.endpointQuery(EndpointQuery.newBuilder.build), 5000.milliseconds)
+
+ endpoints.size should equal(2)
+ }
+
+ test("double key") {
+ val session = this.session.get
+
+ val xml = XmlHelper.read(TestXml.xmlDoubleKey, classOf[Configuration])
+
+ intercept[LoadingException] {
+ XmlImporter.importFromXml(amqpConfig, userConfig, None, xml, parentDir, prompt = false)
+ }
+
+ val modelClient = ModelService.client(session)
+
+ val allEntities = Await.result(modelClient.get(EntityKeySet.newBuilder().addNames("RootB").build()), 5000.milliseconds)
+ allEntities.size should equal(0)
+ }
+
+ test("double name in same frag") {
+ val session = this.session.get
+
+ val xml = XmlHelper.read(TestXml.xmlRootBWithDuplicate, classOf[Configuration])
+
+ intercept[LoadingException] {
+ XmlImporter.importFromXml(amqpConfig, userConfig, None, xml, parentDir, prompt = false)
+ }
+
+ val modelClient = ModelService.client(session)
+
+ val allEntities = Await.result(modelClient.get(EntityKeySet.newBuilder().addNames("RootB").build()), 5000.milliseconds)
+ allEntities.size should equal(0)
+ }
+
+ test("duplicate source edge") {
+ val session = this.session.get
+
+ val xml = XmlHelper.read(TestXml.xmlDuplicateSource, classOf[Configuration])
+
+ intercept[LoadingException] {
+ XmlImporter.importFromXml(amqpConfig, userConfig, None, xml, parentDir, prompt = false)
+ }
+ }
+
+ test("rename to name that already exists in another fragment") {
+ val session = this.session.get
+
+ val xml = XmlHelper.read(TestXml.xmlRootB, classOf[Configuration])
+
+ XmlImporter.importFromXml(amqpConfig, userConfig, None, xml, parentDir, prompt = false)
+
+ val modelClient = ModelService.client(session)
+
+ val keySet = EntityKeySet.newBuilder()
+ .addNames("RootB")
+ .addNames("EquipC")
+ .build()
+
+ val allEntities = Await.result(modelClient.get(keySet), 5000.milliseconds)
+ allEntities.size should equal(2)
+
+ val rootB = allEntities.find(_.getName == "RootB").get
+ val equipC = allEntities.find(_.getName == "EquipC").get
+
+ val xml2 = XmlHelper.read(TestXml.xmlRootBRename(rootB.getUuid, equipC.getUuid), classOf[Configuration])
+
+ intercept[LoadingException] {
+ XmlImporter.importFromXml(amqpConfig, userConfig, None, xml2, parentDir, prompt = false)
+ }
+
+ val xml3 = XmlHelper.read(TestXml.xmlRootBCreateDuplicate, classOf[Configuration])
+
+ intercept[LoadingException] {
+ XmlImporter.importFromXml(amqpConfig, userConfig, None, xml3, parentDir, prompt = false)
+ }
+ }
+
+ test("promoting up the hierarchy does not cause diamond exception") {
+ val session = this.session.get
+
+ val xml = XmlHelper.read(TestXml.xmlRootC, classOf[Configuration])
+
+ XmlImporter.importFromXml(amqpConfig, userConfig, None, xml, parentDir, prompt = false)
+
+ val modelClient = ModelService.client(session)
+
+ val keySet = EntityKeySet.newBuilder()
+ .addNames("RootC")
+ .addNames("EquipE")
+ .addNames("PointC")
+ .build()
+
+ val allEntities = Await.result(modelClient.get(keySet), 5000.milliseconds)
+ allEntities.size should equal(3)
+
+ val rootC = allEntities.find(_.getName == "RootC").get
+ val pointC = allEntities.find(_.getName == "PointC").get
+
+ val xml2 = XmlHelper.read(TestXml.xmlRootCPromote(rootC.getUuid, pointC.getUuid), classOf[Configuration])
+
+ XmlImporter.importFromXml(amqpConfig, userConfig, None, xml2, parentDir, prompt = false)
+
+ val allEntitiesAfter = Await.result(modelClient.get(keySet), 5000.milliseconds)
+ allEntitiesAfter.size should equal(2)
+ allEntitiesAfter.map(_.getName).contains("EquipE") should equal(false)
+ }
+
+ test("deleting") {
+ val session = this.session.get
+
+ val xml = XmlHelper.read(TestXml.xmlSimpleDeleted, classOf[Configuration])
+
+ XmlImporter.importFromXml(amqpConfig, userConfig, None, xml, parentDir, prompt = false)
+
+ val modelClient = ModelService.client(session)
+
+ val keySet = EntityKeySet.newBuilder()
+ .addNames("RootA")
+ .addNames("EquipA")
+ .addNames("PointA")
+ .addNames("CommandA")
+ .addNames("EquipB")
+ .build()
+
+ val allEntities = Await.result(modelClient.get(keySet), 5000.milliseconds)
+ allEntities.size should equal(1)
+ allEntities.head.getName should equal("RootA")
+ }
+}
+
+object TestXml {
+
+ val xmlSimple =
+ """
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ """.stripMargin
+
+ val xmlSimpleDeleted =
+ """
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ """.stripMargin
+
+ val xmlDoubleKey =
+ """
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ """.stripMargin
+
+ val xmlDuplicateSource =
+ """
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ """.stripMargin
+
+ val xmlRootBWithDuplicate =
+ """
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ """.stripMargin
+
+ val xmlRootB =
+ """
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ """.stripMargin
+
+ def xmlRootBRename(rootBUuid: ModelUUID, equipCUuid: ModelUUID) =
+ s"""
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ """.stripMargin
+
+ val xmlRootBCreateDuplicate =
+ """
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ """.stripMargin
+
+ val xmlRootC =
+ """
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ """.stripMargin
+
+ def xmlRootCPromote(rootCUuid: ModelUUID, pointUuid: ModelUUID) =
+ s"""
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ """.stripMargin
+}
diff --git a/io.greenbus.msg.amqp.cfg b/io.greenbus.msg.amqp.cfg
new file mode 100755
index 0000000..d58497e
--- /dev/null
+++ b/io.greenbus.msg.amqp.cfg
@@ -0,0 +1,13 @@
+
+# AMQP settings
+io.greenbus.msg.amqp.host = 127.0.0.1
+io.greenbus.msg.amqp.port = 5672
+io.greenbus.msg.amqp.user = qpid
+io.greenbus.msg.amqp.password = qpid
+io.greenbus.msg.amqp.virtualHost = greenbus
+io.greenbus.msg.amqp.heartbeatTimeSeconds = 30
+io.greenbus.msg.amqp.ssl = false
+io.greenbus.msg.amqp.trustStore = etc/trust-store.jks
+io.greenbus.msg.amqp.trustStorePassword = password
+#io.greenbus.msg.amqp.keyStore = etc/key-store.jks
+#io.greenbus.msg.amqp.keyStorePassword = password
\ No newline at end of file
diff --git a/io.greenbus.sql.cfg b/io.greenbus.sql.cfg
new file mode 100755
index 0000000..268af63
--- /dev/null
+++ b/io.greenbus.sql.cfg
@@ -0,0 +1,17 @@
+
+# sql settings
+
+
+# host address and port of database server
+io.greenbus.sql.url = jdbc:postgresql://127.0.0.1:5432/greenbus3_d
+
+# database user/name password must match what was entered during database creation
+io.greenbus.sql.username = core
+io.greenbus.sql.password = core
+
+# any SQL query that takes longer than slowquery will generate a log message so we can track
+# if debugging a sql issue this can be turned to 0 to see every sql query as it is made
+io.greenbus.sql.slowquery = 100
+
+# configure the max size of the database connection pool
+# io.greenbus.sql.maxactive = 50
\ No newline at end of file
diff --git a/io.greenbus.test.cfg b/io.greenbus.test.cfg
new file mode 100755
index 0000000..26e5c8b
--- /dev/null
+++ b/io.greenbus.test.cfg
@@ -0,0 +1,21 @@
+
+io.greenbus.sql.url = jdbc:postgresql://127.0.0.1:5432/greenbus3_t
+io.greenbus.sql.username = core
+io.greenbus.sql.password = core
+io.greenbus.sql.slowquery = 100
+# io.greenbus.sql.maxactive = 50
+
+io.greenbus.user.username=system
+io.greenbus.user.password=system
+
+io.greenbus.msg.amqp.host = 127.0.0.1
+io.greenbus.msg.amqp.port = 5672
+io.greenbus.msg.amqp.user = qpid
+io.greenbus.msg.amqp.password = qpid
+io.greenbus.msg.amqp.virtualHost = test
+io.greenbus.msg.amqp.heartbeatTimeSeconds = 30
+io.greenbus.msg.amqp.ssl = false
+io.greenbus.msg.amqp.trustStore = etc/trust-store.jks
+io.greenbus.msg.amqp.trustStorePassword = password
+#io.greenbus.msg.amqp.keyStore = etc/key-store.jks
+#io.greenbus.msg.amqp.keyStorePassword = password
diff --git a/io.greenbus.user.cfg b/io.greenbus.user.cfg
new file mode 100755
index 0000000..3edb931
--- /dev/null
+++ b/io.greenbus.user.cfg
@@ -0,0 +1,3 @@
+
+io.greenbus.user.username=system
+io.greenbus.user.password=system
\ No newline at end of file
diff --git a/loader-xml/pom.xml b/loader-xml/pom.xml
new file mode 100755
index 0000000..66e5546
--- /dev/null
+++ b/loader-xml/pom.xml
@@ -0,0 +1,75 @@
+
+ 4.0.0
+
+
+ io.greenbus
+ greenbus-scala-base
+ 3.0.0
+ ../scala-base
+
+
+ greenbus-loader-xml
+ jar
+
+
+
+ AGPLv3
+ http://www.gnu.org/licenses/agpl-3.0.txt
+
+
+
+
+
+
+
+ com.mycila.maven-license-plugin
+ maven-license-plugin
+
+
+
+
+
+ org.codehaus.mojo
+ jaxb2-maven-plugin
+ ${jaxb2-maven-plugin.version}
+
+
+ config
+
+ xjc
+
+
+ src/main/resources
+ configuration.xsd,agents.xsd,events.xsd
+ ${project.build.directory}/generated-sources/jaxb/.ConfigurationStaleFlag2
+
+ false
+
+
+
+
+
+
+
+
+
+
+ io.greenbus
+ greenbus-loading
+ 3.0.0
+
+
+ commons-cli
+ commons-cli
+ 1.2
+
+
+ commons-io
+ commons-io
+ 2.4
+
+
+
+
+
diff --git a/loader-xml/src/main/resources/agents.xsd b/loader-xml/src/main/resources/agents.xsd
new file mode 100755
index 0000000..153a3a1
--- /dev/null
+++ b/loader-xml/src/main/resources/agents.xsd
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/loader-xml/src/main/resources/calculation.xsd b/loader-xml/src/main/resources/calculation.xsd
new file mode 100755
index 0000000..589ba95
--- /dev/null
+++ b/loader-xml/src/main/resources/calculation.xsd
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/loader-xml/src/main/resources/configuration.xsd b/loader-xml/src/main/resources/configuration.xsd
new file mode 100755
index 0000000..1189373
--- /dev/null
+++ b/loader-xml/src/main/resources/configuration.xsd
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The root element of the entire configuration file. It also contains the main sections of the
+ configuration file -messageModel ,
+ actionModel ,equipmentModel , and
+ communicationsModel .
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/loader-xml/src/main/resources/events.xsd b/loader-xml/src/main/resources/events.xsd
new file mode 100644
index 0000000..2de7e24
--- /dev/null
+++ b/loader-xml/src/main/resources/events.xsd
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/loader-xml/src/main/resources/modelHierarchy.xsd b/loader-xml/src/main/resources/modelHierarchy.xsd
new file mode 100755
index 0000000..3287ba1
--- /dev/null
+++ b/loader-xml/src/main/resources/modelHierarchy.xsd
@@ -0,0 +1,400 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Types are tags used for classification of equipment and measurements into groups
+ or categories.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Defines a control. The "name" attribute is the name of the Control. This name will
+ be displayed in the HMI. Control names should not contain spaces or periods "." and should be
+ limited to 64 characters. (ex. Breaker100_Open).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Defines an status measurement. The following are attributes of
+ status
+
+ name
+ - Name of the status measurement that will be displayed in the HMI. Status names should not
+ contain spaces or periods "." and should be limited to 64 characters. (ex. CB1001_Status,
+ Recloser122_status).
+
+
+ unit
+ - The status unit of measurement.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Defines an analog measurement. The following are attributes of
+ analog
+
+ name
+ - Name of the analog measurement that will be displayed in the HMI. Analog names should not
+ contain spaces or periods "." and should be limited to 64 characters. (ex. Line100_Current,
+ Bus201_Voltage).
+
+
+ unit
+ - The analog unit of measurement.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Defines a counter. The
+ name
+ attribute is the of the Counter Measurement. This name will be displayed in the HMI. Counter names
+ should not contain spaces or periods "." and should be limited to 64 characters. (ex.
+ TX300_TotOperations). The
+ unit
+ attribute is the counter unit of measurement.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Defines entities within the system such as stations, equipment groups, equipment, and devices.
+ Measurement tags are represented as
+ equipment.equipment.measurement-name . The parent-child relationship of any measurement tag is
+ defined by nesting of equipment elements.
+ equipment
+ elements can specify all contained equipment or reference an
+ equipmentProfile . The only difference between an
+ equipmentProfile
+ and
+ equipment
+ is that
+ equipment
+ has an optional
+ equipmentProfile
+ attribute. Both can specify multiple profiles in descendant elements. equipment elements can contain
+ the following elements (equipmentProfile ,type ,control ,status ,analog ,counter ,equipment ).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This is the root element of the equipment modeling section, child elements of
+ equipmentModel
+ are profiles and equipment.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This is the root element of the equipment modeling section, child elements of
+ equipmentModel
+ are profiles and equipment.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/loader-xml/src/main/resources/triggersActionsMessages.xsd b/loader-xml/src/main/resources/triggersActionsMessages.xsd
new file mode 100755
index 0000000..e9cb777
--- /dev/null
+++ b/loader-xml/src/main/resources/triggersActionsMessages.xsd
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/loader-xml/src/main/scala/io/greenbus/ldr/CalculationConversion.scala b/loader-xml/src/main/scala/io/greenbus/ldr/CalculationConversion.scala
new file mode 100644
index 0000000..3e9e38b
--- /dev/null
+++ b/loader-xml/src/main/scala/io/greenbus/ldr/CalculationConversion.scala
@@ -0,0 +1,219 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.ldr
+
+import java.util.UUID
+
+import io.greenbus.ldr.xml._
+import io.greenbus.client.service.proto.Calculations
+import io.greenbus.client.service.proto.Calculations._
+import io.greenbus.loader.set.{ LoadingException, EntityId }
+
+import scala.collection.JavaConversions._
+
+object CalculationConversion {
+
+ def toProto(calc: xml.Calculation): (CalculationDescriptor.Builder, Seq[(EntityId, CalculationInput.Builder)]) = {
+
+ val (inputs, inputStratOpt) = if (calc.isSetInputs) {
+ val inputs = calc.getInputs
+
+ val stratOpt = if (inputs.isSetInputQualityStrategy) {
+ Some(inputStratToProto(inputs.getInputQualityStrategy))
+ } else {
+ None
+ }
+
+ val ins = inputs.getSingleOrMulti.map { inputType =>
+
+ val nameOpt = if (inputType.isSetPointName) Some(inputType.getPointName) else None
+ val uuidOpt = if (inputType.isSetPointUuid) Some(inputType.getPointUuid) else None
+ if (nameOpt.isEmpty && uuidOpt.isEmpty) throw new LoadingXmlException("Reference must have name or UUID", inputType)
+
+ if (nameOpt.isEmpty && uuidOpt.isEmpty) {
+ throw new LoadingXmlException("Calculation input must include name or UUID or both", inputType)
+ }
+
+ val entId = EntityId(uuidOpt.map(UUID.fromString), nameOpt)
+
+ val variable = if (inputType.isSetVariable) inputType.getVariable else throw new LoadingXmlException("Calculation missing variable name", inputType)
+
+ val b = CalculationInput.newBuilder().setVariableName(variable)
+ inputType match {
+ case single: xml.Single =>
+ b.setSingle(
+ SingleMeasurement.newBuilder().setStrategy(
+ SingleMeasurement.MeasurementStrategy.MOST_RECENT))
+ case multi: xml.Multi =>
+ val fromOpt = if (multi.isSetFrom) Some(multi.getFrom) else None
+ val limitOpt = if (multi.isSetLimit) Some(multi.getLimit) else None
+ val sinceLast = if (multi.isSetSinceLastPublish) Some(multi.getSinceLastPublish) else None
+
+ val range = MeasurementRange.newBuilder()
+ fromOpt.foreach(range.setFromMs)
+ limitOpt.foreach(range.setLimit)
+ sinceLast.foreach(range.setSinceLast)
+ b.setRange(range)
+ }
+
+ (entId, b)
+ }
+
+ (ins, stratOpt)
+ } else {
+ (Seq(), None)
+ }
+
+ if (!(calc.isSetFormula && calc.getFormula.isSetValue)) {
+ throw new LoadingXmlException("Calculation must include formula", calc)
+ }
+
+ val form = calc.getFormula.getValue
+
+ val strat = if (calc.isSetTriggering) {
+ val trigXml = calc.getTriggering
+ if (trigXml.isSetUpdateOnAnyChange) {
+ TriggerStrategy.newBuilder()
+ .setUpdateAny(trigXml.getUpdateOnAnyChange)
+ .build()
+ } else if (trigXml.isSetUpdateEveryPeriodInMilliseconds) {
+ TriggerStrategy.newBuilder()
+ .setPeriodMs(trigXml.getUpdateEveryPeriodInMilliseconds)
+ .build()
+ } else {
+ throw new LoadingXmlException("Calculation missing valid trigger strategy", calc)
+ }
+ } else {
+ throw new LoadingXmlException("Calculation missing valid trigger strategy", calc)
+ }
+
+ val qualOutput = if (calc.isSetOutput && calc.getOutput.isSetOutputQualityStrategy) {
+ val protoOutputStrat = outputStratToProto(calc.getOutput.getOutputQualityStrategy)
+ OutputQuality.newBuilder().setStrategy(protoOutputStrat)
+ } else {
+ OutputQuality.newBuilder().setStrategy(Calculations.OutputQuality.Strategy.WORST_QUALITY)
+ }
+
+ val builder = CalculationDescriptor.newBuilder()
+ .setFormula(form)
+ .setTriggering(strat)
+ .setQualityOutput(qualOutput)
+
+ inputStratOpt.foreach(strat => builder.setTriggeringQuality(InputQuality.newBuilder().setStrategy(strat)))
+
+ (builder, inputs)
+ }
+
+ def toXml(calc: CalculationDescriptor, uuidToNameMap: Map[UUID, String]): xml.Calculation = {
+
+ val inputs = new Inputs
+ calc.getCalcInputsList.map(in => inputToXml(in, uuidToNameMap)).foreach(inputs.getSingleOrMulti.add)
+ val inputQualStrat = if (calc.hasTriggeringQuality) calc.getTriggeringQuality else throw new LoadingException("Calculation missing input quality")
+ inputs.setInputQualityStrategy(inputStratToXml(inputQualStrat.getStrategy))
+
+ val exprString = if (calc.hasFormula) calc.getFormula else throw new LoadingException("Calculation missing formula")
+ val formula = new Formula
+ formula.setValue(exprString)
+
+ val triggeringElem = new Triggering
+ val triggeringProto = if (calc.hasTriggering) calc.getTriggering else throw new LoadingException("Calculation missing triggering")
+ if (triggeringProto.hasPeriodMs) {
+ triggeringElem.setUpdateEveryPeriodInMilliseconds(triggeringProto.getPeriodMs)
+ } else if (triggeringProto.hasUpdateAny) {
+ triggeringElem.setUpdateOnAnyChange(true)
+ }
+
+ val qualityOutput = if (calc.hasQualityOutput) calc.getQualityOutput else throw new LoadingException("Calculation quality output")
+ val outputElem = new Output
+ outputElem.setOutputQualityStrategy(outputStratToXml(qualityOutput.getStrategy))
+
+ val elem = new xml.Calculation
+ elem.setInputs(inputs)
+ elem.setFormula(formula)
+ elem.setTriggering(triggeringElem)
+ elem.setOutput(outputElem)
+
+ elem
+ }
+
+ private def inputToXml(input: CalculationInput, uuidToNameMap: Map[UUID, String]) = {
+ val uuid = if (input.hasPointUuid) input.getPointUuid else throw new LoadingException("No uuid for calculation input")
+ val variable = if (input.hasVariableName) input.getVariableName else throw new LoadingException("No variable name for calculation input")
+
+ if (input.hasSingle) {
+
+ val elem = new xml.Single
+ elem.setVariable(variable)
+ elem.setPointUuid(uuid.getValue)
+ uuidToNameMap.get(UUID.fromString(uuid.getValue)).foreach(elem.setPointName)
+ elem
+
+ } else if (input.hasRange) {
+
+ val range = input.getRange
+
+ val elem = new Multi
+ elem.setVariable(variable)
+ elem.setPointUuid(uuid.getValue)
+ uuidToNameMap.get(UUID.fromString(uuid.getValue)).foreach(elem.setPointName)
+
+ val fromMsOpt = if (range.hasFromMs) Some(range.getFromMs) else None
+ val limitOpt = if (range.hasLimit) Some(range.getLimit) else None
+
+ val sinceLast = range.hasSinceLast && range.getSinceLast
+
+ fromMsOpt.foreach(elem.setFrom)
+ limitOpt.foreach(elem.setLimit)
+ elem.setSinceLastPublish(sinceLast)
+
+ elem
+
+ } else {
+ throw new LoadingException("Calculation input has no single/multi configuration")
+ }
+ }
+
+ private def outputStratToXml(outputStrat: Calculations.OutputQuality.Strategy): OutputQualityEnum = {
+ outputStrat match {
+ case Calculations.OutputQuality.Strategy.ALWAYS_OK => OutputQualityEnum.ALWAYS_OK
+ case Calculations.OutputQuality.Strategy.WORST_QUALITY => OutputQualityEnum.WORST_QUALITY
+ }
+ }
+ private def outputStratToProto(outputStrat: OutputQualityEnum): Calculations.OutputQuality.Strategy = {
+ outputStrat match {
+ case OutputQualityEnum.ALWAYS_OK => Calculations.OutputQuality.Strategy.ALWAYS_OK
+ case OutputQualityEnum.WORST_QUALITY => Calculations.OutputQuality.Strategy.WORST_QUALITY
+ }
+ }
+
+ private def inputStratToXml(inputStrat: Calculations.InputQuality.Strategy): InputQualityEnum = {
+ inputStrat match {
+ case Calculations.InputQuality.Strategy.ACCEPT_ALL => InputQualityEnum.ACCEPT_ALL
+ case Calculations.InputQuality.Strategy.ONLY_WHEN_ALL_OK => InputQualityEnum.ONLY_WHEN_ALL_OK
+ case Calculations.InputQuality.Strategy.REMOVE_BAD_AND_CALC => InputQualityEnum.REMOVE_BAD_AND_CALC
+ }
+ }
+ private def inputStratToProto(inputStrat: InputQualityEnum): Calculations.InputQuality.Strategy = {
+ inputStrat match {
+ case InputQualityEnum.ACCEPT_ALL => Calculations.InputQuality.Strategy.ACCEPT_ALL
+ case InputQualityEnum.ONLY_WHEN_ALL_OK => Calculations.InputQuality.Strategy.ONLY_WHEN_ALL_OK
+ case InputQualityEnum.REMOVE_BAD_AND_CALC => Calculations.InputQuality.Strategy.REMOVE_BAD_AND_CALC
+ }
+ }
+}
diff --git a/loader-xml/src/main/scala/io/greenbus/ldr/KvFileStorage.scala b/loader-xml/src/main/scala/io/greenbus/ldr/KvFileStorage.scala
new file mode 100644
index 0000000..8cc88ce
--- /dev/null
+++ b/loader-xml/src/main/scala/io/greenbus/ldr/KvFileStorage.scala
@@ -0,0 +1,103 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.ldr
+
+import java.io.File
+
+import org.apache.commons.io.FileUtils
+import io.greenbus.loader.set.Mdl.FlatModelFragment
+import io.greenbus.loader.set._
+
+object KvFileStorage {
+
+ val ignored = Set("calculation", "triggerSet")
+
+ def mapKeyValuesToFileReferences(flat: FlatModelFragment, buildFilename: (String, String) => String): (FlatModelFragment, Seq[(String, Array[Byte])]) = {
+
+ def mapFields(f: EntityFields): (EntityFields, Seq[(String, Array[Byte])]) = {
+ val name = f.id match {
+ case FullEntId(_, name) => name
+ case NamedEntId(name) => name
+ case _ => throw new LoadingException("Names needs to be resolved to write key values")
+ }
+
+ val fileDescs = Vector.newBuilder[(String, Array[Byte])]
+
+ val mappedKvs = f.kvs.map {
+ case (key, vh) =>
+ if (ignored.contains(key)) {
+ (key, vh)
+ } else {
+ vh match {
+ case ByteArrayValue(bytes) =>
+ val filename = buildFilename(name, key)
+ fileDescs += ((filename, bytes))
+ (key, FileReference(filename))
+ case v => (key, v)
+ }
+ }
+ }
+
+ (f.copy(kvs = mappedKvs), fileDescs.result())
+ }
+
+ val refs = Vector.newBuilder[(String, Array[Byte])]
+
+ def genFieldMap(f: EntityFields): EntityFields = {
+ val (mappedFields, mappedRefs) = mapFields(f)
+ refs ++= mappedRefs
+ mappedFields
+ }
+
+ val ents = flat.modelEntities.map(obj => obj.copy(fields = genFieldMap(obj.fields)))
+ val points = flat.points.map(obj => obj.copy(fields = genFieldMap(obj.fields)))
+ val commands = flat.commands.map(obj => obj.copy(fields = genFieldMap(obj.fields)))
+ val endpoints = flat.endpoints.map(obj => obj.copy(fields = genFieldMap(obj.fields)))
+
+ val model = flat.copy(modelEntities = ents, points = points, commands = commands, endpoints = endpoints)
+
+ (model, refs.result())
+ }
+
+ def resolveFileReferences(flat: FlatModelFragment, parent: File): FlatModelFragment = {
+
+ def resolveFields(fields: EntityFields): EntityFields = {
+ val kvs = fields.kvs.map {
+ case (key, vh) =>
+ vh match {
+ case FileReference(filename) =>
+ val bytes = try {
+ FileUtils.readFileToByteArray(new File(parent, filename))
+ } catch {
+ case ex: Throwable => throw new LoadingException(s"Could not open file $filename: " + ex.getMessage)
+ }
+ (key, ByteArrayValue(bytes))
+ case _ => (key, vh)
+ }
+ }
+ fields.copy(kvs = kvs)
+ }
+
+ flat.copy(
+ modelEntities = flat.modelEntities.map(obj => obj.copy(fields = resolveFields(obj.fields))),
+ points = flat.points.map(obj => obj.copy(fields = resolveFields(obj.fields))),
+ commands = flat.commands.map(obj => obj.copy(fields = resolveFields(obj.fields))),
+ endpoints = flat.endpoints.map(obj => obj.copy(fields = resolveFields(obj.fields))))
+ }
+}
diff --git a/loader-xml/src/main/scala/io/greenbus/ldr/LoadingXmlException.scala b/loader-xml/src/main/scala/io/greenbus/ldr/LoadingXmlException.scala
new file mode 100644
index 0000000..c27acf6
--- /dev/null
+++ b/loader-xml/src/main/scala/io/greenbus/ldr/LoadingXmlException.scala
@@ -0,0 +1,23 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.ldr
+
+class LoadingXmlException(msg: String, elem: Any) extends Exception(msg) {
+ def element: Any = elem
+}
diff --git a/loader-xml/src/main/scala/io/greenbus/ldr/TreeToXml.scala b/loader-xml/src/main/scala/io/greenbus/ldr/TreeToXml.scala
new file mode 100644
index 0000000..918ea39
--- /dev/null
+++ b/loader-xml/src/main/scala/io/greenbus/ldr/TreeToXml.scala
@@ -0,0 +1,311 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.ldr
+
+import java.util.UUID
+
+import io.greenbus.ldr.xml.EndpointType.Source
+import io.greenbus.ldr.xml.PointType.{ Triggers, Commands }
+import io.greenbus.ldr.xml._
+import io.greenbus.client.service.proto.Calculations.CalculationDescriptor
+import io.greenbus.client.service.proto.Model.{ StoredValue, CommandCategory, PointCategory }
+import io.greenbus.client.service.proto.Processing.TriggerSet
+import io.greenbus.loader.set._
+import io.greenbus.loader.set.Mdl._
+import io.greenbus.loader.set
+
+object TreeToXml {
+
+ val triggerSetKey = "triggerSet"
+ val calculationKey = "calculation"
+
+ def xml(set: TreeModelFragment): Configuration = {
+
+ val equipModel = new EquipmentModel
+
+ val equipRoots: Seq[Equipment] = set.roots.sortBy(_.node.fields.id).map(equipNode).map {
+ case e: Equipment => e
+ case _ => throw new LoadingException("Roots must be equipment, not points or commands")
+ }
+
+ equipRoots.foreach(equipModel.getEquipment.add)
+ val config = new Configuration
+
+ config.setEquipmentModel(equipModel)
+
+ val endpoints = set.endpoints.sortBy(_.node.fields.id).map(endpointNodeToXml)
+
+ if (endpoints.nonEmpty) {
+ val endpointModel = new EndpointModel
+ endpoints.foreach(endpointModel.getEndpoint.add)
+ config.setEndpointModel(endpointModel)
+ }
+
+ config
+ }
+
+ def xmlType(typ: String) = {
+ val t = new Type
+ t.setName(typ)
+ t
+ }
+
+ private def entIdToRefXml(id: set.EntityId): Reference = {
+ val (optUuid, optName) = set.EntityId.optional(id)
+ val ref = new Reference
+ optName.foreach(ref.setName)
+ optUuid.map(_.toString).foreach(ref.setUuid)
+ ref
+ }
+
+ private def entIdAndTypeToRelationXml(id: set.EntityId, relation: String): Relationship = {
+ val (optUuid, optName) = set.EntityId.optional(id)
+ val rel = new Relationship
+ optName.foreach(rel.setName)
+ optUuid.map(_.toString).foreach(rel.setUuid)
+ rel.setRelation(relation)
+ rel
+ }
+
+ private def equipNode(node: TreeNode): AnyRef = {
+
+ def commonElements(
+ elements: java.util.List[AnyRef],
+ fields: EntityFields,
+ preExtractedCalc: Option[(CalculationDescriptor, Map[UUID, String])],
+ node: TreeNode)(implicit context: String): Unit = {
+
+ fields.types.toSeq.sorted.map(xmlType).foreach(elements.add)
+
+ val (kvs, triggerSetOpt, calcOpt) = mapKeyValues(fields.kvs, preExtractedCalc)
+
+ kvs.sortBy(_.getKey).foreach(elements.add)
+
+ if (node.extParentRefs.nonEmpty) {
+ val owners = new ExternalOwners
+ node.extParentRefs.sorted.map(entIdToRefXml).foreach(owners.getReference.add)
+ elements.add(owners)
+ }
+
+ if (node.otherChildRefs.nonEmpty) {
+ val refs = new ChildRelationships
+ node.otherChildRefs.sorted.map(tup => entIdAndTypeToRelationXml(tup._1, tup._2)).foreach(refs.getRelationship.add)
+ elements.add(refs)
+ }
+
+ if (node.otherParentRefs.nonEmpty) {
+ val parentRefs = new ParentRelationships
+ node.otherParentRefs.sorted.map(tup => entIdAndTypeToRelationXml(tup._1, tup._2)).foreach(parentRefs.getRelationship.add)
+ elements.add(parentRefs)
+ }
+
+ val subs = node.children.sortBy(_.node.fields.id).map(equipNode)
+ subs.foreach(elements.add)
+
+ triggerSetOpt.foreach(elements.add)
+ calcOpt.foreach(elements.add)
+ }
+
+ node.node match {
+ case desc: EntityDesc =>
+ val equip = new Equipment
+ val (optUuid, optName) = set.EntityId.optional(desc.fields.id)
+ optName.foreach(equip.setName)
+ optUuid.map(_.toString).foreach(equip.setUuid)
+
+ implicit val context = equip.getName
+
+ commonElements(equip.getElements, desc.fields, None, node)
+
+ equip
+
+ case desc: PointDesc =>
+ val xml = desc.pointCategory match {
+ case PointCategory.ANALOG => new Analog
+ case PointCategory.STATUS => new Status
+ case PointCategory.COUNTER => new Counter
+ }
+ val (optUuid, optName) = set.EntityId.optional(desc.fields.id)
+ optName.foreach(xml.setName)
+ xml.setUnit(desc.unit)
+ optUuid.map(_.toString).foreach(xml.setUuid)
+
+ implicit val context = xml.getName
+
+ if (node.commandRefs.nonEmpty) {
+ val commands = new Commands
+ node.commandRefs.sorted.map(entIdToRefXml).foreach(commands.getReference.add)
+ xml.getElements.add(commands)
+ }
+
+ val calcOpt = desc.calcHolder match {
+ case NoCalculation | CalcNotChecked => None
+ case NamesResolvedCalc(calc, uuidMap) => Some((calc, uuidMap))
+ case _: UnresolvedCalc => throw new LoadingException("Not expecting UnresolvedCalc in outputter")
+ }
+
+ commonElements(xml.getElements, desc.fields, calcOpt, node)
+
+ xml
+
+ case desc: CommandDesc =>
+ val xml = desc.commandCategory match {
+ case CommandCategory.CONTROL => new Control
+ case _ => new Setpoint
+ }
+
+ val (optUuid, optName) = set.EntityId.optional(desc.fields.id)
+ optName.foreach(xml.setName)
+ xml.setDisplayName(desc.displayName)
+ optUuid.map(_.toString).foreach(xml.setUuid)
+
+ implicit val context = xml.getName
+
+ commonElements(xml.getElements, desc.fields, None, node)
+
+ xml
+ }
+ }
+
+ private def endpointNodeToXml(node: EndpointNode): Endpoint = {
+
+ val xml = new Endpoint
+
+ val fields = node.node.fields
+
+ val (optUuid, optName) = set.EntityId.optional(fields.id)
+ optName.foreach(xml.setName)
+ optUuid.map(_.toString).foreach(xml.setUuid)
+
+ implicit val context = xml.getName
+
+ fields.types.toSeq.map(xmlType).foreach(xml.getElements.add)
+
+ xml.setProtocol(node.node.protocol)
+
+ val kvXmlElems = fields.kvs.distinct.map { case (key, v) => kvHolderToXml(key, v) }
+ kvXmlElems.foreach(xml.getElements.add)
+
+ if (node.otherChildRefs.nonEmpty) {
+ val refs = new ChildRelationships
+ node.otherChildRefs.sorted.map(tup => entIdAndTypeToRelationXml(tup._1, tup._2)).foreach(refs.getRelationship.add)
+ xml.getElements.add(refs)
+ }
+
+ if (node.otherParentRefs.nonEmpty) {
+ val parentRefs = new ParentRelationships
+ node.otherParentRefs.sorted.map(tup => entIdAndTypeToRelationXml(tup._1, tup._2)).foreach(parentRefs.getRelationship.add)
+ xml.getElements.add(parentRefs)
+ }
+
+ node.sourceRefs.sorted.foreach { id =>
+ val elem = new Source
+ val (optUuid, optName) = set.EntityId.optional(id)
+ optName.foreach(elem.setName)
+ optUuid.map(_.toString).foreach(elem.setUuid)
+ xml.getElements.add(elem)
+ }
+
+ xml
+ }
+
+ def storedValueToXml(result: KeyValue, v: StoredValue)(implicit context: String) {
+ if (v.hasBoolValue) {
+ result.setBooleanValue(v.getBoolValue)
+ } else if (v.hasInt32Value) {
+ result.setIntValue(v.getInt32Value)
+ } else if (v.hasInt64Value) {
+ result.setIntValue(v.getInt64Value)
+ } else if (v.hasUint32Value) {
+ result.setIntValue(v.getUint32Value)
+ } else if (v.hasUint64Value) {
+ result.setIntValue(v.getUint64Value)
+ } else if (v.hasDoubleValue) {
+ result.setDoubleValue(v.getDoubleValue)
+ } else if (v.hasStringValue) {
+ result.setStringValue(v.getStringValue)
+ } else {
+ throw new LoadingException(s"Could not convert key value to XML format for $context")
+ }
+ }
+
+ private def kvHolderToXml(key: String, v: ValueHolder)(implicit context: String) = {
+ val result = new KeyValue
+ result.setKey(key)
+
+ v match {
+ case SimpleValue(sv) => storedValueToXml(result, sv)
+ case ByteArrayValue(bytes) =>
+ val size = bytes.length
+ result.setBytesSize(size)
+ case FileReference(name) =>
+ result.setFileName(name)
+ case _ =>
+ throw new LoadingException(s"Could not handle key value type for $context")
+ }
+
+ result
+ }
+
+ def mapKeyValues(
+ kvs: Seq[(String, ValueHolder)],
+ calcOpt: Option[(CalculationDescriptor, Map[UUID, String])])(implicit context: String): (Seq[KeyValue], Option[Triggers], Option[Calculation]) = {
+
+ val (triggers, nonTriggers) = kvs.partition(_._1 == triggerSetKey)
+ val (calcs, nonTriggersOrCalcs) = nonTriggers.partition(_._1 == calculationKey)
+
+ val triggerSetProto = triggers.headOption.map(_._2).map {
+ case ByteArrayValue(bytes) => {
+ try {
+ TriggerSet.parseFrom(bytes)
+ } catch {
+ case ex: Throwable =>
+ throw new LoadingException("Could not parse trigger set stored value: " + ex + s" for $context")
+ }
+ }
+ case _ => throw new LoadingException(s"TriggerSet was not a byte array for $context")
+ }
+
+ val triggerXmlElem = triggerSetProto.map(proto => TriggerActionConversion.toXml(proto))
+
+ val calcXmlElem = calcOpt match {
+ case None =>
+ val calcProto = calcs.headOption.map(_._2).map {
+ case ByteArrayValue(bytes) => {
+ try {
+ CalculationDescriptor.parseFrom(bytes)
+ } catch {
+ case ex: Throwable =>
+ throw new LoadingException("Could not parse calculation stored value: " + ex + s" for $context")
+ }
+ }
+ case _ => throw new LoadingException(s"Calculation was not a byte array for $context")
+ }
+ calcProto.map(p => CalculationConversion.toXml(p, Map()))
+
+ case Some((desc, uuidToNameMap)) => Some(CalculationConversion.toXml(desc, uuidToNameMap))
+ }
+
+ val otherKvs = nonTriggersOrCalcs
+
+ val kvXmlElems = otherKvs.map { case (key, v) => kvHolderToXml(key, v) }
+
+ (kvXmlElems, triggerXmlElem, calcXmlElem)
+ }
+}
diff --git a/loader-xml/src/main/scala/io/greenbus/ldr/TriggerActionConversion.scala b/loader-xml/src/main/scala/io/greenbus/ldr/TriggerActionConversion.scala
new file mode 100644
index 0000000..68f55d2
--- /dev/null
+++ b/loader-xml/src/main/scala/io/greenbus/ldr/TriggerActionConversion.scala
@@ -0,0 +1,293 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.ldr
+
+import io.greenbus.client.service.proto.Processing
+import io.greenbus.client.service.proto.Processing.Filter.FilterType
+import io.greenbus.client.service.proto.Processing._
+import io.greenbus.ldr.xml.IntegerMapping.Mapping
+import io.greenbus.ldr.xml.PointType.Triggers
+import io.greenbus.ldr.xml._
+
+import scala.collection.JavaConversions._
+
+object TriggerActionConversion {
+
+ def triggersToProto(triggers: Triggers)(implicit context: String): TriggerSet = {
+ val protoTriggers = triggers.getTriggerGroup.map(triggerToProto)
+
+ TriggerSet.newBuilder()
+ .addAllTriggers(protoTriggers)
+ .build()
+ }
+
+ def triggerToProto(trig: TriggerBase)(implicit context: String) = {
+ val b: Trigger.Builder = trig match {
+ case elem: xml.Always =>
+ Trigger.newBuilder()
+
+ case elem: xml.Range =>
+ val b = AnalogLimit.newBuilder()
+ if (elem.isSetLow) b.setLowerLimit(elem.getLow)
+ if (elem.isSetHigh) b.setUpperLimit(elem.getHigh)
+ if (elem.isSetDeadband) b.setDeadband(elem.getDeadband)
+
+ Trigger.newBuilder().setAnalogLimit(b.build())
+
+ case elem: xml.Filter =>
+ val filter = if (elem.isSetDeadband) {
+ Processing.Filter.newBuilder()
+ .setType(Processing.Filter.FilterType.DEADBAND)
+ .setDeadbandValue(elem.getDeadband)
+ .build()
+ } else {
+ Processing.Filter.newBuilder()
+ .setType(Processing.Filter.FilterType.DUPLICATES_ONLY)
+ .build()
+ }
+
+ Trigger.newBuilder().setFilter(filter)
+
+ case elem: xml.MatchValue =>
+
+ val tb = Trigger.newBuilder()
+
+ if (elem.isSetBooleanValue) {
+ tb.setBoolValue(elem.getBooleanValue)
+ } else if (elem.isSetIntValue) {
+ tb.setIntValue(elem.getIntValue)
+ } else if (elem.isSetStringValue) {
+ tb.setStringValue(elem.getStringValue)
+ } else {
+ throw new LoadingXmlException(s"Missing value to match in MatchValue element for $context", elem)
+ }
+ tb
+
+ case x => throw new LoadingXmlException("Unhandled trigger element: " + x.getClass.getSimpleName + s" for $context", trig)
+ }
+
+ if (trig.isSetStopProcessingWhen) {
+ b.setStopProcessingWhen(activationTypeToProto(trig.getStopProcessingWhen))
+ }
+
+ val actions = trig.getActionGroup.map(xmlActionToProto)
+ b.addAllActions(actions)
+
+ b.build()
+ }
+
+ def toXml(proto: TriggerSet)(implicit context: String): PointType.Triggers = {
+
+ val triggersXml = new Triggers
+
+ proto.getTriggersList.foreach { trigger =>
+ if (trigger.hasAnalogLimit) {
+ val limit = trigger.getAnalogLimit
+
+ val range = new xml.Range()
+
+ if (limit.hasLowerLimit) range.setLow(limit.getLowerLimit)
+ if (limit.hasUpperLimit) range.setHigh(limit.getUpperLimit)
+ if (limit.hasDeadband) range.setDeadband(limit.getDeadband)
+
+ trigger.getActionsList.foreach(a => fillAction(a, range))
+ if (trigger.hasStopProcessingWhen) range.setStopProcessingWhen(activationTypeToXml(trigger.getStopProcessingWhen))
+ triggersXml.getTriggerGroup.add(range)
+
+ } else if (trigger.hasBoolValue) {
+ val matcher = new MatchValue
+ matcher.setBooleanValue(trigger.getBoolValue)
+ trigger.getActionsList.foreach(a => fillAction(a, matcher))
+ if (trigger.hasStopProcessingWhen) matcher.setStopProcessingWhen(activationTypeToXml(trigger.getStopProcessingWhen))
+ triggersXml.getTriggerGroup.add(matcher)
+
+ } else if (trigger.hasStringValue) {
+
+ val matcher = new MatchValue
+ matcher.setStringValue(trigger.getStringValue)
+ trigger.getActionsList.foreach(a => fillAction(a, matcher))
+ if (trigger.hasStopProcessingWhen) matcher.setStopProcessingWhen(activationTypeToXml(trigger.getStopProcessingWhen))
+ triggersXml.getTriggerGroup.add(matcher)
+
+ } else if (trigger.hasIntValue) {
+
+ val matcher = new MatchValue
+ matcher.setIntValue(trigger.getIntValue)
+ trigger.getActionsList.foreach(a => fillAction(a, matcher))
+ if (trigger.hasStopProcessingWhen) matcher.setStopProcessingWhen(activationTypeToXml(trigger.getStopProcessingWhen))
+ triggersXml.getTriggerGroup.add(matcher)
+
+ } else if (trigger.hasFilter) {
+
+ val filter = trigger.getFilter
+ val filterXml = new xml.Filter
+ filter.getType match {
+ case FilterType.DEADBAND =>
+ if (filter.hasDeadbandValue) filterXml.setDeadband(filter.getDeadbandValue)
+ case FilterType.DUPLICATES_ONLY =>
+ }
+
+ trigger.getActionsList.foreach(a => fillAction(a, filterXml))
+ if (trigger.hasStopProcessingWhen) filterXml.setStopProcessingWhen(activationTypeToXml(trigger.getStopProcessingWhen))
+ triggersXml.getTriggerGroup.add(filterXml)
+
+ } else {
+
+ val alwaysXml = new Always
+ trigger.getActionsList.foreach(a => fillAction(a, alwaysXml))
+ if (trigger.hasStopProcessingWhen) alwaysXml.setStopProcessingWhen(activationTypeToXml(trigger.getStopProcessingWhen))
+ triggersXml.getTriggerGroup.add(alwaysXml)
+ }
+ }
+
+ triggersXml
+ }
+
+ def xmlActionToProto(xmlAction: ActionType)(implicit context: String): Action = {
+ val actType = if (xmlAction.isSetActivation) xmlAction.getActivation else throw new LoadingXmlException(s"Missing activation type for action for $context", xmlAction)
+
+ val b = Action.newBuilder().setType(activationTypeToProto(actType))
+
+ xmlAction match {
+ case elem: xml.Suppress =>
+ b.setSuppress(true)
+ case elem: xml.StripValue =>
+ b.setStripValue(true)
+ case elem: xml.SetBool =>
+ val bValue = if (elem.isSetValue) elem.isValue else throw new LoadingXmlException(s"Missing bool value for SetBool for $context", elem)
+ b.setSetBool(bValue)
+ case elem: xml.Event =>
+ val eventConfig = if (elem.isSetEventType) elem.getEventType else throw new LoadingXmlException(s"Missing event config value for Event action for $context", elem)
+ b.setEvent(EventGeneration.newBuilder().setEventType(eventConfig).build())
+ case elem: xml.Scale =>
+ val scale = if (elem.isSetScale) elem.getScale else throw new LoadingXmlException(s"Missing scale for Scale element for $context", elem)
+ val offset = if (elem.isSetOffset) elem.getOffset else throw new LoadingXmlException(s"Missing offset for Scale element for $context", elem)
+ val forceToDoubleOpt = if (elem.isSetForceToDouble) Some(elem.getForceToDouble) else None
+
+ val ltb = LinearTransform.newBuilder()
+ .setScale(scale)
+ .setOffset(offset)
+
+ forceToDoubleOpt.foreach(ltb.setForceToDouble)
+
+ b.setLinearTransform(ltb.build())
+
+ case elem: xml.BoolMapping =>
+ val falseString = if (elem.isSetFalseString) elem.getFalseString else throw new LoadingXmlException(s"Missing falseString for BoolMapping element for $context", elem)
+ val trueString = if (elem.isSetTrueString) elem.getTrueString else throw new LoadingXmlException(s"Missing trueString for BoolMapping element for $context", elem)
+ b.setBoolTransform(BoolEnumTransform.newBuilder().setFalseString(falseString).setTrueString(trueString).build())
+
+ case elem: xml.IntegerMapping =>
+ val mappings = elem.getMapping.map { m =>
+ val fromInt = if (m.isSetFromInteger) m.getFromInteger else throw new LoadingXmlException(s"Need from integer in IntegerMapping for $context", elem)
+ val toString = if (m.isSetToString) m.getToString else throw new LoadingXmlException(s"Need to string in IntegerMapping for $context", elem)
+ IntToStringMapping.newBuilder().setValue(fromInt).setString(toString).build()
+ }
+
+ b.setIntTransform(IntEnumTransform.newBuilder().addAllMappings(mappings).build())
+
+ case _ => throw new LoadingXmlException(s"Unrecognized action type for $context", xmlAction)
+ }
+
+ b.build()
+ }
+
+ private def fillAction(action: Action, trigger: TriggerBase): Unit = {
+ if (action.hasSuppress) {
+ val elem = new xml.Suppress
+ elem.setActivation(activationTypeToXml(action.getType))
+ trigger.getActionGroup.add(elem)
+
+ } else if (action.hasStripValue) {
+ val elem = new StripValue
+ elem.setActivation(activationTypeToXml(action.getType))
+ trigger.getActionGroup.add(elem)
+
+ } else if (action.hasSetBool) {
+ val elem = new SetBool
+ elem.setActivation(activationTypeToXml(action.getType))
+ elem.setValue(action.getSetBool)
+ trigger.getActionGroup.add(elem)
+
+ } else if (action.hasEvent) {
+ val elem = new Event
+ elem.setActivation(activationTypeToXml(action.getType))
+ elem.setEventType(action.getEvent.getEventType)
+ trigger.getActionGroup.add(elem)
+
+ } else if (action.hasLinearTransform) {
+ val elem = new Scale
+ elem.setActivation(activationTypeToXml(action.getType))
+
+ val trans = action.getLinearTransform
+ if (trans.hasScale) elem.setScale(trans.getScale)
+ if (trans.hasOffset) elem.setOffset(trans.getOffset)
+ if (trans.hasForceToDouble) elem.setForceToDouble(trans.getForceToDouble)
+
+ trigger.getActionGroup.add(elem)
+
+ } else if (action.hasBoolTransform) {
+ val elem = new BoolMapping
+ elem.setActivation(activationTypeToXml(action.getType))
+
+ val boolTrans = action.getBoolTransform
+ if (boolTrans.hasFalseString) elem.setFalseString(boolTrans.getFalseString)
+ if (boolTrans.hasTrueString) elem.setTrueString(boolTrans.getTrueString)
+
+ trigger.getActionGroup.add(elem)
+
+ } else if (action.hasIntTransform) {
+ val elem = new IntegerMapping
+ elem.setActivation(activationTypeToXml(action.getType))
+
+ val intTransform = action.getIntTransform
+ val xmlMapElems = intTransform.getMappingsList.map { map =>
+ val xmlMap = new Mapping
+ xmlMap.setFromInteger(map.getValue)
+ xmlMap.setToString(map.getString)
+ xmlMap
+ }
+ xmlMapElems.foreach(elem.getMapping.add)
+
+ if (intTransform.hasDefaultValue) elem.setDefaultValue(intTransform.getDefaultValue)
+
+ trigger.getActionGroup.add(elem)
+ }
+ }
+
+ def activationTypeToXml(typ: ActivationType): ActivationConditionType = {
+ typ match {
+ case ActivationType.HIGH => ActivationConditionType.HIGH
+ case ActivationType.LOW => ActivationConditionType.LOW
+ case ActivationType.RISING => ActivationConditionType.RISING
+ case ActivationType.FALLING => ActivationConditionType.FALLING
+ case ActivationType.TRANSITION => ActivationConditionType.TRANSITION
+ }
+ }
+ def activationTypeToProto(typ: ActivationConditionType): ActivationType = {
+ typ match {
+ case ActivationConditionType.HIGH => ActivationType.HIGH
+ case ActivationConditionType.LOW => ActivationType.LOW
+ case ActivationConditionType.RISING => ActivationType.RISING
+ case ActivationConditionType.FALLING => ActivationType.FALLING
+ case ActivationConditionType.TRANSITION => ActivationType.TRANSITION
+ }
+ }
+
+}
diff --git a/loader-xml/src/main/scala/io/greenbus/ldr/XmlExporter.scala b/loader-xml/src/main/scala/io/greenbus/ldr/XmlExporter.scala
new file mode 100644
index 0000000..e060b2f
--- /dev/null
+++ b/loader-xml/src/main/scala/io/greenbus/ldr/XmlExporter.scala
@@ -0,0 +1,175 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.ldr
+
+import java.io.File
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.ldr.xml.Configuration
+import org.apache.commons.cli.{ HelpFormatter, Options }
+import org.apache.commons.io.FileUtils
+import io.greenbus.msg.amqp.AmqpSettings
+import io.greenbus.msg.qpid.QpidBroker
+import io.greenbus.client.ServiceConnection
+import io.greenbus.loader.set._
+import io.greenbus.util.{ XmlHelper, UserSettings }
+
+import scala.concurrent.duration._
+import org.apache.commons.cli
+
+object XmlExporter extends Logging {
+
+ val rootFlag = "root"
+ val outputFlag = "output"
+ val directoryFlag = "directory"
+ val kvFilesFlag = "kv-files"
+ val endpointFlag = "endpoint"
+
+ def buildOptions: Options = {
+ val opts = new Options
+ opts.addOption("h", "help", false, "Display this help text")
+ opts.addOption(new cli.Option(null, rootFlag, true, "Root entity to export"))
+ opts.addOption(new cli.Option("d", directoryFlag, true, "Directory to output files to"))
+ opts.addOption(new cli.Option("o", outputFlag, true, "Output filename"))
+ opts.addOption(new cli.Option("k", kvFilesFlag, false, s"Write key values to file. Must be used with --$directoryFlag"))
+ opts.addOption(new cli.Option(null, endpointFlag, true, "Additional Endpoints to export even if not connected to model fragment"))
+ opts
+ }
+
+ def main(args: Array[String]): Unit = {
+ try {
+ run(args)
+ } catch {
+ case ex: LoadingException =>
+ System.err.println(ex.getMessage)
+ case ex: io.greenbus.msg.amqp.util.LoadingException =>
+ System.err.println(ex.getMessage)
+ case ex: java.util.concurrent.TimeoutException =>
+ System.err.println("Service request timed out")
+ case ex: Throwable =>
+ logger.error("Unhandled exception: " + ex)
+ System.err.println("Error: " + ex.getMessage)
+ }
+ }
+
+ def run(args: Array[String]): Unit = {
+
+ val options = buildOptions
+ val parser = new cli.BasicParser
+ val line = parser.parse(options, args)
+
+ def printHelp() {
+ (new HelpFormatter).printHelp("loader file", options)
+ }
+
+ if (line.hasOption("h")) {
+ printHelp()
+ } else {
+
+ val roots = Option(line.getOptionValues(rootFlag)).map(_.toSeq).getOrElse(Seq())
+
+ val additionalEndpoints = Option(line.getOptionValues(endpointFlag)).map(_.toSeq).getOrElse(Seq())
+
+ val dirOpt = Option(line.getOptionValue(directoryFlag))
+
+ val outOpt = Option(line.getOptionValue(outputFlag))
+
+ val kvFiles = line.hasOption(kvFilesFlag)
+
+ if (roots.isEmpty) {
+ System.err.println("Must include one or more root entities.")
+ } else if (kvFiles && dirOpt.isEmpty) {
+ System.err.println("Must specify output directory when writing key values to files.")
+ } else {
+ export(roots, additionalEndpoints, outOpt, dirOpt, kvFiles)
+ }
+ }
+ }
+
+ def export(roots: Seq[String], additionalEndpoints: Seq[String], outputOpt: Option[String], dirOpt: Option[String], kvsToFiles: Boolean) = {
+
+ val baseDir = Option(System.getProperty("io.greenbus.config.base")).getOrElse("")
+ val amqpConfigPath = Option(System.getProperty("io.greenbus.config.amqp")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.msg.amqp.cfg")
+ val userConfigPath = Option(System.getProperty("io.greenbus.config.user")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.user.cfg")
+
+ val config = AmqpSettings.load(amqpConfigPath)
+ val userConfig = UserSettings.load(userConfigPath)
+
+ val conn = try {
+ ServiceConnection.connect(config, QpidBroker, 10000)
+ } catch {
+ case ex: java.io.IOException => throw new LoadingException(ex.getMessage)
+ }
+
+ try {
+
+ val session = conn.login(userConfig.user, userConfig.password, Duration(5000, MILLISECONDS))
+
+ val downloaded = Downloader.downloadByIds(session, roots.map(NamedEntId), additionalEndpoints.map(NamedEntId))
+
+ val flat = DownloadConversion.downloadToIntermediate(downloaded)
+
+ val flatResolved = NameResolver.resolveFlatModelUuids(session, flat)
+
+ val (flatFinal, kvFiles) = if (kvsToFiles) {
+ KvFileStorage.mapKeyValuesToFileReferences(flatResolved, (a, b) => s"$a.$b")
+ } else {
+ (flatResolved, Seq())
+ }
+
+ val tree = Mdl.flatToTree(flatFinal)
+
+ val xml = TreeToXml.xml(tree)
+
+ val text = XmlHelper.writeToString(xml, classOf[Configuration], formatted = true)
+
+ dirOpt match {
+ case Some(dirname) =>
+ writeInDir(text, outputOpt, dirname, kvFiles)
+ case None =>
+ outputOpt match {
+ case Some(filename) =>
+ FileUtils.write(new File(filename), text)
+ case None =>
+ println(text)
+ }
+ }
+
+ } finally {
+ conn.disconnect()
+ }
+ }
+
+ def writeInDir(main: String, outNameOpt: Option[String], dirname: String, kvs: Seq[(String, Array[Byte])]): Unit = {
+ val dirFile = new File(dirname)
+ if (!dirFile.exists()) {
+ dirFile.mkdir()
+ }
+
+ val mainFilename = outNameOpt.getOrElse("config.xml")
+
+ val mainFile = new File(dirFile, mainFilename)
+ FileUtils.write(mainFile, main)
+
+ kvs.foreach {
+ case (name, bytes) =>
+ FileUtils.writeByteArrayToFile(new File(dirFile, name), bytes)
+ }
+ }
+}
diff --git a/loader-xml/src/main/scala/io/greenbus/ldr/XmlImporter.scala b/loader-xml/src/main/scala/io/greenbus/ldr/XmlImporter.scala
new file mode 100644
index 0000000..a8f58b2
--- /dev/null
+++ b/loader-xml/src/main/scala/io/greenbus/ldr/XmlImporter.scala
@@ -0,0 +1,187 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.ldr
+
+import java.io.File
+import javax.xml.bind.UnmarshalException
+import javax.xml.stream.{ XMLStreamException, Location }
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.ldr.xml.Configuration
+import org.apache.commons.cli
+import org.apache.commons.cli.{ HelpFormatter, Options }
+import io.greenbus.msg.amqp.AmqpSettings
+import io.greenbus.msg.qpid.QpidBroker
+import io.greenbus.client.ServiceConnection
+import io.greenbus.loader.set.Mdl.FlatModelFragment
+import io.greenbus.loader.set._
+import io.greenbus.util.{ UserSettings, XmlHelper }
+
+import scala.concurrent.duration._
+
+object XmlImporter extends Logging {
+
+ val rootFlag = "root"
+
+ def buildOptions: Options = {
+ val opts = new Options
+ opts.addOption("h", "help", false, "Display this help text")
+ opts.addOption(new cli.Option(null, rootFlag, true, "Root entity to import"))
+ opts
+ }
+
+ def main(args: Array[String]): Unit = {
+ try {
+ run(args)
+ } catch {
+ case ex: LoadingException =>
+ System.err.println(ex.getMessage)
+ case ex: io.greenbus.msg.amqp.util.LoadingException =>
+ System.err.println(ex.getMessage)
+ case ex: java.util.concurrent.TimeoutException =>
+ System.err.println("Service request timed out")
+ case ex: UnmarshalException =>
+ logger.error("Unmarshal exception: " + ex)
+ Option(ex.getCause) match {
+ case Some(se: XMLStreamException) => System.err.println(se.getMessage)
+ case _ => System.err.println("Unknown error parsing XML.")
+ }
+ case ex: Throwable =>
+ logger.error("Unhandled exception: " + ex)
+ System.err.println("Error: " + ex.getMessage)
+ }
+ }
+
+ def run(args: Array[String]): Unit = {
+
+ val baseDir = Option(System.getProperty("io.greenbus.config.base")).getOrElse("")
+ val amqpConfigPath = Option(System.getProperty("io.greenbus.config.amqp")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.msg.amqp.cfg")
+ val userConfigPath = Option(System.getProperty("io.greenbus.config.user")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.user.cfg")
+
+ val config = AmqpSettings.load(amqpConfigPath)
+ val userConfig = UserSettings.load(userConfigPath)
+
+ val options = buildOptions
+ val parser = new cli.BasicParser
+ val line = parser.parse(options, args)
+
+ def printHelp() {
+ (new HelpFormatter).printHelp("loader file", options)
+ }
+
+ if (line.hasOption("h")) {
+ printHelp()
+ } else {
+ val roots = Option(line.getOptionValues(rootFlag)).map(_.toSeq).getOrElse(Seq())
+
+ if (line.getArgs.length < 1) {
+ System.err.println("Must include file to import.")
+ System.exit(1)
+ }
+ val filename = line.getArgs.head
+
+ val optCommandRoots = if (roots.nonEmpty) Some(roots) else None
+
+ val file = new File(filename)
+ val parentDir = file.getParentFile
+
+ val (xmlConfig, locationMap) = XmlHelper.readWithLocation(file, classOf[Configuration])
+
+ try {
+ importFromXml(config, userConfig, optCommandRoots, xmlConfig, parentDir, prompt = true)
+ } catch {
+ case ex: LoadingXmlException =>
+ throw new LoadingException(ex.getMessage + " " + locationError(ex.element, locationMap))
+ }
+ }
+ }
+
+ def locationError(obj: Any, locations: Map[Any, Location]): String = {
+ locations.get(obj) match {
+ case None => "[line: ?, col: ?]"
+ case Some(loc) => s"[line: ${loc.getLineNumber}, col: ${loc.getColumnNumber}]"
+ }
+ }
+
+ def importFromXml(
+ amqpConfig: AmqpSettings,
+ userConfig: UserSettings,
+ optCommandRoots: Option[Seq[String]],
+ xmlConfig: Configuration,
+ parentDir: File,
+ prompt: Boolean = true) = {
+
+ val treeRepresentation = XmlToModel.convert(xmlConfig)
+
+ val xmlRootIds = treeRepresentation.roots.map(_.node.fields.id)
+
+ val flatModel = Mdl.treeToFlat(treeRepresentation)
+
+ val flatFilesResolved = KvFileStorage.resolveFileReferences(flatModel, parentDir)
+
+ importFragment(amqpConfig, userConfig, optCommandRoots, xmlRootIds, flatFilesResolved, prompt)
+ }
+
+ def importFragment(
+ amqpConfig: AmqpSettings,
+ userConfig: UserSettings,
+ optCommandRoots: Option[Seq[String]],
+ xmlRoots: Seq[EntityId],
+ flatModel: FlatModelFragment,
+ prompt: Boolean = true) = {
+
+ val conn = try {
+ ServiceConnection.connect(amqpConfig, QpidBroker, 10000)
+ } catch {
+ case ex: java.io.IOException => throw new LoadingException(ex.getMessage)
+ }
+
+ try {
+
+ val fragmentEndpointIds = flatModel.endpoints.map(_.fields.id)
+
+ val session = conn.login(userConfig.user, userConfig.password, Duration(5000, MILLISECONDS))
+
+ val downloadSet = optCommandRoots.map { roots =>
+ Downloader.downloadByIdsAndNames(session, xmlRoots, roots, fragmentEndpointIds)
+ }.getOrElse {
+ Downloader.downloadByIds(session, xmlRoots, fragmentEndpointIds)
+ }
+
+ val ((actions, diff), idTuples) = Importer.importDiff(session, flatModel, downloadSet)
+
+ Importer.summarize(diff)
+
+ if (!diff.isEmpty) {
+ if (prompt) {
+ println("Proceed with modifications? (y/N)")
+ val answer = readLine()
+ if (Set("y", "yes").contains(answer.trim.toLowerCase)) {
+ Upload.push(session, actions, idTuples)
+ }
+ } else {
+ Upload.push(session, actions, idTuples)
+ }
+ }
+
+ } finally {
+ conn.disconnect()
+ }
+ }
+}
diff --git a/loader-xml/src/main/scala/io/greenbus/ldr/XmlToModel.scala b/loader-xml/src/main/scala/io/greenbus/ldr/XmlToModel.scala
new file mode 100644
index 0000000..d553a8b
--- /dev/null
+++ b/loader-xml/src/main/scala/io/greenbus/ldr/XmlToModel.scala
@@ -0,0 +1,301 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.ldr
+
+import java.util.UUID
+
+import io.greenbus.ldr.xml.{ SetpointType, Configuration, Equipment }
+import io.greenbus.client.service.proto.Model.{ CommandCategory, PointCategory, StoredValue }
+import io.greenbus.loader.set._
+
+import scala.collection.JavaConversions._
+
+object XmlToModel {
+ import io.greenbus.loader.set.Mdl._
+
+ def classFilter[A](seq: Seq[AnyRef], klass: Class[A]) = {
+ seq.filter(_.getClass == klass).map(_.asInstanceOf[A])
+ }
+
+ def convert(root: Configuration): TreeModelFragment = {
+ val nodes = if (root.isSetEquipmentModel) {
+ val equipModel = root.getEquipmentModel
+ equipModel.getEquipment.map(traverse)
+ } else {
+ Seq()
+ }
+
+ val endpoints = if (root.isSetEndpointModel) {
+ val endModel = root.getEndpointModel
+ endModel.getEndpoint.map(convertEndpoint)
+ } else {
+ Seq()
+ }
+
+ TreeModelFragment(nodes, endpoints)
+ }
+
+ def keyValFromXml(kvElem: xml.KeyValue) = {
+ val key = if (kvElem.isSetKey) kvElem.getKey else throw new LoadingXmlException("KeyValue element missing key name", kvElem)
+
+ val valueHolder = if (kvElem.isSetBooleanValue) {
+ SimpleValue(StoredValue.newBuilder().setBoolValue(kvElem.getBooleanValue).build())
+ } else if (kvElem.isSetStringValue) {
+ SimpleValue(StoredValue.newBuilder().setStringValue(kvElem.getStringValue).build())
+ } else if (kvElem.isSetIntValue) {
+ SimpleValue(StoredValue.newBuilder().setInt64Value(kvElem.getIntValue).build())
+ } else if (kvElem.isSetDoubleValue) {
+ SimpleValue(StoredValue.newBuilder().setDoubleValue(kvElem.getDoubleValue).build())
+ } else if (kvElem.isSetBytesSize) {
+ ByteArrayReference(kvElem.getBytesSize)
+ } else if (kvElem.isSetFileName) {
+ FileReference(kvElem.getFileName)
+ } else {
+ throw new LoadingXmlException("Could not interpret key value", kvElem)
+ }
+
+ (key, valueHolder)
+ }
+
+ def extractOwnerRefs(elems: java.util.List[AnyRef]): Seq[EntityId] = {
+ val externalOwnersOpt = classFilter(elems, classOf[xml.ExternalOwners]).headOption
+ externalOwnersOpt.map { extOwners =>
+ extOwners.getReference.map { ref =>
+ val nameOpt = if (ref.isSetName) Some(ref.getName) else None
+ val uuidOpt = if (ref.isSetUuid) Some(ref.getUuid) else None
+ if (nameOpt.isEmpty && uuidOpt.isEmpty) throw new LoadingXmlException("Reference must have name or UUID", ref)
+ EntityId(uuidOpt.map(UUID.fromString), nameOpt)
+ }
+ }.getOrElse(Seq())
+ }
+
+ def extractCommandRefs(elems: java.util.List[AnyRef]): Seq[EntityId] = {
+ val externalOwnersOpt = classFilter(elems, classOf[xml.PointType.Commands]).headOption
+ externalOwnersOpt.map { extOwners =>
+ extOwners.getReference.map { ref =>
+ val nameOpt = if (ref.isSetName) Some(ref.getName) else None
+ val uuidOpt = if (ref.isSetUuid) Some(ref.getUuid) else None
+ if (nameOpt.isEmpty && uuidOpt.isEmpty) throw new LoadingXmlException("Reference must have name or UUID", ref)
+ EntityId(uuidOpt.map(UUID.fromString), nameOpt)
+ }
+ }.getOrElse(Seq())
+ }
+
+ def extractParentRefs(elems: java.util.List[AnyRef]): Seq[(EntityId, String)] = {
+ val elemSeq = classFilter(elems, classOf[xml.ParentRelationships]).headOption
+ elemSeq.map { elem =>
+ elem.getRelationship.map { rel =>
+ val nameOpt = if (rel.isSetName) Some(rel.getName) else None
+ val uuidOpt = if (rel.isSetUuid) Some(rel.getUuid) else None
+ if (nameOpt.isEmpty && uuidOpt.isEmpty) throw new LoadingXmlException("Reference must have name or UUID", rel)
+ val relation = if (rel.isSetRelation) rel.getRelation else throw new LoadingXmlException("Edge reference missing relationship", rel)
+ (EntityId(uuidOpt.map(UUID.fromString), nameOpt), relation)
+ }
+ }.getOrElse(Seq())
+ }
+
+ def extractChildRefs(elems: java.util.List[AnyRef]): Seq[(EntityId, String)] = {
+ val elemSeq = classFilter(elems, classOf[xml.ChildRelationships]).headOption
+ elemSeq.map { elem =>
+ elem.getRelationship.map { rel =>
+ val nameOpt = if (rel.isSetName) Some(rel.getName) else None
+ val uuidOpt = if (rel.isSetUuid) Some(rel.getUuid) else None
+ if (nameOpt.isEmpty && uuidOpt.isEmpty) throw new LoadingXmlException("Reference must have name or UUID", rel)
+ val relation = if (rel.isSetRelation) rel.getRelation else throw new LoadingXmlException("Edge reference missing relationship", rel)
+ (EntityId(uuidOpt.map(UUID.fromString), nameOpt), relation)
+ }
+ }.getOrElse(Seq())
+ }
+
+ def extractSourceRefs(elems: java.util.List[AnyRef]): Seq[EntityId] = {
+ val sourceRefs = classFilter(elems, classOf[xml.EndpointType.Source])
+ sourceRefs.map { ref =>
+ val nameOpt = if (ref.isSetName) Some(ref.getName) else None
+ val uuidOpt = if (ref.isSetUuid) Some(ref.getUuid) else None
+ if (nameOpt.isEmpty && uuidOpt.isEmpty) throw new LoadingXmlException("Reference must have name or UUID", ref)
+ EntityId(uuidOpt.map(UUID.fromString), nameOpt)
+ }
+ }
+
+ def traverse(equip: Equipment): TreeNode = {
+ val nameOpt = if (equip.isSetName) Some(equip.getName) else None
+ val uuidOpt = if (equip.isSetUuid) Some(equip.getUuid) else None
+ if (nameOpt.isEmpty && uuidOpt.isEmpty) throw new LoadingXmlException("Equipment must have name or UUID", equip)
+
+ val types = classFilter(equip.getElements, classOf[xml.Type]).map { elem =>
+ if (elem.isSetName) elem.getName else throw new LoadingXmlException(s"Type element missing type name for ${equip.getName}", elem)
+ }
+
+ val keysAndHolders = classFilter(equip.getElements, classOf[xml.KeyValue]).map(keyValFromXml)
+
+ val ownerRefs = extractOwnerRefs(equip.getElements)
+
+ val parentRefs = extractParentRefs(equip.getElements)
+
+ val childRefs = extractChildRefs(equip.getElements)
+
+ val childEquips = classFilter(equip.getElements, classOf[xml.Equipment]).map(traverse)
+
+ val points = (classFilter(equip.getElements, classOf[xml.Analog]) ++
+ classFilter(equip.getElements, classOf[xml.Status]) ++
+ classFilter(equip.getElements, classOf[xml.Counter])).map(traverse)
+
+ val commands = (classFilter(equip.getElements, classOf[xml.Control]) ++
+ classFilter(equip.getElements, classOf[xml.Setpoint])).map(traverse)
+
+ val desc = EntityDesc(
+ EntityFields(
+ EntityId(uuidOpt.map(UUID.fromString), nameOpt),
+ types.toSet,
+ keysAndHolders))
+
+ TreeNode(desc, childEquips ++ points ++ commands, ownerRefs, parentRefs, childRefs, Seq())
+ }
+
+ def traverse(pt: xml.PointType): TreeNode = {
+ val nameOpt = if (pt.isSetName) Some(pt.getName) else None
+ val uuidOpt = if (pt.isSetUuid) Some(pt.getUuid) else None
+ if (nameOpt.isEmpty && uuidOpt.isEmpty) throw new LoadingXmlException("Point must have name or UUID", pt)
+
+ implicit val context = pt.getName
+
+ val types = classFilter(pt.getElements, classOf[xml.Type]).map { elem =>
+ if (elem.isSetName) elem.getName else throw new LoadingXmlException(s"Type element missing type name for ${pt.getName}", elem)
+ }
+
+ val keysAndHolders = classFilter(pt.getElements, classOf[xml.KeyValue]).map(keyValFromXml)
+
+ val triggerSetKvOpt = {
+ val triggerCollOpt = classFilter(pt.getElements, classOf[xml.PointType.Triggers]).headOption
+
+ val triggerSetOpt = triggerCollOpt.map(TriggerActionConversion.triggersToProto)
+
+ triggerSetOpt.map(ts => (TreeToXml.triggerSetKey, ByteArrayValue(ts.toByteArray))).toSeq
+ }
+
+ val calcOpt = classFilter(pt.getElements, classOf[xml.Calculation])
+ .headOption
+ .map(CalculationConversion.toProto)
+ .map(tup => UnresolvedCalc(tup._1, tup._2))
+
+ val keysAndTrigger = keysAndHolders ++ Seq(triggerSetKvOpt).flatten
+
+ val pointCategory = pt match {
+ case _: xml.Analog => PointCategory.ANALOG
+ case _: xml.Status => PointCategory.STATUS
+ case _: xml.Counter => PointCategory.COUNTER
+ }
+
+ val unit = if (pt.isSetUnit) pt.getUnit else throw new LoadingXmlException(s"Unit missing from point element for ${pt.getName}", pt)
+
+ val ownerRefs = extractOwnerRefs(pt.getElements)
+
+ val parentRefs = extractParentRefs(pt.getElements)
+
+ val childRefs = extractChildRefs(pt.getElements)
+
+ val commandRefs = extractCommandRefs(pt.getElements)
+
+ val desc = PointDesc(
+ EntityFields(
+ EntityId(uuidOpt.map(UUID.fromString), nameOpt),
+ types.toSet,
+ keysAndTrigger),
+ pointCategory,
+ unit,
+ calcOpt.getOrElse(NoCalculation))
+
+ TreeNode(desc, Seq(), ownerRefs, parentRefs, childRefs, commandRefs)
+ }
+
+ def traverse(elem: xml.Command): TreeNode = {
+ val nameOpt = if (elem.isSetName) Some(elem.getName) else None
+ val uuidOpt = if (elem.isSetUuid) Some(elem.getUuid) else None
+ if (nameOpt.isEmpty && uuidOpt.isEmpty) throw new LoadingXmlException("Command must have name or UUID", elem)
+
+ val types = classFilter(elem.getElements, classOf[xml.Type]).map { elem =>
+ if (elem.isSetName) elem.getName else throw new LoadingXmlException(s"Type element missing type name for ${elem.getName}", elem)
+ }
+
+ val keysAndHolders = classFilter(elem.getElements, classOf[xml.KeyValue]).map(keyValFromXml)
+
+ val commandCategory = elem match {
+ case _: xml.Control => CommandCategory.CONTROL
+ case sp: xml.Setpoint =>
+ if (sp.isSetSetpointType) {
+ sp.getSetpointType match {
+ case SetpointType.DOUBLE => CommandCategory.SETPOINT_DOUBLE
+ case SetpointType.INTEGER => CommandCategory.SETPOINT_INT
+ case SetpointType.STRING => CommandCategory.SETPOINT_STRING
+ }
+ } else {
+ CommandCategory.SETPOINT_DOUBLE
+ }
+ }
+
+ val displayName = if (elem.isSetDisplayName) elem.getDisplayName else throw new LoadingXmlException(s"Display name missing from command element for ${elem.getName}", elem)
+
+ val ownerRefs = extractOwnerRefs(elem.getElements)
+
+ val parentRefs = extractParentRefs(elem.getElements)
+
+ val childRefs = extractChildRefs(elem.getElements)
+
+ val desc = CommandDesc(
+ EntityFields(
+ EntityId(uuidOpt.map(UUID.fromString), nameOpt),
+ types.toSet,
+ keysAndHolders),
+ displayName,
+ commandCategory)
+
+ TreeNode(desc, Seq(), ownerRefs, parentRefs, childRefs, Seq())
+ }
+
+ def convertEndpoint(elem: xml.Endpoint): EndpointNode = {
+ val nameOpt = if (elem.isSetName) Some(elem.getName) else None
+ val uuidOpt = if (elem.isSetUuid) Some(elem.getUuid) else None
+ if (nameOpt.isEmpty && uuidOpt.isEmpty) throw new LoadingXmlException("Endpoint must have name or UUID", elem)
+
+ val protocol = if (elem.isSetProtocol) elem.getProtocol else throw new LoadingException(s"Protocol name must be set for endpoint for ${elem.getName}")
+
+ val types = classFilter(elem.getElements, classOf[xml.Type]).map { elem =>
+ if (elem.isSetName) elem.getName else throw new LoadingXmlException(s"Type element missing type name for ${elem.getName}", elem)
+ }
+
+ val keysAndHolders = classFilter(elem.getElements, classOf[xml.KeyValue]).map(keyValFromXml)
+
+ val sourceRefs = extractSourceRefs(elem.getElements)
+
+ val parentRefs = extractParentRefs(elem.getElements)
+
+ val childRefs = extractChildRefs(elem.getElements)
+
+ EndpointNode(
+ EndpointDesc(
+ EntityFields(
+ EntityId(uuidOpt.map(UUID.fromString), nameOpt),
+ types.toSet,
+ keysAndHolders),
+ protocol),
+ sourceRefs,
+ parentRefs,
+ childRefs)
+ }
+}
diff --git a/loader-xml/src/main/scala/io/greenbus/ldr/auth/AuthConversion.scala b/loader-xml/src/main/scala/io/greenbus/ldr/auth/AuthConversion.scala
new file mode 100644
index 0000000..53c7438
--- /dev/null
+++ b/loader-xml/src/main/scala/io/greenbus/ldr/auth/AuthConversion.scala
@@ -0,0 +1,199 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.ldr.auth
+
+import java.util.UUID
+
+import io.greenbus.ldr.xml.auth._
+import io.greenbus.ldr.XmlToModel
+import io.greenbus.loader.set.LoadingException
+
+import scala.collection.JavaConversions._
+
+object AuthConversion {
+
+ def toXml(authMdl: AuthMdl.AuthSet): Authorization = {
+
+ val xmlSets = authMdl.permSets.map(toXml)
+
+ val xmlAgents = authMdl.agents.map(toXml)
+
+ val xml = new Authorization
+
+ val agentColl = new Agents
+ xmlAgents.foreach(agentColl.getAgent.add)
+
+ val permsColl = new PermissionSets
+ xmlSets.foreach(permsColl.getPermissionSet.add)
+
+ xml.setAgents(agentColl)
+ xml.setPermissionSets(permsColl)
+
+ xml
+ }
+
+ def toXml(agent: AuthMdl.Agent): Agent = {
+ val xml = new Agent
+
+ agent.uuidOpt.foreach(u => xml.setUuid(u.toString))
+ xml.setName(agent.name)
+
+ val xmlPermRefs = agent.permissions.map { p =>
+ val ps = new Agent.PermissionSet
+ ps.setName(p)
+ ps
+ }
+
+ xmlPermRefs.foreach(xml.getPermissionSet.add)
+
+ xml
+ }
+
+ def toXml(permSet: AuthMdl.PermissionSet): PermissionSet = {
+
+ val xml = new PermissionSet
+
+ permSet.idOpt.foreach(id => xml.setId(id.toString))
+ xml.setName(permSet.name)
+
+ permSet.permissions.map(toXml).foreach(xml.getPermissions.add)
+
+ xml
+ }
+
+ def toXml(perm: AuthMdl.Permission): PermissionType = {
+
+ val xml = if (perm.allow) new Allow else new Deny
+
+ val actions = perm.actions.map { a =>
+ val obj = new Action
+ obj.setName(a)
+ obj
+ }
+
+ val resources = perm.resources.map { r =>
+ val obj = new Resource
+ obj.setName(r)
+ obj
+ }
+
+ val selectors = perm.selectors.map { sel =>
+ val xmlSelector = new Selector
+ xmlSelector.setStyle(sel.style)
+
+ val xmlArgs = sel.args.map { arg =>
+ val a = new Argument
+ a.setValue(arg)
+ a
+ }
+ xmlArgs.foreach(xmlSelector.getArgument.add)
+
+ xmlSelector
+ }
+
+ actions.foreach(xml.getElements.add)
+ resources.foreach(xml.getElements.add)
+ selectors.foreach(xml.getElements.add)
+
+ xml
+ }
+
+ def toModel(set: Authorization): AuthMdl.AuthSet = {
+
+ val agents = if (set.isSetAgents) {
+ set.getAgents.getAgent.map(toModel)
+ } else {
+ Seq()
+ }
+
+ val permissionSets = if (set.isSetPermissionSets) {
+ set.getPermissionSets.getPermissionSet.map(toModel)
+ } else {
+ Seq()
+ }
+
+ AuthMdl.AuthSet(permissionSets, agents)
+ }
+
+ def toModel(agent: Agent): AuthMdl.Agent = {
+
+ val name = if (agent.isSetName) agent.getName else throw new LoadingException("PermissionSet must include a name")
+
+ val idOpt = if (agent.isSetUuid) {
+ try {
+ Some(UUID.fromString(agent.getUuid))
+ } catch {
+ case ex: Throwable =>
+ throw new LoadingException(s"UUID for Agent $name must be numeric")
+ }
+ } else {
+ None
+ }
+
+ val permNames = agent.getPermissionSet.map { obj =>
+ if (obj.isSetName) obj.getName else throw new LoadingException(s"Agent permissions for $name must include a name")
+ }
+
+ AuthMdl.Agent(idOpt, name, permNames)
+ }
+
+ def toModel(permSet: PermissionSet): AuthMdl.PermissionSet = {
+
+ val name = if (permSet.isSetName) permSet.getName else throw new LoadingException("PermissionSet must include a name")
+
+ val idOpt = if (permSet.isSetId) {
+ val idStr = permSet.getId
+ val idLong = try { idStr.toLong } catch { case ex: Throwable => throw new LoadingException(s"ID for PermissionSet $name must be numeric") }
+ Some(idLong)
+ } else {
+ None
+ }
+
+ val perms = permSet.getPermissions.map(toModel)
+
+ AuthMdl.PermissionSet(idOpt, name, perms)
+ }
+
+ def toModel(permissionType: PermissionType): AuthMdl.Permission = {
+
+ val isAllow = permissionType match {
+ case _: Allow => true
+ case _: Deny => false
+ }
+
+ val resources = XmlToModel.classFilter(permissionType.getElements.toSeq, classOf[Resource]).map { obj =>
+ if (obj.isSetName) obj.getName else throw new LoadingException("Resources must include a name")
+ }
+
+ val actions = XmlToModel.classFilter(permissionType.getElements.toSeq, classOf[Action]).map { obj =>
+ if (obj.isSetName) obj.getName else throw new LoadingException("Actions must include a name")
+ }
+
+ val selectors = XmlToModel.classFilter(permissionType.getElements.toSeq, classOf[Selector]).map { obj =>
+ val style = if (obj.isSetStyle) obj.getStyle else throw new LoadingException("Selectors must include a style")
+ val args = obj.getArgument.map { arg =>
+ if (arg.isSetValue) arg.getValue else throw new LoadingException("Arguments must include a value")
+ }
+ AuthMdl.EntitySelector(style, args)
+ }
+
+ AuthMdl.Permission(isAllow, resources, actions, selectors)
+ }
+
+}
diff --git a/loader-xml/src/main/scala/io/greenbus/ldr/auth/AuthMdl.scala b/loader-xml/src/main/scala/io/greenbus/ldr/auth/AuthMdl.scala
new file mode 100644
index 0000000..9fba560
--- /dev/null
+++ b/loader-xml/src/main/scala/io/greenbus/ldr/auth/AuthMdl.scala
@@ -0,0 +1,47 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.ldr.auth
+
+import java.util.UUID
+
+object AuthMdl {
+
+ case class EntitySelector(style: String, args: Seq[String])
+
+ case class Permission(allow: Boolean, resources: Seq[String], actions: Seq[String], selectors: Seq[EntitySelector])
+
+ case class PermissionSet(idOpt: Option[Long], name: String, permissions: Seq[Permission])
+
+ case class Agent(uuidOpt: Option[UUID], name: String, permissions: Seq[String], passwordOpt: Option[String] = None)
+
+ case class AuthSet(permSets: Seq[PermissionSet], agents: Seq[Agent])
+
+ def getIdSetForAgents(agents: Seq[Agent]): (Seq[UUID], Seq[String]) = {
+ val uuids = Vector.newBuilder[UUID]
+ val names = Vector.newBuilder[String]
+
+ agents.foreach { agent =>
+ agent.uuidOpt match {
+ case Some(uuid) => uuids += uuid
+ case None => names += agent.name
+ }
+ }
+ (uuids.result(), names.result())
+ }
+}
diff --git a/loader-xml/src/main/scala/io/greenbus/ldr/auth/Download.scala b/loader-xml/src/main/scala/io/greenbus/ldr/auth/Download.scala
new file mode 100644
index 0000000..cfd3b75
--- /dev/null
+++ b/loader-xml/src/main/scala/io/greenbus/ldr/auth/Download.scala
@@ -0,0 +1,119 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.ldr.auth
+
+import java.util.UUID
+
+import io.greenbus.client.service.proto.ModelRequests.EntityPagingParams
+import io.greenbus.msg.Session
+import io.greenbus.client.service.AuthService
+import io.greenbus.client.service.proto.Auth.{ Permission, Agent, PermissionSet }
+import io.greenbus.client.service.proto.AuthRequests.{ AgentKeySet, AgentQuery, PermissionSetQuery }
+import io.greenbus.client.service.proto.Model.{ ModelUUID, ModelID }
+import io.greenbus.ldr.auth.Download.AuthDownloadSet
+import io.greenbus.loader.set.{ UUIDHelpers, Downloader }
+
+import scala.collection.JavaConversions._
+import scala.concurrent.duration._
+
+import scala.concurrent.{ Await, Future }
+
+object Download {
+
+ case class AuthDownloadSet(perms: Seq[PermissionSet], agents: Seq[Agent])
+
+ def getAgents(session: Session, uuids: Seq[UUID], names: Seq[String]): Seq[Agent] = {
+ val client = AuthService.client(session)
+
+ val allKeys = uuids ++ names
+
+ allKeys.grouped(300).flatMap { keyGroup =>
+ val keySet = AgentKeySet.newBuilder()
+
+ keyGroup.foreach {
+ case uuid: UUID => keySet.addUuids(UUIDHelpers.uuidToProtoUUID(uuid))
+ case name: String => keySet.addNames(name)
+ }
+
+ Await.result(client.getAgents(keySet.build()), 10000.milliseconds)
+ }.toVector
+ }
+
+ def download(session: Session) = {
+ val client = AuthService.client(session)
+
+ def permQuery(last: Option[ModelID], size: Int): Future[Seq[PermissionSet]] = {
+ val b = PermissionSetQuery.newBuilder().setPageSize(size)
+ last.foreach(b.setLastId)
+ client.permissionSetQuery(b.build())
+ }
+
+ def permPageBoundary(results: Seq[PermissionSet]): ModelID = {
+ results.last.getId
+ }
+
+ val permSets = Downloader.allPages(200, permQuery, permPageBoundary)
+
+ def agentQuery(last: Option[ModelUUID], size: Int): Future[Seq[Agent]] = {
+
+ val pageB = EntityPagingParams.newBuilder().setPageSize(size)
+
+ last.foreach(pageB.setLastUuid)
+
+ client.agentQuery(AgentQuery.newBuilder().setPagingParams(pageB.build()).build())
+ }
+
+ def agentPageBoundary(results: Seq[Agent]): ModelUUID = {
+ results.last.getUuid
+ }
+
+ val agents = Downloader.allPages(200, agentQuery, agentPageBoundary)
+
+ AuthDownloadSet(permSets, agents)
+ }
+
+}
+
+object DownloadConversion {
+
+ def toIntermediate(downloadSet: AuthDownloadSet): AuthMdl.AuthSet = {
+ val permSets = downloadSet.perms.map { permSet =>
+ AuthMdl.PermissionSet(Some(UUIDHelpers.protoIdToLong(permSet.getId)), permSet.getName, permSet.getPermissionsList.map(permissionToMdl))
+ }
+
+ val agents = downloadSet.agents.map { agent =>
+ AuthMdl.Agent(Some(UUIDHelpers.protoUUIDToUuid(agent.getUuid)), agent.getName, agent.getPermissionSetsList.toSeq)
+ }
+
+ AuthMdl.AuthSet(permSets, agents)
+ }
+
+ def permissionToMdl(proto: Permission): AuthMdl.Permission = {
+ val allow = proto.getAllow
+ val actions = proto.getActionsList.toSeq
+ val resources = proto.getResourcesList.toSeq
+
+ val selectors = proto.getSelectorsList.map { selector =>
+ AuthMdl.EntitySelector(selector.getStyle, selector.getArgumentsList.toSeq)
+ }
+
+ AuthMdl.Permission(allow, resources = resources, actions = actions, selectors)
+ }
+
+}
diff --git a/loader-xml/src/main/scala/io/greenbus/ldr/auth/Exporter.scala b/loader-xml/src/main/scala/io/greenbus/ldr/auth/Exporter.scala
new file mode 100644
index 0000000..00415b5
--- /dev/null
+++ b/loader-xml/src/main/scala/io/greenbus/ldr/auth/Exporter.scala
@@ -0,0 +1,141 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.ldr.auth
+
+import java.io.File
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.ldr.xml.auth.Authorization
+import org.apache.commons.cli
+import org.apache.commons.cli.{ HelpFormatter, Options }
+import org.apache.commons.io.FileUtils
+import io.greenbus.msg.amqp.AmqpSettings
+import io.greenbus.msg.qpid.QpidBroker
+import io.greenbus.client.ServiceConnection
+import io.greenbus.loader.set.LoadingException
+import io.greenbus.util.{ UserSettings, XmlHelper }
+
+import scala.concurrent.duration._
+
+object Exporter extends Logging {
+
+ val outputFlag = "output"
+ val directoryFlag = "directory"
+
+ def buildOptions: Options = {
+ val opts = new Options
+ opts.addOption("h", "help", false, "Display this help text")
+ opts.addOption(new cli.Option("d", directoryFlag, true, "Directory to output files to"))
+ opts.addOption(new cli.Option("o", outputFlag, true, "Output filename"))
+ opts
+ }
+
+ def main(args: Array[String]): Unit = {
+ try {
+ run(args)
+ } catch {
+ case ex: LoadingException =>
+ System.err.println(ex.getMessage)
+ case ex: io.greenbus.msg.amqp.util.LoadingException =>
+ System.err.println(ex.getMessage)
+ case ex: java.util.concurrent.TimeoutException =>
+ System.err.println("Service request timed out")
+ case ex: Throwable =>
+ logger.error("Unhandled exception: " + ex)
+ System.err.println("Error: " + ex.getMessage)
+ }
+ }
+
+ def run(args: Array[String]): Unit = {
+
+ val options = buildOptions
+ val parser = new cli.BasicParser
+ val line = parser.parse(options, args)
+
+ def printHelp() {
+ (new HelpFormatter).printHelp("loader file", options)
+ }
+
+ if (line.hasOption("h")) {
+ printHelp()
+ } else {
+
+ val dirOpt = Option(line.getOptionValue(directoryFlag))
+
+ val outOpt = Option(line.getOptionValue(outputFlag))
+
+ export(outOpt, dirOpt)
+ }
+ }
+
+ def export(outputOpt: Option[String], dirOpt: Option[String]) = {
+
+ val baseDir = Option(System.getProperty("io.greenbus.config.base")).getOrElse("")
+ val amqpConfigPath = Option(System.getProperty("io.greenbus.config.amqp")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.msg.amqp.cfg")
+ val userConfigPath = Option(System.getProperty("io.greenbus.config.user")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.user.cfg")
+
+ val config = AmqpSettings.load(amqpConfigPath)
+ val userConfig = UserSettings.load(userConfigPath)
+
+ val conn = try {
+ ServiceConnection.connect(config, QpidBroker, 10000)
+ } catch {
+ case ex: java.io.IOException => throw new LoadingException(ex.getMessage)
+ }
+
+ try {
+ val session = conn.login(userConfig.user, userConfig.password, Duration(5000, MILLISECONDS))
+
+ val downloadSet = Download.download(session)
+
+ val model = DownloadConversion.toIntermediate(downloadSet)
+
+ val xml = AuthConversion.toXml(model)
+
+ val text = XmlHelper.writeToString(xml, classOf[Authorization], formatted = true)
+
+ dirOpt match {
+ case Some(dirname) =>
+ writeInDir(text, outputOpt, dirname)
+ case None =>
+ outputOpt match {
+ case Some(filename) =>
+ FileUtils.write(new File(filename), text)
+ case None =>
+ println(text)
+ }
+ }
+
+ } finally {
+ conn.disconnect()
+ }
+ }
+
+ def writeInDir(main: String, outNameOpt: Option[String], dirname: String): Unit = {
+ val dirFile = new File(dirname)
+ if (!dirFile.exists()) {
+ dirFile.mkdir()
+ }
+
+ val mainFilename = outNameOpt.getOrElse("authorization.xml")
+
+ val mainFile = new File(dirFile, mainFilename)
+ FileUtils.write(mainFile, main)
+ }
+}
diff --git a/loader-xml/src/main/scala/io/greenbus/ldr/auth/Importer.scala b/loader-xml/src/main/scala/io/greenbus/ldr/auth/Importer.scala
new file mode 100644
index 0000000..6f5a2dc
--- /dev/null
+++ b/loader-xml/src/main/scala/io/greenbus/ldr/auth/Importer.scala
@@ -0,0 +1,154 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.ldr.auth
+
+import java.io.File
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.ldr.xml.auth.Authorization
+import org.apache.commons.cli
+import org.apache.commons.cli.{ HelpFormatter, Options }
+import io.greenbus.msg.amqp.AmqpSettings
+import io.greenbus.msg.qpid.QpidBroker
+import io.greenbus.client.ServiceConnection
+import io.greenbus.loader.set.{ UUIDHelpers, LoadingException }
+import io.greenbus.util.{ UserSettings, XmlHelper }
+
+import scala.concurrent.duration._
+
+object Importer extends Logging {
+
+ def buildOptions: Options = {
+ val opts = new Options
+ opts.addOption("h", "help", false, "Display this help text")
+ opts
+ }
+
+ def main(args: Array[String]): Unit = {
+ try {
+ run(args)
+ } catch {
+ case ex: LoadingException =>
+ System.err.println(ex.getMessage)
+ case ex: io.greenbus.msg.amqp.util.LoadingException =>
+ System.err.println(ex.getMessage)
+ case ex: java.util.concurrent.TimeoutException =>
+ System.err.println("Service request timed out")
+ case ex: Throwable =>
+ logger.error("Unhandled exception: " + ex)
+ System.err.println("Error: " + ex.getMessage)
+ }
+ }
+
+ def run(args: Array[String]): Unit = {
+
+ val options = buildOptions
+ val parser = new cli.BasicParser
+ val line = parser.parse(options, args)
+
+ def printHelp() {
+ (new HelpFormatter).printHelp("loader file", options)
+ }
+
+ if (line.hasOption("h")) {
+ printHelp()
+ } else {
+
+ if (line.getArgs.length < 1) {
+ System.err.println("Must include file to import.")
+ System.exit(1)
+ }
+ val filename = line.getArgs.head
+
+ val file = new File(filename)
+ val parentDir = file.getParentFile
+
+ val back = XmlHelper.read(file, classOf[Authorization])
+
+ val model = AuthConversion.toModel(back)
+
+ importAuth(model)
+ }
+ }
+
+ def importAuth(mdl: AuthMdl.AuthSet) = {
+
+ val baseDir = Option(System.getProperty("io.greenbus.config.base")).getOrElse("")
+ val amqpConfigPath = Option(System.getProperty("io.greenbus.config.amqp")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.msg.amqp.cfg")
+ val userConfigPath = Option(System.getProperty("io.greenbus.config.user")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.user.cfg")
+
+ val config = AmqpSettings.load(amqpConfigPath)
+ val userConfig = UserSettings.load(userConfigPath)
+
+ val conn = try {
+ ServiceConnection.connect(config, QpidBroker, 10000)
+ } catch {
+ case ex: java.io.IOException => throw new LoadingException(ex.getMessage)
+ }
+
+ try {
+
+ val session = conn.login(userConfig.user, userConfig.password, Duration(5000, MILLISECONDS))
+
+ val (uuids, names) = AuthMdl.getIdSetForAgents(mdl.agents)
+
+ val currentAgents = Download.getAgents(session, uuids, names)
+
+ val uuidsSet = currentAgents.map(_.getUuid).map(UUIDHelpers.protoUUIDToUuid).toSet
+ val namesSet = currentAgents.map(_.getName).toSet
+
+ val agents = mdl.agents.map { agent =>
+ if (agent.uuidOpt.exists(uuidsSet.contains) || namesSet.contains(agent.name)) {
+ agent
+ } else {
+
+ def prompt(retries: Int): String = {
+ if (retries == 0) {
+ throw new IllegalArgumentException("Failed to provide a password")
+ } else {
+
+ println(s"Enter new password for agent ${agent.name}: ")
+ val first = readLine()
+ println("Re-enter password: ")
+ val second = readLine()
+ if (first == second) {
+ first
+ } else {
+ println("Passwords did not match.\n")
+ prompt(retries - 1)
+ }
+ }
+ }
+
+ val password = prompt(3)
+
+ agent.copy(passwordOpt = Some(password))
+ }
+ }
+
+ val withPasswords = mdl.copy(agents = agents)
+
+ Upload.upload(session, withPasswords)
+
+ } finally {
+ conn.disconnect()
+ }
+ }
+
+}
diff --git a/loader-xml/src/main/scala/io/greenbus/ldr/auth/Upload.scala b/loader-xml/src/main/scala/io/greenbus/ldr/auth/Upload.scala
new file mode 100644
index 0000000..88ffbae
--- /dev/null
+++ b/loader-xml/src/main/scala/io/greenbus/ldr/auth/Upload.scala
@@ -0,0 +1,88 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.ldr.auth
+
+import io.greenbus.msg.Session
+import io.greenbus.client.service.AuthService
+import io.greenbus.client.service.proto.Auth.{ EntitySelector, Permission }
+import io.greenbus.client.service.proto.AuthRequests.{ PermissionSetTemplate, AgentTemplate }
+import io.greenbus.ldr.auth.AuthMdl.{ PermissionSet, Agent, AuthSet }
+import io.greenbus.loader.set.{ PrintTracker, UUIDHelpers }
+import io.greenbus.util.Timing
+
+import scala.collection.JavaConversions._
+
+object Upload {
+
+ import io.greenbus.loader.set.Upload._
+
+ def upload(session: Session, mdl: AuthSet) = {
+ val timer = Timing.Stopwatch.start
+ val tracker = new PrintTracker(60, System.out)
+
+ val authClient = AuthService.client(session)
+
+ println("\n== Progress:")
+
+ val permSets = chunkedCalls(allChunkSize, mdl.permSets.map(toTemplate), authClient.putPermissionSets, tracker)
+
+ val agents = chunkedCalls(allChunkSize, mdl.agents.map(toTemplate), authClient.putAgents, tracker)
+
+ println("\n")
+ println(s"== Finished in ${timer.elapsed} ms")
+ }
+
+ def toTemplate(agent: Agent): AgentTemplate = {
+ val b = AgentTemplate.newBuilder()
+ .setName(agent.name)
+ .addAllPermissionSets(agent.permissions)
+
+ agent.uuidOpt.map(UUIDHelpers.uuidToProtoUUID).foreach(b.setUuid)
+ agent.passwordOpt.foreach(b.setPassword)
+
+ b.build()
+ }
+
+ def toTemplate(permSet: PermissionSet): PermissionSetTemplate = {
+
+ val b = PermissionSetTemplate.newBuilder()
+ .setName(permSet.name)
+ .addAllPermissions(permSet.permissions.map(toProto))
+
+ permSet.idOpt.map(UUIDHelpers.longToProtoId).foreach(b.setId)
+
+ b.build()
+ }
+
+ def toProto(mdl: AuthMdl.Permission): Permission = {
+ Permission.newBuilder()
+ .setAllow(mdl.allow)
+ .addAllActions(mdl.actions)
+ .addAllResources(mdl.resources)
+ .addAllSelectors(mdl.selectors.map(toProto))
+ .build()
+ }
+
+ def toProto(mdl: AuthMdl.EntitySelector): EntitySelector = {
+ EntitySelector.newBuilder()
+ .setStyle(mdl.style)
+ .addAllArguments(mdl.args)
+ .build()
+ }
+}
diff --git a/loader-xml/src/main/scala/io/greenbus/ldr/event/Download.scala b/loader-xml/src/main/scala/io/greenbus/ldr/event/Download.scala
new file mode 100644
index 0000000..d8dabdd
--- /dev/null
+++ b/loader-xml/src/main/scala/io/greenbus/ldr/event/Download.scala
@@ -0,0 +1,57 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.ldr.event
+
+import io.greenbus.msg.Session
+import io.greenbus.client.service.EventService
+import io.greenbus.client.service.proto.EventRequests.EventConfigQuery
+import io.greenbus.client.service.proto.Events.EventConfig
+import io.greenbus.loader.set.Downloader
+
+import scala.concurrent.Future
+
+object Download {
+
+ def download(session: Session): Seq[EventConfig] = {
+ val client = EventService.client(session)
+
+ def configQuery(last: Option[String], size: Int): Future[Seq[EventConfig]] = {
+ val b = EventConfigQuery.newBuilder().setPageSize(size)
+ last.foreach(b.setLastEventType)
+ client.eventConfigQuery(b.build())
+ }
+
+ def configPageBoundary(results: Seq[EventConfig]): String = {
+ results.last.getEventType
+ }
+
+ Downloader.allPages(200, configQuery, configPageBoundary)
+ }
+
+ def toMdl(cfg: EventConfig): EventMdl.EventConfig = {
+
+ val eventType = cfg.getEventType
+ val designation = cfg.getDesignation
+ val alarmStateOpt = if (cfg.hasAlarmState) Some(cfg.getAlarmState) else None
+ val severity = cfg.getSeverity
+ val resource = cfg.getResource
+
+ EventMdl.EventConfig(eventType, severity, designation, alarmStateOpt, resource)
+ }
+}
diff --git a/loader-xml/src/main/scala/io/greenbus/ldr/event/EventMdl.scala b/loader-xml/src/main/scala/io/greenbus/ldr/event/EventMdl.scala
new file mode 100644
index 0000000..a5fb3d5
--- /dev/null
+++ b/loader-xml/src/main/scala/io/greenbus/ldr/event/EventMdl.scala
@@ -0,0 +1,111 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.ldr.event
+
+import io.greenbus.ldr.xml.events.{ EventConfigs, Events, EventConfig }
+import io.greenbus.client.service.proto.Events.Alarm
+import io.greenbus.client.service.proto.Events.EventConfig.Designation
+import io.greenbus.loader.set.LoadingException
+
+import scala.collection.JavaConversions._
+
+object EventMdl {
+
+ case class EventConfig(typ: String, severity: Int, designation: Designation, initialAlarmState: Option[Alarm.State], resource: String)
+
+}
+
+object EventConversion {
+
+ def toXml(all: Seq[EventMdl.EventConfig]): Events = {
+ val xml = new Events
+
+ val coll = new EventConfigs
+ all.map(toXml).foreach(coll.getEventConfig.add)
+
+ xml.setEventConfigs(coll)
+
+ xml
+ }
+
+ def toXml(mdl: EventMdl.EventConfig): EventConfig = {
+ val xml = new EventConfig
+ xml.setEventType(mdl.typ)
+ xml.setSeverity(mdl.severity)
+ xml.setDesignation(toXml(mdl.designation))
+ xml.setValue(mdl.resource)
+ mdl.initialAlarmState.map(toXml).foreach(xml.setInitialAlarmState)
+ xml
+ }
+
+ def toXml(des: Designation): String = {
+ des match {
+ case Designation.EVENT => "EVENT"
+ case Designation.ALARM => "ALARM"
+ case Designation.LOG => "LOG"
+ }
+ }
+
+ def toXml(alarmState: Alarm.State): String = {
+ alarmState match {
+ case Alarm.State.UNACK_AUDIBLE => "UNACK_AUDIBLE"
+ case Alarm.State.UNACK_SILENT => "UNACK_SILENT"
+ case Alarm.State.ACKNOWLEDGED => "ACKNOWLEDGED"
+ case Alarm.State.REMOVED => "REMOVED"
+ }
+ }
+
+ def toModel(xml: Events): Seq[EventMdl.EventConfig] = {
+ if (xml.isSetEventConfigs) {
+ xml.getEventConfigs.getEventConfig.map(toModel)
+ } else {
+ Seq()
+ }
+ }
+
+ def toModel(eventConfig: EventConfig): EventMdl.EventConfig = {
+
+ val eventType = if (eventConfig.isSetEventType) eventConfig.getEventType else throw new LoadingException("EventConfig must include event type")
+ val severity = if (eventConfig.isSetSeverity) eventConfig.getSeverity else throw new LoadingException(s"EventConfig $eventType must include severity")
+ val designationStr = if (eventConfig.isSetDesignation) eventConfig.getDesignation else throw new LoadingException(s"EventConfig $eventType must include designation")
+ val initialAlarmStrOpt = if (eventConfig.isSetInitialAlarmState) Some(eventConfig.getInitialAlarmState) else None
+ val resource = if (eventConfig.isSetValue) eventConfig.getValue else throw new LoadingException(s"EventConfig $eventType must include resource string")
+
+ EventMdl.EventConfig(eventType, severity, desigToModel(designationStr), initialAlarmStrOpt.map(stateToModel), resource)
+ }
+
+ def desigToModel(des: String): Designation = {
+ des match {
+ case "EVENT" => Designation.EVENT
+ case "ALARM" => Designation.ALARM
+ case "LOG" => Designation.LOG
+ }
+ }
+
+ def stateToModel(alarmState: String): Alarm.State = {
+ alarmState match {
+ case "UNACK_AUDIBLE" => Alarm.State.UNACK_AUDIBLE
+ case "UNACK_SILENT" => Alarm.State.UNACK_SILENT
+ case "ACKNOWLEDGED" => Alarm.State.ACKNOWLEDGED
+ case "REMOVED" => Alarm.State.REMOVED
+ }
+ }
+
+}
+
diff --git a/loader-xml/src/main/scala/io/greenbus/ldr/event/Exporter.scala b/loader-xml/src/main/scala/io/greenbus/ldr/event/Exporter.scala
new file mode 100644
index 0000000..9cf6a49
--- /dev/null
+++ b/loader-xml/src/main/scala/io/greenbus/ldr/event/Exporter.scala
@@ -0,0 +1,141 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.ldr.event
+
+import java.io.File
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.client.ServiceConnection
+import io.greenbus.ldr.xml.events.Events
+import io.greenbus.loader.set.LoadingException
+import io.greenbus.msg.amqp.AmqpSettings
+import io.greenbus.msg.qpid.QpidBroker
+import io.greenbus.util.{ UserSettings, XmlHelper }
+import org.apache.commons.cli
+import org.apache.commons.cli.{ HelpFormatter, Options }
+import org.apache.commons.io.FileUtils
+
+import scala.concurrent.duration._
+
+object Exporter extends Logging {
+
+ val outputFlag = "output"
+ val directoryFlag = "directory"
+
+ def buildOptions: Options = {
+ val opts = new Options
+ opts.addOption("h", "help", false, "Display this help text")
+ opts.addOption(new cli.Option("d", directoryFlag, true, "Directory to output files to"))
+ opts.addOption(new cli.Option("o", outputFlag, true, "Output filename"))
+ opts
+ }
+
+ def main(args: Array[String]): Unit = {
+ try {
+ run(args)
+ } catch {
+ case ex: LoadingException =>
+ System.err.println(ex.getMessage)
+ case ex: io.greenbus.msg.amqp.util.LoadingException =>
+ System.err.println(ex.getMessage)
+ case ex: java.util.concurrent.TimeoutException =>
+ System.err.println("Service request timed out")
+ case ex: Throwable =>
+ logger.error("Unhandled exception: " + ex)
+ System.err.println("Error: " + ex.getMessage)
+ }
+ }
+
+ def run(args: Array[String]): Unit = {
+
+ val options = buildOptions
+ val parser = new cli.BasicParser
+ val line = parser.parse(options, args)
+
+ def printHelp() {
+ (new HelpFormatter).printHelp("loader file", options)
+ }
+
+ if (line.hasOption("h")) {
+ printHelp()
+ } else {
+
+ val dirOpt = Option(line.getOptionValue(directoryFlag))
+
+ val outOpt = Option(line.getOptionValue(outputFlag))
+
+ export(outOpt, dirOpt)
+ }
+ }
+
+ def export(outputOpt: Option[String], dirOpt: Option[String]) = {
+
+ val baseDir = Option(System.getProperty("io.greenbus.config.base")).getOrElse("")
+ val amqpConfigPath = Option(System.getProperty("io.greenbus.config.amqp")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.msg.amqp.cfg")
+ val userConfigPath = Option(System.getProperty("io.greenbus.config.user")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.user.cfg")
+
+ val config = AmqpSettings.load(amqpConfigPath)
+ val userConfig = UserSettings.load(userConfigPath)
+
+ val conn = try {
+ ServiceConnection.connect(config, QpidBroker, 10000)
+ } catch {
+ case ex: java.io.IOException => throw new LoadingException(ex.getMessage)
+ }
+
+ try {
+ val session = conn.login(userConfig.user, userConfig.password, Duration(5000, MILLISECONDS))
+
+ val downloadSet = Download.download(session)
+
+ val allConfigs = downloadSet.map(Download.toMdl)
+
+ val xml = EventConversion.toXml(allConfigs)
+
+ val text = XmlHelper.writeToString(xml, classOf[Events], formatted = true)
+
+ dirOpt match {
+ case Some(dirname) =>
+ writeInDir(text, outputOpt, dirname)
+ case None =>
+ outputOpt match {
+ case Some(filename) =>
+ FileUtils.write(new File(filename), text)
+ case None =>
+ println(text)
+ }
+ }
+
+ } finally {
+ conn.disconnect()
+ }
+ }
+
+ def writeInDir(main: String, outNameOpt: Option[String], dirname: String): Unit = {
+ val dirFile = new File(dirname)
+ if (!dirFile.exists()) {
+ dirFile.mkdir()
+ }
+
+ val mainFilename = outNameOpt.getOrElse("event-config.xml")
+
+ val mainFile = new File(dirFile, mainFilename)
+ FileUtils.write(mainFile, main)
+ }
+}
diff --git a/loader-xml/src/main/scala/io/greenbus/ldr/event/Importer.scala b/loader-xml/src/main/scala/io/greenbus/ldr/event/Importer.scala
new file mode 100644
index 0000000..a0b81d3
--- /dev/null
+++ b/loader-xml/src/main/scala/io/greenbus/ldr/event/Importer.scala
@@ -0,0 +1,114 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.ldr.event
+
+import java.io.File
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.ldr.xml.events.Events
+import org.apache.commons.cli
+import org.apache.commons.cli.{ HelpFormatter, Options }
+import io.greenbus.msg.amqp.AmqpSettings
+import io.greenbus.msg.qpid.QpidBroker
+import io.greenbus.client.ServiceConnection
+import io.greenbus.loader.set.LoadingException
+import io.greenbus.util.{ UserSettings, XmlHelper }
+
+import scala.concurrent.duration._
+
+object Importer extends Logging {
+
+ def buildOptions: Options = {
+ val opts = new Options
+ opts.addOption("h", "help", false, "Display this help text")
+ opts
+ }
+
+ def main(args: Array[String]): Unit = {
+ try {
+ run(args)
+ } catch {
+ case ex: LoadingException =>
+ System.err.println(ex.getMessage)
+ case ex: io.greenbus.msg.amqp.util.LoadingException =>
+ System.err.println(ex.getMessage)
+ case ex: java.util.concurrent.TimeoutException =>
+ System.err.println("Service request timed out")
+ case ex: Throwable =>
+ logger.error("Unhandled exception: " + ex)
+ System.err.println("Error: " + ex.getMessage)
+ }
+ }
+
+ def run(args: Array[String]): Unit = {
+
+ val options = buildOptions
+ val parser = new cli.BasicParser
+ val line = parser.parse(options, args)
+
+ def printHelp() {
+ (new HelpFormatter).printHelp("loader file", options)
+ }
+
+ if (line.hasOption("h")) {
+ printHelp()
+ } else {
+
+ if (line.getArgs.length < 1) {
+ System.err.println("Must include file to import.")
+ System.exit(1)
+ }
+ val filename = line.getArgs.head
+
+ val file = new File(filename)
+ val parentDir = file.getParentFile
+
+ val back = XmlHelper.read(file, classOf[Events])
+
+ val model = EventConversion.toModel(back)
+
+ importEventConfig(model)
+ }
+ }
+
+ def importEventConfig(all: Seq[EventMdl.EventConfig]) = {
+
+ val baseDir = Option(System.getProperty("io.greenbus.config.base")).getOrElse("")
+ val amqpConfigPath = Option(System.getProperty("io.greenbus.config.amqp")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.msg.amqp.cfg")
+ val userConfigPath = Option(System.getProperty("io.greenbus.config.user")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.user.cfg")
+
+ val config = AmqpSettings.load(amqpConfigPath)
+ val userConfig = UserSettings.load(userConfigPath)
+
+ val conn = try {
+ ServiceConnection.connect(config, QpidBroker, 10000)
+ } catch {
+ case ex: java.io.IOException => throw new LoadingException(ex.getMessage)
+ }
+
+ try {
+ val session = conn.login(userConfig.user, userConfig.password, Duration(5000, MILLISECONDS))
+
+ Upload.upload(session, all)
+
+ } finally {
+ conn.disconnect()
+ }
+ }
+}
diff --git a/loader-xml/src/main/scala/io/greenbus/ldr/event/Upload.scala b/loader-xml/src/main/scala/io/greenbus/ldr/event/Upload.scala
new file mode 100644
index 0000000..49a1af0
--- /dev/null
+++ b/loader-xml/src/main/scala/io/greenbus/ldr/event/Upload.scala
@@ -0,0 +1,57 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.ldr.event
+
+import io.greenbus.msg.Session
+import io.greenbus.client.service.EventService
+import io.greenbus.client.service.proto.EventRequests.EventConfigTemplate
+import io.greenbus.loader.set.PrintTracker
+import io.greenbus.util.Timing
+
+object Upload {
+
+ import io.greenbus.loader.set.Upload._
+
+ def upload(session: Session, all: Seq[EventMdl.EventConfig]) = {
+ val timer = Timing.Stopwatch.start
+ val tracker = new PrintTracker(60, System.out)
+
+ val client = EventService.client(session)
+
+ println("\n== Progress:")
+
+ val eventConfigs = chunkedCalls(allChunkSize, all.map(toTemplate), client.putEventConfigs, tracker)
+
+ println("\n")
+ println(s"== Finished in ${timer.elapsed} ms")
+ }
+
+ def toTemplate(mdl: EventMdl.EventConfig): EventConfigTemplate = {
+
+ val b = EventConfigTemplate.newBuilder()
+ .setEventType(mdl.typ)
+ .setSeverity(mdl.severity)
+ .setDesignation(mdl.designation)
+ .setResource(mdl.resource)
+
+ mdl.initialAlarmState.foreach(b.setAlarmState)
+
+ b.build()
+ }
+}
diff --git a/loading/pom.xml b/loading/pom.xml
new file mode 100755
index 0000000..37b9012
--- /dev/null
+++ b/loading/pom.xml
@@ -0,0 +1,59 @@
+
+ 4.0.0
+
+
+ io.greenbus
+ greenbus-scala-base
+ 3.0.0
+ ../scala-base
+
+
+ greenbus-loading
+ jar
+
+
+
+ AGPLv3
+ http://www.gnu.org/licenses/agpl-3.0.txt
+
+
+
+
+
+
+
+ com.mycila.maven-license-plugin
+ maven-license-plugin
+
+
+
+
+
+
+
+
+
+ io.greenbus
+ greenbus-client
+ 3.0.0
+
+
+ io.greenbus
+ greenbus-util
+ 3.0.0
+
+
+ io.greenbus.msg
+ greenbus-msg-qpid
+ 1.0.0
+
+
+ commons-io
+ commons-io
+ 2.4
+
+
+
+
+
diff --git a/loading/src/main/scala/io/greenbus/loader/set/Actions.scala b/loading/src/main/scala/io/greenbus/loader/set/Actions.scala
new file mode 100644
index 0000000..1284aa3
--- /dev/null
+++ b/loading/src/main/scala/io/greenbus/loader/set/Actions.scala
@@ -0,0 +1,271 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.loader.set
+
+import java.util.UUID
+
+import io.greenbus.client.service.proto.Model.{ PointCategory, CommandCategory }
+import io.greenbus.loader.set.Mdl.EdgeDesc
+
+object Actions {
+
+ case class PutEntity(uuidOpt: Option[UUID], name: String, types: Set[String])
+ case class DeleteEntity(uuid: UUID)
+
+ case class PutPoint(uuidOpt: Option[UUID], name: String, types: Set[String], category: PointCategory, unit: String)
+ case class DeletePoint(uuid: UUID)
+
+ case class PutCommand(uuidOpt: Option[UUID], name: String, types: Set[String], category: CommandCategory, displayName: String)
+ case class DeleteCommand(uuid: UUID)
+
+ case class PutEndpoint(uuidOpt: Option[UUID], name: String, types: Set[String], protocol: String)
+ case class DeleteEndpoint(uuid: UUID)
+
+ case class PutEdge(desc: EdgeDesc)
+ case class DeleteEdge(parent: UUID, relationship: String, child: UUID)
+
+ case class PutKeyValueByUuid(uuid: UUID, key: String, vh: ValueHolder)
+ case class PutKeyValueByName(name: String, key: String, vh: ValueHolder)
+ case class DeleteKeyValue(uuid: UUID, key: String)
+
+ case class PutCalcByUuid(uuid: UUID, calc: CalculationHolder)
+ case class PutCalcByName(name: String, calc: CalculationHolder)
+
+ class ActionCache {
+
+ val entityPuts = Vector.newBuilder[PutEntity]
+ val entityDeletes = Vector.newBuilder[DeleteEntity]
+
+ val pointPuts = Vector.newBuilder[PutPoint]
+ val pointDeletes = Vector.newBuilder[DeletePoint]
+
+ val commandPuts = Vector.newBuilder[PutCommand]
+ val commandDeletes = Vector.newBuilder[DeleteCommand]
+
+ val endpointPuts = Vector.newBuilder[PutEndpoint]
+ val endpointDeletes = Vector.newBuilder[DeleteEndpoint]
+
+ val edgePuts = Vector.newBuilder[PutEdge]
+ val edgeDeletes = Vector.newBuilder[DeleteEdge]
+
+ val calcPutsByUuid = Vector.newBuilder[PutCalcByUuid]
+ val calcPutsByName = Vector.newBuilder[PutCalcByName]
+
+ val keyValuePutByUuids = Vector.newBuilder[PutKeyValueByUuid]
+ val keyValuePutByNames = Vector.newBuilder[PutKeyValueByName]
+ val keyValueDeletes = Vector.newBuilder[DeleteKeyValue]
+
+ def result(): ActionsList = {
+ ActionsList(
+ entityPuts.result(),
+ entityDeletes.result(),
+ pointPuts.result(),
+ pointDeletes.result(),
+ commandPuts.result(),
+ commandDeletes.result(),
+ endpointPuts.result(),
+ endpointDeletes.result(),
+ edgePuts.result(),
+ edgeDeletes.result(),
+ keyValuePutByUuids.result(),
+ keyValuePutByNames.result(),
+ keyValueDeletes.result(),
+ calcPutsByUuid.result(),
+ calcPutsByName.result())
+ }
+ }
+
+ case class ActionsList(
+ entityPuts: Vector[PutEntity],
+ entityDeletes: Vector[DeleteEntity],
+ pointPuts: Vector[PutPoint],
+ pointDeletes: Vector[DeletePoint],
+ commandPuts: Vector[PutCommand],
+ commandDeletes: Vector[DeleteCommand],
+ endpointPuts: Vector[PutEndpoint],
+ endpointDeletes: Vector[DeleteEndpoint],
+ edgePuts: Vector[PutEdge],
+ edgeDeletes: Vector[DeleteEdge],
+ keyValuePutByUuids: Vector[PutKeyValueByUuid],
+ keyValuePutByNames: Vector[PutKeyValueByName],
+ keyValueDeletes: Vector[DeleteKeyValue],
+ calcPutsByUuid: Vector[PutCalcByUuid],
+ calcPutsByName: Vector[PutCalcByName])
+
+}
+
+object Differences {
+
+ trait EntBasedCreate {
+ val name: String
+ val types: Set[String]
+ val keys: Set[String]
+ val withUuid: Boolean
+ }
+
+ trait EntBasedChange {
+ val currentName: String
+ val nameOpt: Option[ParamChange[String]]
+ val addedTypes: Set[String]
+ val removedTypes: Set[String]
+ val addedKeys: Set[String]
+ val modifiedKeys: Set[String]
+ val removedKeys: Set[String]
+ }
+
+ case class ParamChange[A](original: A, updated: A)
+
+ case class EntityCreated(name: String, types: Set[String], keys: Set[String], withUuid: Boolean = false) extends EntBasedCreate
+
+ case class EntityChanged(
+ currentName: String,
+ nameOpt: Option[ParamChange[String]],
+ addedTypes: Set[String],
+ removedTypes: Set[String],
+ addedKeys: Set[String],
+ modifiedKeys: Set[String],
+ removedKeys: Set[String]) extends EntBasedChange
+
+ case class EntityDeleted(name: String)
+
+ case class PointCreated(name: String, types: Set[String], keys: Set[String], category: PointCategory, unit: String, withUuid: Boolean = false) extends EntBasedCreate
+
+ case class PointChanged(
+ currentName: String,
+ nameOpt: Option[ParamChange[String]],
+ addedTypes: Set[String],
+ removedTypes: Set[String],
+ addedKeys: Set[String],
+ modifiedKeys: Set[String],
+ removedKeys: Set[String],
+ categoryOpt: Option[ParamChange[PointCategory]],
+ unitOpt: Option[ParamChange[String]]) extends EntBasedChange
+
+ case class PointDeleted(name: String)
+
+ case class CommandCreated(name: String, types: Set[String], keys: Set[String], category: CommandCategory, displayName: String, withUuid: Boolean = false) extends EntBasedCreate
+
+ case class CommandChanged(
+ currentName: String,
+ nameOpt: Option[ParamChange[String]],
+ addedTypes: Set[String],
+ removedTypes: Set[String],
+ addedKeys: Set[String],
+ modifiedKeys: Set[String],
+ removedKeys: Set[String],
+ categoryOpt: Option[ParamChange[CommandCategory]],
+ displayNameOpt: Option[ParamChange[String]]) extends EntBasedChange
+
+ case class CommandDeleted(name: String)
+
+ case class EndpointCreated(name: String, types: Set[String], keys: Set[String], protocol: String, withUuid: Boolean = false) extends EntBasedCreate
+
+ case class EndpointChanged(
+ currentName: String,
+ nameOpt: Option[ParamChange[String]],
+ addedTypes: Set[String],
+ removedTypes: Set[String],
+ addedKeys: Set[String],
+ modifiedKeys: Set[String],
+ removedKeys: Set[String],
+ protocolOpt: Option[ParamChange[String]]) extends EntBasedChange
+
+ case class EndpointDeleted(name: String)
+
+ case class KeyValueRecord(entityName: String, key: String)
+
+ case class EdgeRecord(parent: String, relation: String, child: String)
+
+ class RecordBuilder {
+ val entCreateRecords = Vector.newBuilder[EntityCreated]
+ val entChangedRecords = Vector.newBuilder[EntityChanged]
+ val entDeleteRecords = Vector.newBuilder[EntityDeleted]
+
+ val pointCreateRecords = Vector.newBuilder[PointCreated]
+ val pointChangedRecords = Vector.newBuilder[PointChanged]
+ val pointDeleteRecords = Vector.newBuilder[PointDeleted]
+
+ val commandCreateRecords = Vector.newBuilder[CommandCreated]
+ val commandChangedRecords = Vector.newBuilder[CommandChanged]
+ val commandDeleteRecords = Vector.newBuilder[CommandDeleted]
+
+ val endpointCreateRecords = Vector.newBuilder[EndpointCreated]
+ val endpointChangedRecords = Vector.newBuilder[EndpointChanged]
+ val endpointDeleteRecords = Vector.newBuilder[EndpointDeleted]
+
+ /*val keyValueCreateRecords = Vector.newBuilder[KeyValueRecord]
+ val keyValueChangedRecords = Vector.newBuilder[KeyValueRecord]
+ val keyValueDeleteRecords = Vector.newBuilder[KeyValueRecord]*/
+
+ val edgeCreateRecords = Vector.newBuilder[EdgeRecord]
+ val edgeDeleteRecords = Vector.newBuilder[EdgeRecord]
+
+ def result(): DiffRecord = {
+ DiffRecord(
+ entCreateRecords.result(),
+ entChangedRecords.result(),
+ entDeleteRecords.result(),
+ pointCreateRecords.result(),
+ pointChangedRecords.result(),
+ pointDeleteRecords.result(),
+ commandCreateRecords.result(),
+ commandChangedRecords.result(),
+ commandDeleteRecords.result(),
+ endpointCreateRecords.result(),
+ endpointChangedRecords.result(),
+ endpointDeleteRecords.result(),
+ edgeCreateRecords.result(),
+ edgeDeleteRecords.result())
+ }
+ }
+
+ case class DiffRecord(
+ entCreateRecords: Vector[EntityCreated],
+ entChangedRecords: Vector[EntityChanged],
+ entDeleteRecords: Vector[EntityDeleted],
+ pointCreateRecords: Vector[PointCreated],
+ pointChangedRecords: Vector[PointChanged],
+ pointDeleteRecords: Vector[PointDeleted],
+ commandCreateRecords: Vector[CommandCreated],
+ commandChangedRecords: Vector[CommandChanged],
+ commandDeleteRecords: Vector[CommandDeleted],
+ endpointCreateRecords: Vector[EndpointCreated],
+ endpointChangedRecords: Vector[EndpointChanged],
+ endpointDeleteRecords: Vector[EndpointDeleted],
+ edgeCreateRecords: Vector[EdgeRecord],
+ edgeDeleteRecords: Vector[EdgeRecord]) {
+
+ def isEmpty: Boolean = {
+ entCreateRecords.isEmpty &&
+ entChangedRecords.isEmpty &&
+ entDeleteRecords.isEmpty &&
+ pointCreateRecords.isEmpty &&
+ pointChangedRecords.isEmpty &&
+ pointDeleteRecords.isEmpty &&
+ commandCreateRecords.isEmpty &&
+ commandChangedRecords.isEmpty &&
+ commandDeleteRecords.isEmpty &&
+ endpointCreateRecords.isEmpty &&
+ endpointChangedRecords.isEmpty &&
+ endpointDeleteRecords.isEmpty &&
+ edgeCreateRecords.isEmpty &&
+ edgeDeleteRecords.isEmpty
+ }
+ }
+}
diff --git a/loading/src/main/scala/io/greenbus/loader/set/Download.scala b/loading/src/main/scala/io/greenbus/loader/set/Download.scala
new file mode 100644
index 0000000..1185cab
--- /dev/null
+++ b/loading/src/main/scala/io/greenbus/loader/set/Download.scala
@@ -0,0 +1,521 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.loader.set
+
+import java.util.UUID
+
+import io.greenbus.msg.Session
+import io.greenbus.client.service.ModelService
+import io.greenbus.client.service.proto.Calculations.CalculationDescriptor
+import io.greenbus.client.service.proto.Model._
+import io.greenbus.client.service.proto.ModelRequests.{ EntityEdgeQuery, EntityKeySet }
+import io.greenbus.loader.set.Downloader.DownloadSet
+import io.greenbus.loader.set.Mdl._
+import UUIDHelpers._
+
+import scala.collection.JavaConversions._
+import scala.concurrent.duration._
+import scala.concurrent.{ Await, Future }
+
+object Downloader {
+
+ case class DownloadSet(
+ modelEntities: Seq[Entity],
+ points: Seq[Point],
+ commands: Seq[Command],
+ endpoints: Seq[Endpoint],
+ edges: Seq[EntityEdge],
+ keyValues: Seq[EntityKeyValue])
+
+ val pageSizeForGet = 500
+ val timeoutMs = 10000
+
+ private def entityIdsToKeySet(ids: Seq[EntityId]): Option[EntityKeySet] = {
+
+ val (uuids, names) = NameResolver.mostSpecificIdElements(ids)
+
+ if (uuids.nonEmpty || names.nonEmpty) {
+ Some(EntityKeySet.newBuilder()
+ .addAllUuids(uuids.map(UUIDHelpers.uuidToProtoUUID))
+ .addAllNames(names)
+ .build())
+ } else {
+ None
+ }
+ }
+
+ def downloadByIdsAndNames(session: Session, rootIds: Seq[EntityId], rootNames: Seq[String], endpointIds: Seq[EntityId]) = {
+
+ val (uuids, names) = NameResolver.mostSpecificIdElements(rootIds)
+
+ val rootKeySet = EntityKeySet.newBuilder()
+ .addAllUuids(uuids.map(UUIDHelpers.uuidToProtoUUID))
+ .addAllNames(names ++ rootNames)
+ .build()
+
+ download(session, rootKeySet, entityIdsToKeySet(endpointIds))
+ }
+
+ def downloadByIds(session: Session, rootIds: Seq[EntityId], endpointIds: Seq[EntityId]): DownloadSet = {
+
+ val rootKeySet = entityIdsToKeySet(rootIds).getOrElse {
+ throw new IllegalArgumentException("Must include at least one root id")
+ }
+
+ download(session, rootKeySet, entityIdsToKeySet(endpointIds))
+ }
+
+ def download(session: Session, rootNames: Seq[String]): DownloadSet = {
+
+ val rootKeySet = EntityKeySet.newBuilder().addAllNames(rootNames).build()
+
+ download(session, rootKeySet, None)
+ }
+
+ def download(session: Session, rootKeySet: EntityKeySet, endpointKeySetOpt: Option[EntityKeySet]): DownloadSet = {
+
+ val modelClient = ModelService.client(session)
+
+ val rootResults = Await.result(modelClient.get(rootKeySet), timeoutMs.milliseconds)
+
+ if (rootResults.nonEmpty) {
+ downloadSet(session, rootResults, endpointKeySetOpt)
+ } else {
+ DownloadSet(Seq(), Seq(), Seq(), Seq(), Seq(), Seq())
+ }
+ }
+
+ private def downloadSet(session: Session, roots: Seq[Entity], endpointKeySetOpt: Option[EntityKeySet]): DownloadSet = {
+
+ val rootUuids = roots.map(_.getUuid)
+
+ val ownsSet = getOwnsSet(session, rootUuids)
+
+ val ownsEntities = getEntities(session, ownsSet)
+
+ val pointEntities = ownsEntities.filter(_.getTypesList.contains("Point"))
+ val pointUuids = pointEntities.map(_.getUuid)
+ val pointUuidSet = pointUuids.toSet
+ val points = getPoints(session, pointUuids)
+
+ val commandEntities = ownsEntities.filter(_.getTypesList.contains("Command"))
+ val commandUuids = commandEntities.map(_.getUuid)
+ val commandUuidSet = commandUuids.toSet
+ val commands = getCommands(session, commandUuids)
+
+ val allEdges = getAllEdges(session, ownsSet)
+
+ val sourceEndpointUuids = endpointUuidsFromEdges(allEdges).distinct
+
+ val endpointsBySource = getEndpoints(session, sourceEndpointUuids)
+
+ val endpointsFromFragment = endpointKeySetOpt.map { keySet =>
+ getEndpoints(session, keySet)
+ }.getOrElse(Seq())
+
+ val endpoints = distinctBy(endpointsBySource ++ endpointsFromFragment, { e: Endpoint => e.getUuid })
+ val endpointUuids = endpoints.map(_.getUuid)
+
+ val keyValues = getKeyValues(session, (ownsSet ++ endpointUuids).distinct)
+
+ val modelEntities = ownsEntities.filterNot(ent => (pointUuidSet union commandUuidSet).contains(ent.getUuid))
+
+ DownloadSet(modelEntities, points, commands, endpoints, allEdges, keyValues)
+ }
+
+ private def distinctBy[A, B](seq: Seq[A], id: A => B): Seq[A] = {
+ val b = Seq.newBuilder[A]
+ val seen = scala.collection.mutable.HashSet[B]()
+ for (x <- seq) {
+ val elemId = id(x)
+ if (!seen(elemId)) {
+ b += x
+ seen += elemId
+ }
+ }
+ b.result()
+ }
+
+ private def getOwnsSet(session: Session, roots: Seq[ModelUUID]): Seq[ModelUUID] = {
+ val results = getOwnsSetEdges(session, roots)
+ roots ++ results.map(_.getChild)
+ }
+
+ private def getOwnsSetEdges(session: Session, roots: Seq[ModelUUID]): Seq[EntityEdge] = {
+
+ val modelClient = ModelService.client(session)
+
+ def getEdgeQuery(last: Option[ModelID], size: Int): Future[Seq[EntityEdge]] = {
+ val b = EntityEdgeQuery.newBuilder()
+ .addAllParentUuids(roots)
+ .addRelationships("owns")
+ .setPageSize(size)
+
+ last.foreach(b.setLastId)
+
+ val query = b.build()
+
+ modelClient.edgeQuery(query)
+ }
+
+ def pageBoundary(results: Seq[EntityEdge]): ModelID = {
+ results.last.getId
+ }
+
+ allPages(pageSizeForGet, getEdgeQuery, pageBoundary)
+ }
+
+ private def endpointUuidsFromEdges(edges: Seq[EntityEdge]): Seq[ModelUUID] = {
+ edges.filter(_.getRelationship == "source").map(_.getParent)
+ }
+
+ private def getAllEdges(session: Session, set: Seq[ModelUUID]): Seq[EntityEdge] = {
+
+ val modelClient = ModelService.client(session)
+
+ def parentQuery = {
+ EntityEdgeQuery.newBuilder()
+ .addAllParentUuids(set)
+ }
+ def childQuery = {
+ EntityEdgeQuery.newBuilder()
+ .addAllChildUuids(set)
+ }
+
+ def edgeQuery(build: => EntityEdgeQuery.Builder)(last: Option[ModelID], size: Int): Future[Seq[EntityEdge]] = {
+ val b = build
+ .setDepthLimit(1)
+ .setPageSize(size)
+
+ last.foreach(b.setLastId)
+
+ val query = b.build()
+
+ modelClient.edgeQuery(query)
+ }
+
+ def pageBoundary(results: Seq[EntityEdge]): ModelID = {
+ results.last.getId
+ }
+
+ (allPages(pageSizeForGet, edgeQuery(parentQuery), pageBoundary) ++
+ allPages(pageSizeForGet, edgeQuery(childQuery), pageBoundary)).distinct
+ }
+
+ def getObs[A](uuids: Seq[ModelUUID], get: EntityKeySet => Future[Seq[A]]): Seq[A] = {
+ uuids.grouped(pageSizeForGet).flatMap { set =>
+ if (set.nonEmpty) {
+ val entKeySet = EntityKeySet.newBuilder().addAllUuids(set).build()
+ Await.result(get(entKeySet), timeoutMs.milliseconds)
+ } else {
+ Seq()
+ }
+ }.toVector
+ }
+
+ private def getEntities(session: Session, uuids: Seq[ModelUUID]): Seq[Entity] = {
+ val modelClient = ModelService.client(session)
+ getObs(uuids, modelClient.get)
+ }
+
+ private def getPoints(session: Session, uuids: Seq[ModelUUID]): Seq[Point] = {
+ val modelClient = ModelService.client(session)
+ getObs(uuids, modelClient.getPoints)
+ }
+
+ private def getCommands(session: Session, uuids: Seq[ModelUUID]): Seq[Command] = {
+ val modelClient = ModelService.client(session)
+ getObs(uuids, modelClient.getCommands)
+ }
+
+ private def getEndpoints(session: Session, uuids: Seq[ModelUUID]): Seq[Endpoint] = {
+ val modelClient = ModelService.client(session)
+ getObs(uuids, modelClient.getEndpoints)
+ }
+
+ private def getEndpoints(session: Session, keySet: EntityKeySet): Seq[Endpoint] = {
+ val modelClient = ModelService.client(session)
+ Await.result(modelClient.getEndpoints(keySet), timeoutMs.milliseconds)
+ }
+
+ private def getKeyValues(session: Session, uuids: Seq[ModelUUID]): Seq[EntityKeyValue] = {
+ val modelClient = ModelService.client(session)
+
+ val keyPairs = uuids.grouped(pageSizeForGet).flatMap { set =>
+ if (set.nonEmpty) {
+ Await.result(modelClient.getEntityKeys(set), timeoutMs.milliseconds)
+ } else {
+ Seq()
+ }
+ }.toVector
+
+ keyPairs.grouped(pageSizeForGet).flatMap { set =>
+ if (set.nonEmpty) {
+ Await.result(modelClient.getEntityKeyValues(set), timeoutMs.milliseconds)
+ } else {
+ Seq()
+ }
+ }.toVector
+ }
+
+ def allPages[A, B](pageSize: Int, getResults: (Option[B], Int) => Future[Seq[A]], pageBoundary: Seq[A] => B): Seq[A] = {
+
+ def rest(accum: Seq[A], boundary: Option[B]): Seq[A] = {
+ val results = Await.result(getResults(boundary, pageSize), timeoutMs.milliseconds)
+ if (results.isEmpty || results.size < pageSize) {
+ accum ++ results
+ } else {
+ rest(accum ++ results, Some(pageBoundary(results)))
+ }
+ }
+
+ rest(Seq(), None)
+ }
+}
+
+object NameResolver {
+
+ val calculationKey = "calculation"
+
+ def resolveFlatModelUuids(session: Session, model: FlatModelFragment): FlatModelFragment = {
+
+ val allFields = model.modelEntities.map(_.fields) ++ model.points.map(_.fields) ++ model.commands.map(_.fields) ++ model.endpoints.map(_.fields)
+ val allEntIds = allFields.map(_.id)
+ val knownMap = entIdsToMap(allEntIds)
+
+ val fromEdges = unresolvedEdgeUuids(model.edges, knownMap.keySet)
+
+ val pointsWithCalcOpt: Seq[(PointDesc, Option[CalculationDescriptor])] = model.points.map { p =>
+ val (calcSeq, otherKvs) = p.fields.kvs.partition(_._1 == calculationKey)
+ val calcOpt = calcSeq.headOption.map(_._2).map(valueHolderToCalc)
+
+ (p.copy(fields = p.fields.copy(kvs = otherKvs)), calcOpt)
+ }
+
+ val allCalcDescs = pointsWithCalcOpt.flatMap(_._2)
+
+ val fromCalcs = allCalcDescs.flatMap(uuidsFromCalc).filterNot(knownMap.keySet.contains)
+
+ val allUnresolved = fromEdges ++ fromCalcs
+
+ val entities = lookupEntitiesByUuid(session, allUnresolved)
+
+ val resolvedUuidToNames = entities.map(e => (UUID.fromString(e.getUuid.getValue), e.getName)).toMap
+
+ val fullMap: Map[UUID, String] = knownMap ++ resolvedUuidToNames
+
+ def resolveEntId(id: EntityId): EntityId = {
+ id match {
+ case UuidEntId(uuid) =>
+ val name = fullMap.getOrElse(uuid, throw new LoadingException("UUID was not resolved: " + uuid))
+ FullEntId(uuid, name)
+ case x => x
+ }
+ }
+
+ val resolvedEdges = model.edges.map(e => e.copy(parent = resolveEntId(e.parent), child = resolveEntId(e.child)))
+
+ val resolvedPointDescs = pointsWithCalcOpt.map {
+ case (pointDesc, Some(calcDesc)) => pointDesc.copy(calcHolder = NamesResolvedCalc(calcDesc, fullMap))
+ case (pointDesc, None) => pointDesc.copy(calcHolder = NoCalculation)
+ }
+
+ model.copy(edges = resolvedEdges, points = resolvedPointDescs)
+ }
+
+ def resolveExternalReferences(session: Session, uuidRefs: Seq[UUID], nameRefs: Seq[String], downloadSet: DownloadSet): (Set[UUID], Set[String], Seq[(UUID, String)]) = {
+
+ val downloadSetTuples = downloadTuples(downloadSet)
+ val downloadUuidsSet = downloadSetTuples.map(_._1).toSet
+ val downloadNamesSet = downloadSetTuples.map(_._2).toSet
+
+ val unresUuidRefs = uuidRefs.filterNot(downloadUuidsSet.contains)
+ val unresNameRefs = nameRefs.filterNot(downloadNamesSet.contains)
+
+ val uuidEnts = lookupEntitiesByUuid(session, unresUuidRefs)
+ val nameEnts = lookupEntitiesByName(session, unresNameRefs)
+
+ val resolveTups = (uuidEnts.map(obj => (obj.getUuid, obj.getName)) ++
+ nameEnts.map(obj => (obj.getUuid, obj.getName)))
+ .map { case (uuid, name) => (protoUUIDToUuid(uuid), name) }
+
+ val resolveUuids = resolveTups.map(_._1)
+ val resolveNames = resolveTups.map(_._2)
+
+ (downloadUuidsSet ++ resolveUuids, downloadNamesSet ++ resolveNames, downloadSetTuples ++ resolveTups)
+ }
+
+ private def downloadTuples(downloadSet: DownloadSet) = {
+ (downloadSet.modelEntities.map(obj => (obj.getUuid, obj.getName)) ++
+ downloadSet.points.map(obj => (obj.getUuid, obj.getName)) ++
+ downloadSet.commands.map(obj => (obj.getUuid, obj.getName)) ++
+ downloadSet.endpoints.map(obj => (obj.getUuid, obj.getName)))
+ .map { case (uuid, name) => (protoUUIDToUuid(uuid), name) }
+ }
+
+ def lookupEntitiesByUuid(session: Session, set: Seq[UUID]): Seq[Entity] = {
+ val modelClient = ModelService.client(session)
+ set.grouped(Downloader.pageSizeForGet).flatMap { uuids =>
+ if (uuids.nonEmpty) {
+ val modelUuids = uuids.map(u => ModelUUID.newBuilder().setValue(u.toString).build())
+ val keySet = EntityKeySet.newBuilder().addAllUuids(modelUuids).build()
+ Await.result(modelClient.get(keySet), Downloader.timeoutMs.milliseconds)
+ } else {
+ Seq()
+ }
+ }.toVector
+ }
+
+ def lookupEntitiesByName(session: Session, set: Seq[String]): Seq[Entity] = {
+ val modelClient = ModelService.client(session)
+ set.grouped(Downloader.pageSizeForGet).flatMap { uuids =>
+ if (uuids.nonEmpty) {
+ val keySet = EntityKeySet.newBuilder().addAllNames(set).build()
+ Await.result(modelClient.get(keySet), Downloader.timeoutMs.milliseconds)
+ } else {
+ Seq()
+ }
+ }.toVector
+ }
+
+ private def uuidsFromCalc(calc: CalculationDescriptor): Seq[UUID] = {
+ calc.getCalcInputsList.map(_.getPointUuid).map(uuid => UUID.fromString(uuid.getValue))
+ }
+
+ def mostSpecificIdElements(ids: Seq[EntityId]): (Seq[UUID], Seq[String]) = {
+ val uuids = Vector.newBuilder[UUID]
+ val names = Vector.newBuilder[String]
+
+ ids.foreach {
+ case FullEntId(uuid, _) => uuids += uuid
+ case UuidEntId(uuid) => uuids += uuid
+ case NamedEntId(name) => names += name
+ }
+
+ (uuids.result(), names.result())
+ }
+
+ def entIdsToMap(ids: Seq[EntityId]): Map[UUID, String] = {
+ ids.flatMap {
+ case FullEntId(uuid, name) => Some((uuid, name))
+ case _ => None
+ }.toMap
+ }
+
+ def uuidsFromEntIds(ids: Seq[EntityId]): Seq[UUID] = {
+ ids.flatMap {
+ case UuidEntId(uuid) => Some(uuid)
+ case _ => None
+ }
+ }
+
+ def unresolvedEdgeUuids(descs: Seq[EdgeDesc], known: Set[UUID]): Seq[UUID] = {
+ val allEntIds = descs.flatMap(e => Seq(e.child, e.parent))
+ val uuids = uuidsFromEntIds(allEntIds)
+ uuids.filterNot(known.contains)
+ }
+
+ private def valueHolderToCalc(v: ValueHolder): CalculationDescriptor = {
+ val byteArray = v match {
+ case ByteArrayValue(bytes) => bytes
+ case SimpleValue(sv) => if (sv.hasByteArrayValue) sv.getByteArrayValue.toByteArray else throw new LoadingException("Calculation key value was not a byte array")
+ case _ => throw new LoadingException("Calculation key value was not a byte array")
+ }
+
+ try {
+ CalculationDescriptor.parseFrom(byteArray)
+ } catch {
+ case ex: Throwable =>
+ throw new LoadingException("Could not parse calculation stored value: " + ex)
+ }
+ }
+}
+
+object DownloadConversion {
+
+ implicit def protoUUIDToUuid(uuid: ModelUUID): UUID = UUID.fromString(uuid.getValue)
+
+ private def toEntityDesc(ent: Entity, kvs: Seq[(String, StoredValue)]): EntityDesc = {
+ EntityDesc(
+ EntityFields(
+ FullEntId(ent.getUuid, ent.getName),
+ ent.getTypesList.toSet,
+ kvs.map(tup => (tup._1, storedValueToRepr(tup._2)))))
+ }
+
+ private def toPointDesc(p: Point, kvs: Seq[(String, StoredValue)]): PointDesc = {
+ PointDesc(
+ EntityFields(
+ FullEntId(p.getUuid, p.getName),
+ p.getTypesList.toSet,
+ kvs.map(tup => (tup._1, storedValueToRepr(tup._2)))),
+ p.getPointCategory,
+ p.getUnit,
+ CalcNotChecked)
+ }
+
+ private def toCommandDesc(c: Command, kvs: Seq[(String, StoredValue)]): CommandDesc = {
+ CommandDesc(
+ EntityFields(
+ FullEntId(c.getUuid, c.getName),
+ c.getTypesList.toSet,
+ kvs.map(tup => (tup._1, storedValueToRepr(tup._2)))),
+ c.getDisplayName,
+ c.getCommandCategory)
+ }
+
+ private def toEndpointDesc(e: Endpoint, kvs: Seq[(String, StoredValue)]): EndpointDesc = {
+ EndpointDesc(
+ EntityFields(
+ FullEntId(e.getUuid, e.getName),
+ e.getTypesList.toSet,
+ kvs.map(tup => (tup._1, storedValueToRepr(tup._2)))),
+ e.getProtocol)
+ }
+
+ def storedValueToRepr(v: StoredValue): ValueHolder = {
+ if (v.hasByteArrayValue) {
+ ByteArrayValue(v.getByteArrayValue.toByteArray)
+ } else {
+ SimpleValue(v)
+ }
+ }
+
+ def downloadToIntermediate(set: DownloadSet): Mdl.FlatModelFragment = {
+ import Mdl._
+
+ val kvMap: Map[ModelUUID, Seq[(String, StoredValue)]] = set.keyValues.groupBy(_.getUuid).map {
+ case (uuid, ekvs) => (uuid, ekvs.map(ekv => (ekv.getKey, ekv.getValue)))
+ }
+
+ def kvsForUuid(uuid: ModelUUID) = kvMap.getOrElse(uuid, Seq())
+
+ val modelDescs = set.modelEntities.map(ent => toEntityDesc(ent, kvsForUuid(ent.getUuid)))
+
+ val pointDescs = set.points.map(pt => toPointDesc(pt, kvsForUuid(pt.getUuid)))
+
+ val commandDescs = set.commands.map(cmd => toCommandDesc(cmd, kvsForUuid(cmd.getUuid)))
+
+ val endpointDescs = set.endpoints.map(end => toEndpointDesc(end, kvsForUuid(end.getUuid)))
+
+ val edgeDescs = set.edges.map(e => EdgeDesc(UuidEntId(e.getParent), e.getRelationship, UuidEntId(e.getChild)))
+
+ FlatModelFragment(modelDescs, pointDescs, commandDescs, endpointDescs, edgeDescs)
+ }
+}
diff --git a/loading/src/main/scala/io/greenbus/loader/set/Importer.scala b/loading/src/main/scala/io/greenbus/loader/set/Importer.scala
new file mode 100644
index 0000000..805b442
--- /dev/null
+++ b/loading/src/main/scala/io/greenbus/loader/set/Importer.scala
@@ -0,0 +1,770 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.loader.set
+
+import java.util
+import java.util.UUID
+
+import io.greenbus.msg.Session
+import io.greenbus.client.service.proto.Calculations.{ CalculationInput, CalculationDescriptor }
+import io.greenbus.client.service.proto.Model._
+import io.greenbus.loader.set.Actions.ActionsList
+import io.greenbus.loader.set.Differences.{ DiffRecord, EntBasedChange, EntBasedCreate, ParamChange }
+import io.greenbus.loader.set.Downloader.DownloadSet
+import io.greenbus.loader.set.Mdl._
+
+import scala.collection.JavaConversions._
+
+object Importer {
+
+ import io.greenbus.loader.set.UUIDHelpers._
+
+ def importDiff(session: Session, update: FlatModelFragment, downloadSet: DownloadSet): ((ActionsList, DiffRecord), Seq[(UUID, String)]) = {
+
+ performDiff(update, downloadSet, NameResolver.resolveExternalReferences(session, _, _, _), NameResolver.lookupEntitiesByName(session, _))
+ }
+
+ type ResolveNames = (Seq[UUID], Seq[String], DownloadSet) => (Set[UUID], Set[String], Seq[(UUID, String)])
+
+ def performDiff(update: FlatModelFragment, downloadSet: DownloadSet, resolveFunc: ResolveNames, getEntsForNames: Seq[String] => Seq[Entity]): ((ActionsList, DiffRecord), Seq[(UUID, String)]) = {
+
+ val namesOfModelObjects = SetDiff.namesInSet(update)
+
+ SetDiff.checkForNameDuplicatesInFragment(namesOfModelObjects)
+
+ SetDiff.checkForDuplicateKeys(update)
+
+ checkForDuplicateNamesOfExternalObjects(update, downloadSet, getEntsForNames)
+
+ checkForDuplicateEdges(update)
+
+ val (extUuids, extNames, updateTuples) = SetDiff.analyzeIdentities(update)
+
+ val (foundUuids, foundNames, resolvedTuples) = resolveFunc(extUuids, extNames, downloadSet)
+
+ val resolvedFlat = SetDiff.resolveIdentities(update, foundUuids, foundNames)
+
+ val allIdTuples = resolvedTuples ++ updateTuples
+
+ (compare(resolvedFlat, allIdTuples, downloadSet), allIdTuples)
+ }
+
+ def checkForDuplicateEdges(model: FlatModelFragment): Unit = {
+
+ val namesOnly: Seq[(String, String, String)] = model.edges.flatMap { e =>
+ for {
+ parentName <- EntityId.toNameOpt(e.parent)
+ childName <- EntityId.toNameOpt(e.child)
+ } yield (parentName, e.relationship, childName)
+ }
+
+ val duplicates = namesOnly.groupBy(a => a).filter { case (k, v) => v.size > 1 }.values.map(_.head)
+
+ if (duplicates.nonEmpty) {
+ throw new LoadingException("The following edges were duplicated: \n\n" + duplicates.map {
+ case (parent, rel, child) =>
+ "\t" + parent + " --[" + rel + "]--> " + child
+ }.mkString("\n"))
+ }
+ }
+
+ def checkForDuplicateNamesOfExternalObjects(model: FlatModelFragment, downloadSet: DownloadSet, getEntsForNames: Seq[String] => Seq[Entity]): Unit = {
+
+ val nameToUuidInDownloadSet: Seq[(String, UUID)] =
+ (downloadSet.modelEntities.map(obj => (obj.getName, obj.getUuid)) ++
+ downloadSet.points.map(obj => (obj.getName, obj.getUuid)) ++
+ downloadSet.commands.map(obj => (obj.getName, obj.getUuid)) ++
+ downloadSet.endpoints.map(obj => (obj.getName, obj.getUuid)))
+ .map(tup => (tup._1, UUIDHelpers.protoUUIDToUuid(tup._2)))
+
+ val nameToUuidInSetMap = nameToUuidInDownloadSet.toMap
+
+ val allObjIdsInFrag = model.modelEntities.map(_.fields.id) ++
+ model.points.map(_.fields.id) ++
+ model.commands.map(_.fields.id) ++
+ model.endpoints.map(_.fields.id)
+
+ val (namedOnly, uuidNamePairs) = SetDiff.objectIdBreakdown(allObjIdsInFrag)
+
+ val (namedExisting, namedNotExistingInFrag) = namedOnly.partition(nameToUuidInSetMap.contains)
+
+ val allUuidNamePairs: Seq[(String, UUID)] = uuidNamePairs.map(t => (t._2, t._1)) ++ namedExisting.flatMap(n => nameToUuidInSetMap.get(n).map(u => (n, u)))
+
+ val allObjNames = namedNotExistingInFrag ++ allUuidNamePairs.map(_._1)
+
+ val entsForNames = getEntsForNames(allObjNames)
+
+ val nameToUuidInSystem = entsForNames.map(e => (e.getName, UUIDHelpers.protoUUIDToUuid(e.getUuid))).toMap
+
+ namedNotExistingInFrag.foreach { name =>
+ if (nameToUuidInSystem.contains(name)) {
+ throw new LoadingException(s"Attempting to create $name but it already exists in the system")
+ }
+ }
+
+ allUuidNamePairs.foreach {
+ case (name, uuid) =>
+ nameToUuidInSystem.get(name) match {
+ case None =>
+ case Some(lookupUuid) =>
+ if (uuid != lookupUuid) {
+ throw new LoadingException(s"Attempting to use name $name but it already exists in the system")
+ }
+ }
+ }
+ }
+
+ def summarize(diff: DiffRecord): Unit = {
+
+ def commonCreate(created: EntBasedCreate): Unit = {
+ println("\t" + created.name)
+ if (created.withUuid) {
+ println("\t\t(* with UUID)")
+ }
+ println("\t\tTypes: " + created.types.mkString(", "))
+ println("\t\tKey values: " + created.keys.mkString(", "))
+ }
+
+ def commonChange(changed: EntBasedChange): Unit = {
+ println("\t" + changed.currentName)
+ changed.nameOpt.foreach {
+ case ParamChange(prev, now) => println(s"\t\tName change: $prev -> $now")
+ }
+ if (changed.addedTypes.nonEmpty) {
+ println("\t\tTypes added: " + changed.addedTypes.mkString(", "))
+ }
+ if (changed.removedTypes.nonEmpty) {
+ println("\t\tTypes removed: " + changed.removedTypes.mkString(", "))
+ }
+ if (changed.addedKeys.nonEmpty) {
+ println("\t\tKey values added: " + changed.addedKeys.mkString(", "))
+ }
+ if (changed.modifiedKeys.nonEmpty) {
+ println("\t\tKey values modified: " + changed.modifiedKeys.mkString(", "))
+ }
+ if (changed.removedKeys.nonEmpty) {
+ println("\t\tKey values removed: " + changed.removedKeys.mkString(", "))
+ }
+ }
+
+ def printParam[A](name: String, opt: Option[ParamChange[A]]): Unit = {
+ opt.foreach {
+ case ParamChange(prev, now) => println(s"\t\t$name change: ${prev.toString} -> ${now.toString}")
+ }
+ }
+
+ if (diff.isEmpty) {
+ println("No changes.")
+ } else {
+
+ if (diff.entCreateRecords.nonEmpty) {
+ println("Created entities: ")
+ diff.entCreateRecords.foreach { created =>
+ commonCreate(created)
+ }
+ println()
+ }
+
+ if (diff.entChangedRecords.nonEmpty) {
+ println("Modified entities: ")
+ diff.entChangedRecords.foreach { changed =>
+ commonChange(changed)
+ }
+ println()
+ }
+
+ if (diff.entDeleteRecords.nonEmpty) {
+ println("Deleted entities: ")
+ diff.entDeleteRecords.foreach { deleted =>
+ println("\t" + deleted.name)
+ }
+ println()
+ }
+
+ if (diff.pointCreateRecords.nonEmpty) {
+ println("Created points: ")
+ diff.pointCreateRecords.foreach { created =>
+ commonCreate(created)
+ println("\t\tCategory: " + created.category)
+ println("\t\tUnit: " + created.unit)
+ }
+ println()
+ }
+
+ if (diff.pointChangedRecords.nonEmpty) {
+ println("Modified points: ")
+ diff.pointChangedRecords.foreach { changed =>
+ commonChange(changed)
+ printParam("Category", changed.categoryOpt)
+ printParam("Unit", changed.unitOpt)
+ }
+ println()
+ }
+
+ if (diff.pointDeleteRecords.nonEmpty) {
+ println("Deleted points: ")
+ diff.pointDeleteRecords.foreach { deleted =>
+ println("\t" + deleted.name)
+ }
+ println()
+ }
+
+ if (diff.commandCreateRecords.nonEmpty) {
+ println("Created commands: ")
+ diff.commandCreateRecords.foreach { created =>
+ commonCreate(created)
+ println("\t\tCategory: " + created.category)
+ println("\t\tDisplay name: " + created.displayName)
+ }
+ println()
+ }
+
+ if (diff.commandChangedRecords.nonEmpty) {
+ println("Modified commands: ")
+ diff.commandChangedRecords.foreach { changed =>
+ commonChange(changed)
+ printParam("Category", changed.categoryOpt)
+ printParam("Display name", changed.displayNameOpt)
+ }
+ println()
+ }
+
+ if (diff.commandDeleteRecords.nonEmpty) {
+ println("Deleted commands: ")
+ diff.commandDeleteRecords.foreach { deleted =>
+ println("\t" + deleted.name)
+ }
+ println()
+ }
+
+ if (diff.endpointCreateRecords.nonEmpty) {
+ println("Created endpoints: ")
+ diff.endpointCreateRecords.foreach { created =>
+ commonCreate(created)
+ println("\t\tProtocol: " + created.protocol)
+ }
+ println()
+ }
+
+ if (diff.endpointChangedRecords.nonEmpty) {
+ println("Modified endpoints: ")
+ diff.endpointChangedRecords.foreach { changed =>
+ commonChange(changed)
+ printParam("Protocol", changed.protocolOpt)
+ }
+ println()
+ }
+
+ if (diff.endpointDeleteRecords.nonEmpty) {
+ println("Deleted endpoints: ")
+ diff.endpointDeleteRecords.foreach { deleted =>
+ println("\t" + deleted.name)
+ }
+ println()
+ }
+
+ if (diff.edgeCreateRecords.nonEmpty) {
+ println("Created edges: ")
+ diff.edgeCreateRecords.foreach { edge =>
+ println("\t" + edge.parent + " --[" + edge.relation + "]--> " + edge.child)
+ }
+ println()
+ }
+
+ if (diff.edgeDeleteRecords.nonEmpty) {
+ println("Deleted edges: ")
+ diff.edgeDeleteRecords.foreach { edge =>
+ println("\t" + edge.parent + " --[" + edge.relation + "]--> " + edge.child)
+ }
+ println()
+ }
+
+ }
+
+ }
+
+ def compare(update: FlatModelFragment, resolvedIds: Seq[(UUID, String)], downloadSet: DownloadSet): (ActionsList, DiffRecord) = {
+ import io.greenbus.loader.set.Actions._
+ import io.greenbus.loader.set.Differences._
+
+ val resolvedNameMap = resolvedIds.map(tup => (tup._2, tup._1)).toMap
+ val resolvedUuidMap = resolvedIds.toMap
+
+ val uuidToEnt = downloadSet.modelEntities.map(obj => (protoUUIDToUuid(obj.getUuid), obj)).toMap
+ val nameToEnt = downloadSet.modelEntities.map(obj => (obj.getName, obj)).toMap
+
+ val uuidToPoint = downloadSet.points.map(obj => (protoUUIDToUuid(obj.getUuid), obj)).toMap
+ val nameToPoint = downloadSet.points.map(obj => (obj.getName, obj)).toMap
+
+ val uuidToCommand = downloadSet.commands.map(obj => (protoUUIDToUuid(obj.getUuid), obj)).toMap
+ val nameToCommand = downloadSet.commands.map(obj => (obj.getName, obj)).toMap
+
+ val uuidToEndpoint = downloadSet.endpoints.map(obj => (protoUUIDToUuid(obj.getUuid), obj)).toMap
+ val nameToEndpoint = downloadSet.endpoints.map(obj => (obj.getName, obj)).toMap
+
+ val uuidToKeyValues: Map[UUID, Seq[EntityKeyValue]] = downloadSet.keyValues.groupBy(kv => protoUUIDToUuid(kv.getUuid))
+
+ val cache = new ActionCache
+ val records = new RecordBuilder
+
+ def handleNewKeyValues(uuidOpt: Option[UUID], name: String, kvs: Seq[(String, ValueHolder)]): Unit = {
+ kvs.foreach {
+ case (key, vh) =>
+ uuidOpt match {
+ case None => cache.keyValuePutByNames += PutKeyValueByName(name, key, vh)
+ case Some(uuid) => cache.keyValuePutByUuids += PutKeyValueByUuid(uuid, key, vh)
+ }
+ }
+ }
+
+ def handleKvs(uuid: UUID, name: String, kvs: Seq[(String, ValueHolder)]): (Set[String], Set[String], Set[String]) = {
+ val currentKvs = uuidToKeyValues.getOrElse(uuid, Seq()).filterNot(_.getKey == "calculation")
+ val (keyPuts, keysAdded, keysModified, keysDeleted) = keySetDiff(currentKvs, kvs)
+
+ keyPuts.foreach(p => cache.keyValuePutByUuids += PutKeyValueByUuid(uuid, p._1, p._2))
+ keysDeleted.foreach(k => cache.keyValueDeletes += DeleteKeyValue(uuid, k))
+
+ (keysAdded, keysModified, keysDeleted)
+ }
+
+ def typeDiff(existing: Set[String], update: Set[String]): (Set[String], Set[String]) = {
+ val typesToAdd = update diff existing
+ val typesToRemove = existing diff update
+ (typesToAdd, typesToRemove)
+ }
+
+ def optParamChange[A](old: A, next: A): Option[ParamChange[A]] = {
+ if (old != next) Some(ParamChange(old, next)) else None
+ }
+
+ def handleEntityUpdate(uuid: UUID, name: String, ent: EntityDesc, existingEnt: Entity): Unit = {
+
+ val nameChangeOpt = optParamChange(existingEnt.getName, name)
+
+ val (typesToAdd, typesToRemove) = typeDiff(existingEnt.getTypesList.toSet, ent.fields.types)
+ val (keysAdded, keysModified, keysDeleted) = handleKvs(uuid, name, ent.fields.kvs)
+ val keyChange = keysAdded.nonEmpty || keysModified.nonEmpty || keysDeleted.nonEmpty
+
+ if (name != existingEnt.getName || typesToAdd.nonEmpty || typesToRemove.nonEmpty || keyChange) {
+ cache.entityPuts += PutEntity(Some(uuid), name, ent.fields.types)
+ records.entChangedRecords += EntityChanged(existingEnt.getName, nameChangeOpt, typesToAdd, typesToRemove, keysAdded, keysModified, keysDeleted)
+ }
+ }
+
+ def handleNewEntity(uuidOpt: Option[UUID], name: String, ent: EntityDesc) {
+ cache.entityPuts += PutEntity(uuidOpt, name, ent.fields.types)
+ val keys = ent.fields.kvs.map(_._1).toSet
+ records.entCreateRecords += EntityCreated(name, ent.fields.types, keys, withUuid = uuidOpt.nonEmpty)
+ handleNewKeyValues(uuidOpt, name, ent.fields.kvs)
+ }
+
+ def handlePointUpdate(uuid: UUID, name: String, pt: PointDesc, existing: Point): Unit = {
+
+ val nameChangeOpt = optParamChange(existing.getName, name)
+
+ val (typesToAdd, typesToRemove) = typeDiff(existing.getTypesList.toSet, pt.fields.types)
+
+ val existCalcOpt = uuidToKeyValues.getOrElse(uuid, Seq()).find(_.getKey == "calculation").map(_.getValue)
+
+ val (calcAdd, calcModify, calcRemove) = calcDiff(pt.calcHolder, existCalcOpt, resolvedNameMap) match {
+ case CalcSame => (Seq(), Seq(), Seq())
+ case CalcCreate =>
+ cache.calcPutsByUuid += PutCalcByUuid(uuid, pt.calcHolder)
+ (Seq("calculation"), Seq(), Seq())
+ case CalcUpdate =>
+ cache.calcPutsByUuid += PutCalcByUuid(uuid, pt.calcHolder)
+ (Seq(), Seq("calculation"), Seq())
+ case CalcDelete =>
+ cache.keyValueDeletes += DeleteKeyValue(uuid, "calculation")
+ (Seq(), Seq(), Seq("calculation"))
+ }
+
+ val pointKeys = pt.fields.kvs.filterNot(_._1 == "calculation")
+ val (nonCalcKeysAdded, nonCalcKeysModified, nonCalcKeysDeleted) = handleKvs(uuid, name, pointKeys)
+ val keysAdded = nonCalcKeysAdded ++ calcAdd
+ val keysModified = nonCalcKeysModified ++ calcModify
+ val keysDeleted = nonCalcKeysDeleted ++ calcRemove
+
+ val keyChange = keysAdded.nonEmpty || keysModified.nonEmpty || keysDeleted.nonEmpty
+
+ val pointCategoryOpt = optParamChange(existing.getPointCategory, pt.pointCategory)
+ val unitOpt = optParamChange(existing.getUnit, pt.unit)
+
+ if (name != existing.getName || typesToAdd.nonEmpty || typesToRemove.nonEmpty || keyChange || pointCategoryOpt.nonEmpty || unitOpt.nonEmpty) {
+ cache.pointPuts += PutPoint(Some(uuid), name, pt.fields.types, pt.pointCategory, pt.unit)
+ records.pointChangedRecords += PointChanged(existing.getName, nameChangeOpt, typesToAdd, typesToRemove, keysAdded, keysModified, keysDeleted, pointCategoryOpt, unitOpt)
+ }
+ }
+
+ def handleNewPoint(uuidOpt: Option[UUID], name: String, pt: PointDesc) {
+ cache.pointPuts += PutPoint(uuidOpt, name, pt.fields.types, pt.pointCategory, pt.unit)
+
+ pt.calcHolder match {
+ case NoCalculation =>
+ case _ =>
+ uuidOpt match {
+ case None => cache.calcPutsByName += PutCalcByName(name, pt.calcHolder)
+ case Some(uuid) => cache.calcPutsByUuid += PutCalcByUuid(uuid, pt.calcHolder)
+ }
+ }
+
+ val allKvs = pt.fields.kvs //++ calcKvSeq
+
+ val keys = allKvs.map(_._1).toSet
+ records.pointCreateRecords += PointCreated(name, pt.fields.types, keys, pt.pointCategory, pt.unit, withUuid = uuidOpt.nonEmpty)
+ handleNewKeyValues(uuidOpt, name, allKvs)
+ }
+
+ def handleCommandUpdate(uuid: UUID, name: String, cmd: CommandDesc, existing: Command): Unit = {
+
+ val nameChangeOpt = optParamChange(existing.getName, name)
+
+ val (typesToAdd, typesToRemove) = typeDiff(existing.getTypesList.toSet, cmd.fields.types)
+ val (keysAdded, keysModified, keysDeleted) = handleKvs(uuid, name, cmd.fields.kvs)
+ val keyChange = keysAdded.nonEmpty || keysModified.nonEmpty || keysDeleted.nonEmpty
+
+ val commandCategoryOpt = optParamChange(existing.getCommandCategory, cmd.commandCategory)
+ val displayNameOpt = optParamChange(existing.getDisplayName, cmd.displayName)
+
+ if (name != existing.getName || typesToAdd.nonEmpty || typesToRemove.nonEmpty || keyChange || commandCategoryOpt.nonEmpty || displayNameOpt.nonEmpty) {
+ cache.commandPuts += PutCommand(Some(uuid), name, cmd.fields.types, cmd.commandCategory, cmd.displayName)
+ records.commandChangedRecords += CommandChanged(existing.getName, nameChangeOpt, typesToAdd, typesToRemove, keysAdded, keysModified, keysDeleted, commandCategoryOpt, displayNameOpt)
+ }
+ }
+
+ def handleNewCommand(uuidOpt: Option[UUID], name: String, cmd: CommandDesc) {
+ cache.commandPuts += PutCommand(uuidOpt, name, cmd.fields.types, cmd.commandCategory, cmd.displayName)
+ val keys = cmd.fields.kvs.map(_._1).toSet
+ records.commandCreateRecords += CommandCreated(name, cmd.fields.types, keys, cmd.commandCategory, cmd.displayName, withUuid = uuidOpt.nonEmpty)
+ handleNewKeyValues(uuidOpt, name, cmd.fields.kvs)
+ }
+
+ def handleEndpointUpdate(uuid: UUID, name: String, end: EndpointDesc, existing: Endpoint): Unit = {
+
+ val nameChangeOpt = optParamChange(existing.getName, name)
+
+ val (typesToAdd, typesToRemove) = typeDiff(existing.getTypesList.toSet, end.fields.types)
+ val (keysAdded, keysModified, keysDeleted) = handleKvs(uuid, name, end.fields.kvs)
+ val keyChange = keysAdded.nonEmpty || keysModified.nonEmpty || keysDeleted.nonEmpty
+
+ val protocolOpt = optParamChange(existing.getProtocol, end.protocol)
+
+ if (name != existing.getName || typesToAdd.nonEmpty || typesToRemove.nonEmpty || keyChange || protocolOpt.nonEmpty) {
+ cache.endpointPuts += PutEndpoint(Some(uuid), name, end.fields.types, end.protocol)
+ records.endpointChangedRecords += EndpointChanged(existing.getName, nameChangeOpt, typesToAdd, typesToRemove, keysAdded, keysModified, keysDeleted, protocolOpt)
+ }
+ }
+
+ def handleNewEndpoint(uuidOpt: Option[UUID], name: String, end: EndpointDesc) {
+ cache.endpointPuts += PutEndpoint(uuidOpt, name, end.fields.types, end.protocol)
+ val keys = end.fields.kvs.map(_._1).toSet
+ records.endpointCreateRecords += EndpointCreated(name, end.fields.types, keys, end.protocol, withUuid = uuidOpt.nonEmpty)
+ handleNewKeyValues(uuidOpt, name, end.fields.kvs)
+ }
+
+ def handleObject[Desc, Proto](
+ obj: Desc,
+ fields: EntityFields,
+ uuidMap: Map[UUID, Proto],
+ nameMap: Map[String, Proto],
+ handleUpdate: (UUID, String, Desc, Proto) => Unit,
+ handleNew: (Option[UUID], String, Desc) => Unit,
+ protoToUuid: Proto => ModelUUID): Unit = {
+
+ fields.id match {
+ case FullEntId(uuid, name) =>
+ // could be import of old system to a new DB
+ // could be name/param change of existing (including no change at all)
+ uuidMap.get(uuid) match {
+ case None => handleNew(Some(uuid), name, obj)
+ case Some(existing) => handleUpdate(uuid, name, obj, existing)
+ }
+ case NamedEntId(name) =>
+ nameMap.get(name) match {
+ case None => handleNew(None, name, obj)
+ case Some(existing) => handleUpdate(protoUUIDToUuid(protoToUuid(existing)), name, obj, existing)
+ }
+ case _ => throw new LoadingException("Entity must have name")
+ }
+ }
+
+ def splitIdSets(ids: Seq[EntityId]): (Set[UUID], Set[String]) = {
+ val (onlyUuids, onlyNames, full) = SetDiff.idReferenceBreakdown(ids)
+ ((onlyUuids ++ full.map(_._1)).toSet, onlyNames.toSet)
+ }
+
+ // if the uuid is present we MUST match that; ignore name
+ val (fragModelUuidSet, fragModelNameSet) = splitIdSets(update.modelEntities.map(_.fields.id))
+ val (fragPointUuidSet, fragPointNameSet) = splitIdSets(update.points.map(_.fields.id))
+ val (fragCommandUuidSet, fragCommandNameSet) = splitIdSets(update.commands.map(_.fields.id))
+ val (fragEndpointUuidSet, fragEndpointNameSet) = splitIdSets(update.endpoints.map(_.fields.id))
+
+ downloadSet.modelEntities.foreach { obj =>
+ val uuid = UUIDHelpers.protoUUIDToUuid(obj.getUuid)
+ if (!fragModelUuidSet.contains(uuid) && !fragModelNameSet.contains(obj.getName)) {
+ cache.entityDeletes += DeleteEntity(uuid)
+ records.entDeleteRecords += EntityDeleted(obj.getName)
+ }
+ }
+
+ downloadSet.points.foreach { obj =>
+ val uuid = UUIDHelpers.protoUUIDToUuid(obj.getUuid)
+ if (!fragPointUuidSet.contains(uuid) && !fragPointNameSet.contains(obj.getName)) {
+ cache.pointDeletes += DeletePoint(uuid)
+ records.pointDeleteRecords += PointDeleted(obj.getName)
+ }
+ }
+ downloadSet.commands.foreach { obj =>
+ val uuid = UUIDHelpers.protoUUIDToUuid(obj.getUuid)
+ if (!fragCommandUuidSet.contains(uuid) && !fragCommandNameSet.contains(obj.getName)) {
+ cache.commandDeletes += DeleteCommand(uuid)
+ records.commandDeleteRecords += CommandDeleted(obj.getName)
+ }
+ }
+ downloadSet.endpoints.foreach { obj =>
+ val uuid = UUIDHelpers.protoUUIDToUuid(obj.getUuid)
+ if (!fragEndpointUuidSet.contains(uuid) && !fragEndpointNameSet.contains(obj.getName)) {
+ cache.endpointDeletes += DeleteEndpoint(uuid)
+ records.endpointDeleteRecords += EndpointDeleted(obj.getName)
+ }
+ }
+
+ update.modelEntities.foreach { ent =>
+ handleObject(ent, ent.fields, uuidToEnt, nameToEnt, handleEntityUpdate, handleNewEntity, { proto: Entity => proto.getUuid })
+ }
+
+ update.points.foreach { pt =>
+ handleObject(pt, pt.fields, uuidToPoint, nameToPoint, handlePointUpdate, handleNewPoint, { proto: Point => proto.getUuid })
+ }
+
+ update.commands.foreach { cmd =>
+ handleObject(cmd, cmd.fields, uuidToCommand, nameToCommand, handleCommandUpdate, handleNewCommand, { proto: Command => proto.getUuid })
+ }
+
+ update.endpoints.foreach { end =>
+ handleObject(end, end.fields, uuidToEndpoint, nameToEndpoint, handleEndpointUpdate, handleNewEndpoint, { proto: Endpoint => proto.getUuid })
+ }
+
+ val (edgeAdds, edgeDeletes) = edgeDiffWhileUnresolved(update.edges, downloadSet.edges, resolvedUuidMap, resolvedNameMap)
+ edgeAdds.foreach { desc =>
+ cache.edgePuts += PutEdge(desc)
+ records.edgeCreateRecords += EdgeRecord(EntityId.prettyPrint(desc.parent), desc.relationship, EntityId.prettyPrint(desc.child))
+ }
+
+ edgeDeletes.foreach { tup =>
+ cache.edgeDeletes += DeleteEdge(tup._1, tup._2, tup._3)
+ records.edgeDeleteRecords += EdgeRecord(resolvedUuidMap(tup._1), tup._2, resolvedUuidMap(tup._3))
+ }
+
+ (cache.result(), records.result())
+ }
+
+ def keySetDiff(original: Seq[EntityKeyValue], update: Seq[(String, ValueHolder)]): (Seq[(String, ValueHolder)], Set[String], Set[String], Set[String]) = {
+ val originalKeyMap = original.map(kv => (kv.getKey, kv)).toMap
+ val originalKeySet = originalKeyMap.keySet
+
+ val updateKeyMap = update.map(kv => (kv._1, kv._2)).toMap
+ val updateKeySet = updateKeyMap.keySet
+
+ val keysToAdd = updateKeySet diff originalKeySet
+ val keysToRemove = originalKeySet diff updateKeySet
+
+ val possibleChangeSet = updateKeyMap -- keysToAdd
+
+ val changed = possibleChangeSet.filter {
+ case (key, vh) =>
+ val origKv = originalKeyMap(key)
+ vh match {
+ case SimpleValue(sv) => !origKv.getValue.equals(sv)
+ case ByteArrayReference(length) => false
+ case ByteArrayValue(bytes) =>
+ !(origKv.getValue.hasByteArrayValue && util.Arrays.equals(bytes, origKv.getValue.getByteArrayValue.toByteArray))
+ case FileReference(ref) => true
+ }
+ }
+
+ val changedKeys = changed.keySet
+
+ val allPuts = update.filter(tup => (keysToAdd ++ changedKeys).contains(tup._1))
+
+ (allPuts, keysToAdd, changedKeys, keysToRemove)
+ }
+
+ sealed trait CalcAction
+ case object CalcSame extends CalcAction
+ case object CalcCreate extends CalcAction
+ case object CalcUpdate extends CalcAction
+ case object CalcDelete extends CalcAction
+
+ def calcDiff(calcHolder: CalculationHolder, existing: Option[StoredValue], nameMap: Map[String, UUID]): CalcAction = {
+
+ val unresolvedOpt = calcHolder match {
+ case NoCalculation => None
+ case unres: UnresolvedCalc => Some(unres)
+ case _ => throw new LoadingException("Calculation must be nonexistent or unresolved")
+ }
+
+ (existing, unresolvedOpt) match {
+ case (None, None) => CalcSame
+ case (None, Some(unres)) => CalcCreate
+ case (Some(sv), None) => CalcDelete
+ case (Some(sv), Some(unres)) =>
+ if (sv.hasByteArrayValue) {
+ try {
+ val desc = CalculationDescriptor.parseFrom(sv.getByteArrayValue)
+ val descWithoutInputs = desc.toBuilder.clearCalcInputs()
+
+ if (descWithoutInputs.build() == unres.builder.build()) {
+ val existingInputs = desc.getCalcInputsList.map(in => (in.getVariableName, in.getPointUuid, in))
+ val existingVars = existingInputs.map(_._1)
+ val updateVars = unres.inputs.map(_._2.getVariableName)
+
+ if (existingVars.sorted == updateVars.sorted) {
+ val existVarMap = existingInputs.map(tup => (tup._1, (tup._2, tup._3))).toMap
+
+ def inputEquals(a: CalculationInput.Builder, b: CalculationInput.Builder): Boolean = {
+ a.clearPointUuid().clearVariableName().build() == b.clearPointUuid().clearVariableName().build()
+ }
+
+ val changed = !unres.inputs.forall {
+ case (id, builder) =>
+ val keyName = builder.getVariableName
+ val existingForKey = existVarMap(keyName)
+ val existingUuid = protoUUIDToUuid(existingForKey._1)
+
+ id match {
+ case FullEntId(uuid, name) =>
+ (uuid == existingUuid || nameMap.get(name) == Some(existingUuid)) &&
+ inputEquals(builder, existingForKey._2.toBuilder)
+ case UuidEntId(uuid) =>
+ uuid == existingUuid && inputEquals(builder, existingForKey._2.toBuilder)
+ case NamedEntId(name) =>
+ nameMap.get(name) == Some(existingUuid) && inputEquals(builder, existingForKey._2.toBuilder)
+ }
+ }
+
+ if (changed) CalcUpdate else CalcSame
+
+ } else {
+ CalcUpdate
+ }
+ } else {
+ CalcUpdate
+ }
+ } catch {
+ case ex: Throwable => CalcUpdate
+ }
+ } else {
+ CalcUpdate
+ }
+ }
+ }
+
+ def resolveCalcOpt(calc: CalculationHolder, nameMap: Map[String, UUID]): Option[CalculationDescriptor] = {
+ calc match {
+ case NoCalculation => None
+ case UnresolvedCalc(builder, inputs) =>
+ val resolvedInputs = inputs.map {
+ case (id, inputBuilder) =>
+ val resolvedUuid = entIdToUuid(id, nameMap)
+ inputBuilder.setPointUuid(uuidToProtoUUID(resolvedUuid)).build()
+ }
+
+ resolvedInputs.foreach(builder.addCalcInputs)
+ Some(builder.build())
+
+ case CalcNotChecked | _: NamesResolvedCalc => throw new IllegalArgumentException("Can only resolve calculations from XML import")
+ }
+ }
+
+ def resolveCalc(calc: CalculationHolder, nameMap: Map[String, UUID]): CalculationDescriptor = {
+ calc match {
+ case UnresolvedCalc(builder, inputs) =>
+
+ val resolvedInputs = inputs.map {
+ case (id, inputBuilder) =>
+ val resolvedUuid = entIdToUuid(id, nameMap)
+ inputBuilder.setPointUuid(uuidToProtoUUID(resolvedUuid)).build()
+ }
+
+ resolvedInputs.foreach(builder.addCalcInputs)
+ builder.build()
+
+ case _ => throw new IllegalArgumentException("Can only resolve calculations from XML import")
+ }
+ }
+
+ def edgeDiffWhileUnresolved(
+ updateDescs: Seq[EdgeDesc],
+ existingEdges: Seq[EntityEdge],
+ uuidMap: Map[UUID, String],
+ nameMap: Map[String, UUID]): (Seq[EdgeDesc], Set[(UUID, String, UUID)]) = {
+
+ val existingSet = existingEdges.map(e => (protoUUIDToUuid(e.getParent), e.getRelationship, protoUUIDToUuid(e.getChild))).toSet
+
+ val adds = updateDescs.filter { desc =>
+ val parentUuidOpt = entIdToUuidOpt(desc.parent, nameMap)
+ val childUuidOpt = entIdToUuidOpt(desc.child, nameMap)
+ (parentUuidOpt, childUuidOpt) match {
+ case (Some(parentUuid), Some(childUuid)) => !existingSet.contains((parentUuid, desc.relationship, childUuid))
+ case _ => true
+ }
+ }
+
+ val updateSet = updateDescs.toSet
+
+ val removes = existingSet.filter {
+ case (parentUuid, relationship, childUuid) =>
+ val parentName = uuidMap.getOrElse(parentUuid, throw new LoadingException(s"Existing edge referred to UUID $parentUuid that was unresolved"))
+ val childName = uuidMap.getOrElse(childUuid, throw new LoadingException(s"Existing edge referred to UUID $childUuid that was unresolved"))
+
+ def permute(uuid: UUID, name: String) = Seq(FullEntId(uuid, name), UuidEntId(uuid), NamedEntId(name))
+
+ val equivalentDescs: Seq[EdgeDesc] = for {
+ parentId <- permute(parentUuid, parentName)
+ childId <- permute(childUuid, childName)
+ } yield {
+ EdgeDesc(parentId, relationship, childId)
+ }
+
+ !equivalentDescs.exists(updateSet.contains)
+ }
+
+ (adds, removes)
+ }
+
+ def resolveEdgeDesc(desc: EdgeDesc, nameMap: Map[String, UUID]): (UUID, String, UUID) = {
+ (entIdToUuid(desc.parent, nameMap), desc.relationship, entIdToUuid(desc.child, nameMap))
+ }
+
+ def entIdToUuid(id: EntityId, nameMap: Map[String, UUID]): UUID = {
+ id match {
+ case FullEntId(uuid, _) => uuid
+ case UuidEntId(uuid) => uuid
+ case NamedEntId(name) => nameMap.getOrElse(name, throw new IllegalArgumentException(s"Referenced unresolved name: $name"))
+ }
+ }
+ def entIdToUuidOpt(id: EntityId, nameMap: Map[String, UUID]): Option[UUID] = {
+ id match {
+ case FullEntId(uuid, _) => Some(uuid)
+ case UuidEntId(uuid) => Some(uuid)
+ case NamedEntId(name) => nameMap.get(name)
+ }
+ }
+}
diff --git a/loading/src/main/scala/io/greenbus/loader/set/LoadingException.scala b/loading/src/main/scala/io/greenbus/loader/set/LoadingException.scala
new file mode 100644
index 0000000..093dfac
--- /dev/null
+++ b/loading/src/main/scala/io/greenbus/loader/set/LoadingException.scala
@@ -0,0 +1,21 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.loader.set
+
+class LoadingException(msg: String) extends Exception(msg)
\ No newline at end of file
diff --git a/loading/src/main/scala/io/greenbus/loader/set/Mdl.scala b/loading/src/main/scala/io/greenbus/loader/set/Mdl.scala
new file mode 100644
index 0000000..4dd4589
--- /dev/null
+++ b/loading/src/main/scala/io/greenbus/loader/set/Mdl.scala
@@ -0,0 +1,283 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.loader.set
+
+import java.util.UUID
+
+import io.greenbus.client.service.proto.Calculations.{ CalculationDescriptor, CalculationInput }
+import io.greenbus.client.service.proto.Model
+import io.greenbus.client.service.proto.Model.StoredValue
+
+object EntityId {
+ def optional(id: EntityId): (Option[UUID], Option[String]) = {
+ id match {
+ case NamedEntId(name) => (None, Some(name))
+ case UuidEntId(uuid) => (Some(uuid), None)
+ case FullEntId(uuid, name) => (Some(uuid), Some(name))
+ }
+ }
+
+ def toUuidOpt(id: EntityId): Option[UUID] = {
+ id match {
+ case UuidEntId(uuid) => Some(uuid)
+ case FullEntId(uuid, name) => Some(uuid)
+ case NamedEntId(_) => None
+ }
+ }
+ def toNameOpt(id: EntityId): Option[String] = {
+ id match {
+ case UuidEntId(_) => None
+ case FullEntId(_, name) => Some(name)
+ case NamedEntId(name) => Some(name)
+ }
+ }
+
+ def apply(optUuid: Option[UUID], optName: Option[String]): EntityId = {
+ (optUuid, optName) match {
+ case (None, None) => throw new LoadingException("Must have either name or UUID")
+ case (Some(uuid), None) => UuidEntId(uuid)
+ case (None, Some(name)) => NamedEntId(name)
+ case (Some(uuid), Some(name)) => FullEntId(uuid, name)
+ }
+ }
+
+ def prettyPrint(id: EntityId): String = {
+ id match {
+ case NamedEntId(name) => name
+ case UuidEntId(uuid) => uuid.toString
+ case FullEntId(uuid, name) => name
+ }
+ }
+
+ def lessThan(a: EntityId, b: EntityId): Boolean = {
+ (optional(a), optional(b)) match {
+ case ((_, Some(nameA)), (_, Some(nameB))) => nameA < nameB
+ case ((_, Some(_)), (_, None)) => true
+ case ((_, None), (_, Some(_))) => false
+ case ((Some(uuidA), None), (Some(uuidB), None)) => uuidA.compareTo(uuidB) < 0
+ case _ => false
+ }
+ }
+
+ implicit def ordering[A <: EntityId]: Ordering[EntityId] = {
+ Ordering.fromLessThan(lessThan)
+ }
+}
+
+sealed trait EntityId
+case class NamedEntId(name: String) extends EntityId
+case class UuidEntId(uuid: UUID) extends EntityId
+case class FullEntId(uuid: UUID, name: String) extends EntityId
+
+case class EntityFields(
+ id: EntityId,
+ types: Set[String],
+ kvs: Seq[(String, ValueHolder)])
+
+sealed trait ValueHolder
+case class SimpleValue(sv: StoredValue) extends ValueHolder
+case class ByteArrayValue(bytes: Array[Byte]) extends ValueHolder
+case class ByteArrayReference(size: Long) extends ValueHolder
+case class FileReference(name: String) extends ValueHolder
+
+sealed trait CalculationHolder
+case object NoCalculation extends CalculationHolder
+case object CalcNotChecked extends CalculationHolder
+case class UnresolvedCalc(builder: CalculationDescriptor.Builder, inputs: Seq[(EntityId, CalculationInput.Builder)]) extends CalculationHolder
+case class NamesResolvedCalc(calc: CalculationDescriptor, uuidToName: Map[UUID, String]) extends CalculationHolder
+
+object Mdl {
+
+ sealed trait ModelObject {
+ val fields: EntityFields
+ }
+
+ sealed trait EquipNode extends ModelObject
+
+ case class EntityDesc(fields: EntityFields) extends EquipNode
+
+ case class PointDesc(
+ fields: EntityFields,
+ pointCategory: Model.PointCategory,
+ unit: String,
+ calcHolder: CalculationHolder) extends EquipNode
+
+ case class CommandDesc(
+ fields: EntityFields,
+ displayName: String,
+ commandCategory: Model.CommandCategory) extends EquipNode
+
+ case class EndpointDesc(
+ fields: EntityFields,
+ protocol: String) extends ModelObject
+
+ case class EdgeDesc(parent: EntityId, relationship: String, child: EntityId)
+
+ case class FlatModelFragment(
+ modelEntities: Seq[EntityDesc],
+ points: Seq[PointDesc],
+ commands: Seq[CommandDesc],
+ endpoints: Seq[EndpointDesc],
+ edges: Seq[EdgeDesc])
+
+ case class TreeNode(
+ node: EquipNode,
+ children: Seq[TreeNode],
+ extParentRefs: Seq[EntityId],
+ otherParentRefs: Seq[(EntityId, String)],
+ otherChildRefs: Seq[(EntityId, String)],
+ commandRefs: Seq[EntityId])
+
+ case class EndpointNode(node: EndpointDesc,
+ sourceRefs: Seq[EntityId],
+ otherParentRefs: Seq[(EntityId, String)],
+ otherChildRefs: Seq[(EntityId, String)])
+
+ case class TreeModelFragment(
+ roots: Seq[TreeNode],
+ endpoints: Seq[EndpointNode])
+
+ def forceEntIdToUuid(id: EntityId): UUID = {
+ EntityId.toUuidOpt(id).getOrElse(throw new LoadingException("Must have UUID resolved"))
+ }
+
+ def flatToTree(flat: FlatModelFragment): TreeModelFragment = {
+
+ val uuidAndEquips = flat.modelEntities.map(e => (forceEntIdToUuid(e.fields.id), e))
+ val uuidAndPoints = flat.points.map(e => (forceEntIdToUuid(e.fields.id), e))
+ val uuidAndCommands = flat.commands.map(e => (forceEntIdToUuid(e.fields.id), e))
+
+ val equipUuids = uuidAndEquips.map(_._1) ++ uuidAndPoints.map(_._1) ++ uuidAndCommands.map(_._1)
+ val equipUuidSet: Set[UUID] = equipUuids.toSet
+
+ val roots = findRoots(equipUuids, flat.edges)
+
+ val parentToChildMap = flat.edges.groupBy(e => forceEntIdToUuid(e.parent))
+ val childToParentMap = flat.edges.groupBy(e => forceEntIdToUuid(e.child))
+
+ val equipNodeMap: Map[UUID, EquipNode] = (uuidAndEquips ++ uuidAndPoints ++ uuidAndCommands).toMap
+
+ def fillTree(uuids: Seq[UUID]): Seq[TreeNode] = {
+ val nodes: Seq[(UUID, EquipNode)] = uuids.flatMap(uuid => equipNodeMap.get(uuid).map(n => (uuid, n)))
+ nodes.map {
+ case (uuid, node) =>
+ val allChildEdges = parentToChildMap.getOrElse(uuid, Seq())
+ val allParentEdges = childToParentMap.getOrElse(uuid, Seq())
+
+ val extParentOwns = allParentEdges
+ .filter(e => e.relationship == "owns" && !equipUuidSet.contains(forceEntIdToUuid(e.parent)))
+ .map(e => e.parent)
+
+ val ownsChildren = allChildEdges.filter(_.relationship == "owns").map(_.child).map(forceEntIdToUuid)
+
+ val commandRefs = allChildEdges.filter(_.relationship == "feedback").map(e => e.child)
+
+ val builtInSet = Set("owns", "source", "feedback")
+
+ val otherParents = allParentEdges
+ .filterNot(e => builtInSet.contains(e.relationship))
+ .map(e => (e.parent, e.relationship))
+
+ val otherChildren = allChildEdges
+ .filterNot(e => builtInSet.contains(e.relationship))
+ .map(e => (e.child, e.relationship))
+
+ TreeNode(
+ node,
+ fillTree(ownsChildren),
+ extParentOwns,
+ otherParents,
+ otherChildren,
+ commandRefs)
+ }
+ }
+
+ val rootNodes = fillTree(roots)
+
+ val endpointNodes = flat.endpoints.map { desc =>
+ val uuid = forceEntIdToUuid(desc.fields.id)
+ val allChildEdges = parentToChildMap.getOrElse(uuid, Seq())
+ val allParentEdges = childToParentMap.getOrElse(uuid, Seq())
+
+ val (sourceEdges, otherChildEdges) = allChildEdges.partition(_.relationship == "source")
+
+ val sourceRefs = sourceEdges.map(_.child)
+
+ EndpointNode(desc, sourceRefs, allParentEdges.map(e => (e.parent, e.relationship)), otherChildEdges.map(e => (e.child, e.relationship)))
+ }
+
+ TreeModelFragment(rootNodes, endpointNodes)
+ }
+
+ private def findRoots(nodes: Seq[UUID], edges: Seq[EdgeDesc]): Seq[UUID] = {
+ val nodeSet = nodes.toSet
+
+ val nodesWithParentInSet = edges.filter(e => e.relationship == "owns" && nodeSet.contains(forceEntIdToUuid(e.parent)))
+ .map(_.child)
+ .map(forceEntIdToUuid)
+ .toSet
+
+ nodes.filterNot(nodesWithParentInSet.contains)
+ }
+
+ def treeToFlat(tree: TreeModelFragment): FlatModelFragment = {
+
+ val entities = Vector.newBuilder[EntityDesc]
+ val points = Vector.newBuilder[PointDesc]
+ val commands = Vector.newBuilder[CommandDesc]
+ val endpoints = Vector.newBuilder[EndpointDesc]
+
+ val edges = Vector.newBuilder[EdgeDesc]
+
+ def traverse(node: TreeNode): EntityId = {
+
+ val nodeId = node.node match {
+ case desc: EntityDesc =>
+ entities += desc; desc.fields.id
+ case desc: PointDesc =>
+ points += desc; desc.fields.id
+ case desc: CommandDesc =>
+ commands += desc; desc.fields.id
+ }
+
+ node.extParentRefs.map(parent => EdgeDesc(parent, "owns", nodeId)).foreach(edges.+=)
+ node.otherChildRefs.map { case (child, rel) => EdgeDesc(nodeId, rel, child) }.foreach(edges.+=)
+ node.otherParentRefs.map { case (parent, rel) => EdgeDesc(parent, rel, nodeId) }.foreach(edges.+=)
+ node.commandRefs.map(cmdId => EdgeDesc(nodeId, "feedback", cmdId)).foreach(edges.+=)
+
+ val childIds = node.children.map(traverse)
+
+ childIds.map(childId => EdgeDesc(nodeId, "owns", childId)).foreach(edges.+=)
+
+ nodeId
+ }
+
+ tree.roots.foreach(traverse)
+
+ tree.endpoints.foreach { node =>
+ endpoints += node.node
+ val nodeId = node.node.fields.id
+ node.sourceRefs.foreach(ref => edges += EdgeDesc(nodeId, "source", ref))
+ node.otherParentRefs.foreach { case (ref, relation) => edges += EdgeDesc(ref, relation, nodeId) }
+ node.otherChildRefs.foreach { case (ref, relation) => edges += EdgeDesc(nodeId, relation, ref) }
+ }
+
+ FlatModelFragment(entities.result(), points.result(), commands.result(), endpoints.result(), edges.result())
+ }
+}
diff --git a/loading/src/main/scala/io/greenbus/loader/set/SetDiff.scala b/loading/src/main/scala/io/greenbus/loader/set/SetDiff.scala
new file mode 100644
index 0000000..11ad18c
--- /dev/null
+++ b/loading/src/main/scala/io/greenbus/loader/set/SetDiff.scala
@@ -0,0 +1,220 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.loader.set
+
+import java.util.UUID
+
+import io.greenbus.loader.set.Mdl.{ FlatModelFragment, ModelObject }
+
+object SetDiff {
+
+ def checkForDuplicateKeys(flat: FlatModelFragment): Unit = {
+
+ def checkEnt(e: ModelObject): Unit = { check(e.fields) }
+
+ def check(fields: EntityFields): Unit = {
+ fields.kvs.groupBy(_._1).foreach {
+ case (key, tups) =>
+ if (tups.size > 1) {
+ throw new LoadingException(s"Duplicate key $key for ${EntityId.prettyPrint(fields.id)}")
+ }
+ }
+ }
+
+ flat.modelEntities.foreach(checkEnt)
+ flat.points.foreach(checkEnt)
+ flat.commands.foreach(checkEnt)
+ flat.endpoints.foreach(checkEnt)
+ }
+
+ def analyzeIdentities(flat: FlatModelFragment): (Seq[UUID], Seq[String], Seq[(UUID, String)]) = {
+
+ val (objUuidsSet, allObjNames, idTuples) = fragmentIdBreakdown(flat)
+
+ def filterExternalRefs(refUuids: Seq[UUID], refNames: Seq[String], refFullIds: Seq[(UUID, String)]) = {
+ val externalUuidOnlyRefs = refUuids.filterNot(objUuidsSet.contains)
+ val externalNameRefs = refNames.filterNot(allObjNames.contains)
+
+ val externalUuidsOfFullRefs = refFullIds.filterNot { case (uuid, name) => refUuids.contains(uuid) || refNames.contains(name) }.map(_._1)
+
+ val allExternalUuidRefs = externalUuidOnlyRefs ++ externalUuidsOfFullRefs
+
+ (allExternalUuidRefs, externalNameRefs)
+ }
+
+ val allRefIds = flat.edges.flatMap { edge => Seq(edge.parent, edge.child) }
+ val (refUuids, refNames, refFullIds) = idReferenceBreakdown(allRefIds)
+ val (extEdgeRefsByUuid, extEdgeRefsByName) = filterExternalRefs(refUuids, refNames, refFullIds)
+
+ val calcRefIds = flat.points.flatMap(p => calcHolderRefs(p.calcHolder))
+ val (calcRefUuids, calcRefNames, calcRefFullIds) = idReferenceBreakdown(calcRefIds)
+ val (extCalcRefsByUuid, extCalcRefsByName) = filterExternalRefs(calcRefUuids, calcRefNames, calcRefFullIds)
+
+ (extEdgeRefsByUuid ++ extCalcRefsByUuid, extEdgeRefsByName ++ extCalcRefsByName, idTuples)
+ }
+
+ def resolveIdentities(flat: FlatModelFragment, foundUuids: Set[UUID], foundNames: Set[String]) = {
+
+ val (objUuidsSet, objNamesSet, _) = fragmentIdBreakdown(flat)
+
+ val extantUuids = foundUuids ++ objUuidsSet
+ val extantNames = foundNames ++ objNamesSet
+
+ def resolveId(id: EntityId): Boolean = {
+ id match {
+ case FullEntId(uuid, name) => extantUuids.contains(uuid) || extantNames.contains(name)
+ case UuidEntId(uuid) => extantUuids.contains(uuid)
+ case NamedEntId(name) => extantNames.contains(name)
+ }
+ }
+
+ flat.points.foreach { point =>
+ val ids = calcHolderRefs(point.calcHolder)
+ ids.foreach { id =>
+ if (!resolveId(id)) {
+ throw new LoadingException(s"Calculation for point ${EntityId.prettyPrint(point.fields.id)} has unresolved reference to input ${EntityId.prettyPrint(id)}")
+ }
+ }
+ }
+
+ val resolvedEdges = flat.edges.flatMap { edge =>
+ if (resolveId(edge.parent) && resolveId(edge.child)) Some(edge) else None
+ }
+
+ flat.copy(edges = resolvedEdges)
+ }
+
+ private def fragmentIdBreakdown(flat: FlatModelFragment): (Set[UUID], Set[String], Seq[(UUID, String)]) = {
+ val allFields = flat.modelEntities.map(_.fields.id) ++
+ flat.points.map(_.fields.id) ++
+ flat.commands.map(_.fields.id) ++
+ flat.endpoints.map(_.fields.id)
+
+ val (objNamesOnly, objFullIds) = objectIdBreakdown(allFields)
+ val objUuidsSet = objFullIds.map(_._1).toSet
+ val allObjNames = (objNamesOnly ++ objFullIds.map(_._2)).toSet
+
+ (objUuidsSet, allObjNames, objFullIds)
+ }
+
+ def objectIdBreakdown(ids: Seq[EntityId]): (Seq[String], Seq[(UUID, String)]) = {
+
+ val namedOnly = Vector.newBuilder[String]
+ val fullyIdentified = Vector.newBuilder[(UUID, String)]
+
+ ids.foreach {
+ case FullEntId(uuid, name) => fullyIdentified += ((uuid, name))
+ case NamedEntId(name) => namedOnly += name
+ case _ => throw new LoadingException("IDs for model objects must include a name")
+ }
+
+ (namedOnly.result(), fullyIdentified.result())
+ }
+
+ def idReferenceBreakdown(ids: Seq[EntityId]): (Seq[UUID], Seq[String], Seq[(UUID, String)]) = {
+
+ val uuidOnly = Vector.newBuilder[UUID]
+ val namedOnly = Vector.newBuilder[String]
+ val fullyIdentified = Vector.newBuilder[(UUID, String)]
+
+ ids.foreach {
+ case FullEntId(uuid, name) => fullyIdentified += ((uuid, name))
+ case NamedEntId(name) => namedOnly += name
+ case UuidEntId(uuid) => uuidOnly += uuid
+ }
+
+ (uuidOnly.result(), namedOnly.result(), fullyIdentified.result())
+ }
+
+ def checkForNameDuplicatesInFragment(names: Seq[String]): Unit = {
+ val grouped = names.groupBy(s => s)
+
+ val duplicateGroups = grouped.values.filter(_.size > 1).toVector
+
+ if (duplicateGroups.nonEmpty) {
+ val duplicates = duplicateGroups.map(_.head)
+
+ throw new LoadingException("The following names were duplicated in the model: " + duplicates.mkString(", "))
+ }
+ }
+
+ def namesInSet(set: FlatModelFragment): Seq[String] = {
+ val allFields = set.modelEntities.map(_.fields) ++ set.points.map(_.fields) ++ set.commands.map(_.fields) ++ set.endpoints.map(_.fields)
+
+ val namesInFragment = allFields.map(_.id).map {
+ case FullEntId(uuid, name) => name
+ case NamedEntId(name) => name
+ case UuidEntId(uuid) => throw new LoadingException("Equipment must have a name specified")
+ }
+
+ namesInFragment
+ }
+
+ def findUnresolvedNameReferences(set: FlatModelFragment, namesInFragmentSet: Set[String]): Seq[String] = {
+
+ val nameOnlyEdgeReferences = set.edges.flatMap { edge => Seq(idToOnlyNameOpt(edge.parent), idToOnlyNameOpt(edge.child)).flatten }
+
+ val unresolvedEdgeNames = nameOnlyEdgeReferences.filterNot(namesInFragmentSet.contains)
+
+ val nameOnlyCalcReferences = set.points.map(_.calcHolder).flatMap(calcHolderToOnlyNameOpt)
+
+ val unresolvedCalcInputNames = nameOnlyCalcReferences.filterNot(namesInFragmentSet.contains)
+
+ unresolvedEdgeNames ++ unresolvedCalcInputNames
+ }
+
+ private def calcHolderRefs(calc: CalculationHolder): Seq[EntityId] = {
+ calc match {
+ case UnresolvedCalc(_, inputs) => inputs.map(_._1)
+ case _ => Seq()
+ }
+ }
+
+ private def calcHolderToOnlyNameOpt(calc: CalculationHolder): Seq[String] = {
+ calc match {
+ case UnresolvedCalc(_, inputs) =>
+ inputs.flatMap {
+ case (id, _) => idToOnlyNameOpt(id)
+ }
+ case _ => Seq()
+ }
+ }
+
+ private def idToOnlyNameOpt(id: EntityId): Option[String] = {
+ id match {
+ case NamedEntId(name) => Some(name)
+ case _ => None
+ }
+ }
+
+ private def idToNameOpt(id: EntityId): Option[String] = {
+ id match {
+ case NamedEntId(name) => Some(name)
+ case FullEntId(_, name) => Some(name)
+ case _ => None
+ }
+ }
+
+ private def idToUuidNamePair(id: EntityId): Option[(UUID, String)] = {
+ id match {
+ case FullEntId(uuid, name) => Some((uuid, name))
+ case _ => None
+ }
+ }
+}
diff --git a/loading/src/main/scala/io/greenbus/loader/set/Tracker.scala b/loading/src/main/scala/io/greenbus/loader/set/Tracker.scala
new file mode 100644
index 0000000..e493432
--- /dev/null
+++ b/loading/src/main/scala/io/greenbus/loader/set/Tracker.scala
@@ -0,0 +1,54 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.loader.set
+
+import java.io.PrintStream
+
+trait Tracker {
+ def added()
+ def added(n: Int)
+}
+
+class PrintTracker(width: Int, stream: PrintStream) extends Tracker {
+
+ private var index = 0
+
+ private def incr() = {
+ index = index + 1
+ if (index > width) {
+ stream.append('\n')
+ index = 0
+ }
+ }
+
+ def added(n: Int) {
+ Range(0, n).foreach(_ => added())
+ }
+
+ def added() {
+ stream.append(".")
+ incr()
+ }
+}
+
+object SilentTracker extends Tracker {
+ def added(): Unit = {}
+
+ def added(n: Int): Unit = {}
+}
\ No newline at end of file
diff --git a/loading/src/main/scala/io/greenbus/loader/set/UUIDHelpers.scala b/loading/src/main/scala/io/greenbus/loader/set/UUIDHelpers.scala
new file mode 100644
index 0000000..ba9fa00
--- /dev/null
+++ b/loading/src/main/scala/io/greenbus/loader/set/UUIDHelpers.scala
@@ -0,0 +1,31 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.loader.set
+
+import java.util.UUID
+
+import io.greenbus.client.service.proto.Model.{ ModelID, ModelUUID }
+
+object UUIDHelpers {
+ implicit def uuidToProtoUUID(uuid: UUID): ModelUUID = ModelUUID.newBuilder().setValue(uuid.toString).build()
+ implicit def protoUUIDToUuid(uuid: ModelUUID): UUID = UUID.fromString(uuid.getValue)
+
+ implicit def longToProtoId(id: Long): ModelID = ModelID.newBuilder().setValue(id.toString).build()
+ implicit def protoIdToLong(id: ModelID): Long = id.getValue.toLong
+}
diff --git a/loading/src/main/scala/io/greenbus/loader/set/Upload.scala b/loading/src/main/scala/io/greenbus/loader/set/Upload.scala
new file mode 100644
index 0000000..6629574
--- /dev/null
+++ b/loading/src/main/scala/io/greenbus/loader/set/Upload.scala
@@ -0,0 +1,209 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.loader.set
+
+import java.io.File
+import java.util.UUID
+
+import com.google.protobuf.ByteString
+import org.apache.commons.io.FileUtils
+import io.greenbus.msg.Session
+import io.greenbus.client.service.ModelService
+import io.greenbus.client.service.proto.Model.{ EntityKeyValue, StoredValue }
+import io.greenbus.client.service.proto.ModelRequests._
+import io.greenbus.loader.set.Actions._
+import io.greenbus.loader.set.UUIDHelpers._
+import io.greenbus.util.Timing
+
+import scala.collection.JavaConversions._
+import scala.concurrent.duration._
+import scala.concurrent.{ Await, Future }
+
+object Upload {
+
+ val allChunkSize = 300
+
+ def push(session: Session, actions: ActionsList, idTuples: Seq[(UUID, String)]) = {
+
+ val timer = Timing.Stopwatch.start
+ val tracker = new PrintTracker(60, System.out)
+
+ val modelClient = ModelService.client(session)
+
+ val keyValuesByNameLoaded = actions.keyValuePutByNames.map(kvn => (kvn.name, kvn.key, valueHolderToStoredValue(kvn.vh)))
+ val keyValuesByUuidLoaded = actions.keyValuePutByUuids.map(kvu => (kvu.uuid, kvu.key, valueHolderToStoredValue(kvu.vh)))
+
+ println("\n== Progress:")
+
+ // DELETES
+ chunkedCalls(allChunkSize, actions.entityDeletes.map(_.uuid).map(uuidToProtoUUID), modelClient.delete, tracker)
+ chunkedCalls(allChunkSize, actions.pointDeletes.map(_.uuid).map(uuidToProtoUUID), modelClient.deletePoints, tracker)
+ chunkedCalls(allChunkSize, actions.commandDeletes.map(_.uuid).map(uuidToProtoUUID), modelClient.deleteCommands, tracker)
+ chunkedCalls(allChunkSize, actions.endpointDeletes.map(_.uuid).map(uuidToProtoUUID), modelClient.deleteEndpoints, tracker)
+
+ val delKeyPairs = actions.keyValueDeletes.map(dkv => EntityKeyPair.newBuilder().setUuid(uuidToProtoUUID(dkv.uuid)).setKey(dkv.key).build())
+ val deletedKvs = chunkedCalls(allChunkSize, delKeyPairs, modelClient.deleteEntityKeyValues, tracker)
+
+ chunkedCalls(allChunkSize, actions.edgeDeletes.map(toTemplate), modelClient.deleteEdges, tracker)
+
+ // ADDS/ MODIFIES
+ val ents = chunkedCalls(allChunkSize, actions.entityPuts.map(toTemplate), modelClient.put, tracker)
+
+ val points = chunkedCalls(allChunkSize, actions.pointPuts.map(toTemplate), modelClient.putPoints, tracker)
+
+ val commands = chunkedCalls(allChunkSize, actions.commandPuts.map(toTemplate), modelClient.putCommands, tracker)
+
+ val endpoints = chunkedCalls(allChunkSize, actions.endpointPuts.map(toTemplate), modelClient.putEndpoints, tracker)
+
+ val prevIdMap = idTuples.map(tup => (tup._2, tup._1)).toMap
+
+ val putNameMap: Map[String, UUID] = (ents.map(obj => (obj.getName, protoUUIDToUuid(obj.getUuid))) ++
+ points.map(obj => (obj.getName, protoUUIDToUuid(obj.getUuid))) ++
+ commands.map(obj => (obj.getName, protoUUIDToUuid(obj.getUuid))) ++
+ endpoints.map(obj => (obj.getName, protoUUIDToUuid(obj.getUuid)))).toMap
+
+ val nameMap = prevIdMap ++ putNameMap
+
+ def nameToUuid(name: String) = nameMap.getOrElse(name, throw new LoadingException(s"Name $name not found from object puts"))
+
+ val kvPuts: Seq[(UUID, String, StoredValue)] =
+ keyValuesByNameLoaded.map { case (name, key, sv) => (nameToUuid(name), key, sv) } ++
+ keyValuesByUuidLoaded
+
+ val kvs = chunkedCalls(allChunkSize, kvPuts.map(toTemplate), modelClient.putEntityKeyValues, tracker)
+
+ chunkedCalls(allChunkSize, actions.calcPutsByUuid.map(put => buildCalc(put.uuid, put.calc, nameMap)), modelClient.putEntityKeyValues, tracker)
+ chunkedCalls(allChunkSize, actions.calcPutsByName.map(put => buildCalc(nameToUuid(put.name), put.calc, nameMap)), modelClient.putEntityKeyValues, tracker)
+
+ val resolvedEdgeAdds = actions.edgePuts.map(put => Importer.resolveEdgeDesc(put.desc, nameMap)).distinct
+
+ chunkedCalls(allChunkSize, resolvedEdgeAdds.map(toTemplate), modelClient.putEdges, tracker)
+
+ println("\n")
+ println(s"== Finished in ${timer.elapsed} ms")
+ }
+
+ def chunkedCalls[A, B](chunkSize: Int, all: Seq[A], put: Seq[A] => Future[Seq[B]], tracker: Tracker): Seq[B] = {
+ all.grouped(chunkSize).flatMap { chunk =>
+ val results = Await.result(put(chunk), Duration(60000, MILLISECONDS))
+ tracker.added()
+ results
+ }.toVector
+ }
+
+ def toTemplate(act: PutEntity): EntityTemplate = {
+ val b = EntityTemplate.newBuilder()
+ .setName(act.name)
+ .addAllTypes(act.types)
+
+ act.uuidOpt.map(uuidToProtoUUID).foreach(b.setUuid)
+
+ b.build()
+ }
+
+ def toTemplate(act: PutPoint): PointTemplate = {
+ val entB = EntityTemplate.newBuilder()
+ .setName(act.name)
+ .addAllTypes(act.types)
+
+ act.uuidOpt.map(uuidToProtoUUID).foreach(entB.setUuid)
+
+ PointTemplate.newBuilder()
+ .setEntityTemplate(entB.build())
+ .setPointCategory(act.category)
+ .setUnit(act.unit)
+ .build()
+ }
+
+ def toTemplate(act: PutCommand): CommandTemplate = {
+ val entB = EntityTemplate.newBuilder()
+ .setName(act.name)
+ .addAllTypes(act.types)
+
+ act.uuidOpt.map(uuidToProtoUUID).foreach(entB.setUuid)
+
+ CommandTemplate.newBuilder()
+ .setEntityTemplate(entB.build())
+ .setCategory(act.category)
+ .setDisplayName(act.displayName)
+ .build()
+ }
+
+ def toTemplate(act: PutEndpoint): EndpointTemplate = {
+ val entB = EntityTemplate.newBuilder()
+ .setName(act.name)
+ .addAllTypes(act.types)
+
+ act.uuidOpt.map(uuidToProtoUUID).foreach(entB.setUuid)
+
+ EndpointTemplate.newBuilder()
+ .setEntityTemplate(entB.build())
+ .setProtocol(act.protocol)
+ .build()
+ }
+
+ def toTemplate(tup: (UUID, String, UUID)): EntityEdgeDescriptor = {
+ EntityEdgeDescriptor.newBuilder()
+ .setParentUuid(uuidToProtoUUID(tup._1))
+ .setChildUuid(uuidToProtoUUID(tup._3))
+ .setRelationship(tup._2)
+ .build()
+ }
+ def toTemplate(act: DeleteEdge): EntityEdgeDescriptor = {
+ EntityEdgeDescriptor.newBuilder()
+ .setParentUuid(uuidToProtoUUID(act.parent))
+ .setChildUuid(uuidToProtoUUID(act.child))
+ .setRelationship(act.relationship)
+ .build()
+ }
+
+ def toTemplate(kv: (UUID, String, StoredValue)): EntityKeyValue = {
+ val (uuid, key, sv) = kv
+ EntityKeyValue.newBuilder()
+ .setUuid(uuidToProtoUUID(uuid))
+ .setKey(key)
+ .setValue(sv)
+ .build()
+ }
+
+ def buildCalc(uuid: UUID, calculationHolder: CalculationHolder, nameMap: Map[String, UUID]): EntityKeyValue = {
+ val calcDesc = Importer.resolveCalc(calculationHolder, nameMap)
+ EntityKeyValue.newBuilder()
+ .setUuid(uuidToProtoUUID(uuid))
+ .setKey("calculation")
+ .setValue(StoredValue.newBuilder().setByteArrayValue(ByteString.copyFrom(calcDesc.toByteArray)).build())
+ .build()
+ }
+
+ def valueHolderToStoredValue(vh: ValueHolder): StoredValue = {
+ vh match {
+ case SimpleValue(sv) => sv
+ case ByteArrayValue(bytes) => StoredValue.newBuilder().setByteArrayValue(ByteString.copyFrom(bytes)).build()
+ case ByteArrayReference(size) => throw new LoadingException("Cannot upload a byte array referenced by size")
+ case FileReference(filename) =>
+ val bytes = try {
+ FileUtils.readFileToByteArray(new File(filename))
+ } catch {
+ case ex: Throwable => throw new LoadingException(s"Could not open file $filename: " + ex.getMessage)
+ }
+ StoredValue.newBuilder().setByteArrayValue(ByteString.copyFrom(bytes)).build()
+ }
+ }
+
+}
diff --git a/mstore-sql/pom.xml b/mstore-sql/pom.xml
new file mode 100755
index 0000000..8bf6377
--- /dev/null
+++ b/mstore-sql/pom.xml
@@ -0,0 +1,63 @@
+
+ 4.0.0
+
+
+ io.greenbus
+ greenbus-scala-base
+ 3.0.0
+ ../scala-base
+
+
+ greenbus-mstore-sql
+ jar
+
+
+
+ AGPLv3
+ http://www.gnu.org/licenses/agpl-3.0.txt
+
+
+
+
+
+
+
+ com.mycila.maven-license-plugin
+ maven-license-plugin
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ ../
+
+
+
+
+
+
+
+ io.greenbus
+ greenbus-client
+ 3.0.0
+
+
+ io.greenbus
+ greenbus-sql
+ 3.0.0
+
+
+ io.greenbus
+ greenbus-sql
+ 3.0.0
+ test-jar
+ test
+
+
+
+
+
diff --git a/mstore-sql/src/main/scala/io/greenbus/mstore/MeasurementValueSource.scala b/mstore-sql/src/main/scala/io/greenbus/mstore/MeasurementValueSource.scala
new file mode 100644
index 0000000..34207b4
--- /dev/null
+++ b/mstore-sql/src/main/scala/io/greenbus/mstore/MeasurementValueSource.scala
@@ -0,0 +1,38 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.mstore
+
+import java.util.UUID
+import io.greenbus.client.service.proto.Measurements.Measurement
+
+trait MeasurementValueSource {
+
+ def get(ids: Seq[UUID]): Seq[(UUID, Measurement)]
+}
+
+trait MeasurementHistorySource {
+ def getHistory(id: UUID, begin: Option[Long], end: Option[Long], limit: Int, latest: Boolean): Seq[Measurement]
+}
+
+trait MeasurementValueSink {
+ def declare(points: Seq[UUID])
+ def put(entries: Seq[(UUID, Measurement)])
+}
+
+trait MeasurementValueStore extends MeasurementValueSource with MeasurementValueSink
diff --git a/mstore-sql/src/main/scala/io/greenbus/mstore/sql/MeasurementStoreSchema.scala b/mstore-sql/src/main/scala/io/greenbus/mstore/sql/MeasurementStoreSchema.scala
new file mode 100644
index 0000000..a1aa557
--- /dev/null
+++ b/mstore-sql/src/main/scala/io/greenbus/mstore/sql/MeasurementStoreSchema.scala
@@ -0,0 +1,37 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.mstore.sql
+
+import org.squeryl.{ KeyedEntity, Schema }
+import java.util.UUID
+import org.squeryl.PrimitiveTypeMode._
+
+case class CurrentValueRow(id: UUID, bytes: Option[Array[Byte]]) extends KeyedEntity[UUID]
+case class HistoricalValueRow(pointId: UUID, time: Long, bytes: Array[Byte])
+
+object MeasurementStoreSchema extends Schema {
+
+ val currentValues = table[CurrentValueRow]
+
+ val historicalValues = table[HistoricalValueRow]
+
+ on(historicalValues)(s => declare(
+ columns(s.pointId.~, s.time) are (indexed)))
+
+}
diff --git a/mstore-sql/src/main/scala/io/greenbus/mstore/sql/SqlMeasurementStore.scala b/mstore-sql/src/main/scala/io/greenbus/mstore/sql/SqlMeasurementStore.scala
new file mode 100644
index 0000000..58b9fa6
--- /dev/null
+++ b/mstore-sql/src/main/scala/io/greenbus/mstore/sql/SqlMeasurementStore.scala
@@ -0,0 +1,151 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.mstore.sql
+
+import java.util.UUID
+import io.greenbus.client.service.proto.Measurements.Measurement
+import io.greenbus.sql.DbConnection
+import org.squeryl.PrimitiveTypeMode._
+import MeasurementStoreSchema._
+import io.greenbus.mstore.{ MeasurementHistorySource, MeasurementValueStore, MeasurementValueSource }
+
+trait MeasurementStore {
+ def get(ids: Seq[UUID]): Seq[(UUID, Measurement)]
+ def put(entries: Seq[(UUID, Measurement)])
+}
+
+class SqlCurrentValueAndHistorian(sql: DbConnection) extends MeasurementValueStore with MeasurementHistorySource {
+
+ def declare(points: Seq[UUID]) {
+ sql.inTransaction {
+ CurrentValueOperations.declare(points)
+ }
+ }
+
+ def put(entries: Seq[(UUID, Measurement)]) {
+
+ val withTime: Seq[(UUID, Long, Array[Byte])] =
+ entries.map {
+ case (id, meas) =>
+ val time = if (meas.hasTime) meas.getTime else System.currentTimeMillis()
+ (id, time, meas.toByteArray)
+ }
+
+ val withoutTime = withTime.map(tup => (tup._1, tup._3))
+
+ sql.inTransaction {
+ CurrentValueOperations.put(withoutTime)
+ HistoricalValueOperations.put(withTime)
+ }
+ }
+
+ def get(ids: Seq[UUID]): Seq[(UUID, Measurement)] = {
+ sql.inTransaction {
+ CurrentValueOperations.get(ids)
+ }
+ }
+
+ def getHistory(id: UUID, begin: Option[Long], end: Option[Long], limit: Int, latest: Boolean): Seq[Measurement] = {
+ sql.inTransaction {
+ HistoricalValueOperations.getHistory(id, begin, end, limit, latest)
+ }
+ }
+}
+
+// Used in services that already set up a squeryl transaction
+object SimpleInTransactionCurrentValueStore extends MeasurementValueSource {
+ def get(ids: Seq[UUID]): Seq[(UUID, Measurement)] = {
+ CurrentValueOperations.get(ids)
+ }
+}
+object SimpleInTransactionHistoryStore extends MeasurementHistorySource {
+ def getHistory(id: UUID, begin: Option[Long], end: Option[Long], limit: Int, latest: Boolean): Seq[Measurement] = {
+ HistoricalValueOperations.getHistory(id, begin, end, limit, latest)
+ }
+}
+
+object CurrentValueOperations {
+
+ def declare(points: Seq[UUID]) {
+ val distinctPoints = points.distinct
+ val existing =
+ from(currentValues)(v =>
+ where(v.id in distinctPoints)
+ select (v.id)).toSet
+
+ val inserts = distinctPoints.filterNot(existing.contains).map(CurrentValueRow(_, None))
+ currentValues.insert(inserts)
+ }
+
+ def put(entries: Seq[(UUID, Array[Byte])]) {
+ val rows = entries.map {
+ case (id, bytes) => CurrentValueRow(id, Some(bytes))
+ }
+
+ currentValues.forceUpdate(rows)
+ }
+
+ def get(ids: Seq[UUID]): Seq[(UUID, Measurement)] = {
+ val results =
+ from(currentValues)(cv =>
+ where((cv.id in ids) and (cv.bytes isNotNull))
+ select (cv.id, cv.bytes)).toSeq
+
+ results.map {
+ case (id, bytes) => (id, Measurement.parseFrom(bytes.get))
+ }
+ }
+}
+
+object HistoricalValueOperations {
+
+ def put(entries: Seq[(UUID, Long, Array[Byte])]) {
+ val rows = entries.map {
+ case (id, time, bytes) => HistoricalValueRow(id, time, bytes)
+ }
+
+ historicalValues.insert(rows)
+ }
+
+ def getHistory(id: UUID, begin: Option[Long], end: Option[Long], limit: Int, latest: Boolean): Seq[Measurement] = {
+
+ import org.squeryl.dsl.ast.{ OrderByArg, ExpressionNode }
+ def timeOrder(time: ExpressionNode) = {
+ if (!latest) {
+ new OrderByArg(time).asc
+ } else {
+ new OrderByArg(time).desc
+ }
+ }
+
+ // Switch ordering to get either beginning of the window or end of the window
+ val bytes: Seq[Array[Byte]] =
+ from(historicalValues)(hv =>
+ where(hv.pointId === id and
+ (hv.time gt begin.?) and
+ (hv.time lte end.?))
+ select (hv.bytes)
+ orderBy (timeOrder(hv.time))).page(0, limit).toSeq
+
+ val inOrder = if (!latest) bytes else bytes.reverse
+
+ inOrder.map(Measurement.parseFrom)
+ }
+}
+
diff --git a/mstore-sql/src/test/scala/io/greenbus/mstore/sql/SqlCurrentValueAndHistorianTest.scala b/mstore-sql/src/test/scala/io/greenbus/mstore/sql/SqlCurrentValueAndHistorianTest.scala
new file mode 100644
index 0000000..b7aa30c
--- /dev/null
+++ b/mstore-sql/src/test/scala/io/greenbus/mstore/sql/SqlCurrentValueAndHistorianTest.scala
@@ -0,0 +1,195 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.mstore.sql
+
+import io.greenbus.sql.test.{ RunTestsInsideTransaction, DatabaseUsingTestBase }
+import java.util.UUID
+import io.greenbus.client.service.proto.Measurements.{ Quality, Measurement }
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+@RunWith(classOf[JUnitRunner])
+class SqlCurrentValueAndHistorianTest extends DatabaseUsingTestBase with RunTestsInsideTransaction {
+
+ def schemas = List(MeasurementStoreSchema)
+
+ def buildMeas(v: Int, time: Long): Measurement = {
+ Measurement.newBuilder
+ .setType(Measurement.Type.INT)
+ .setIntVal(v)
+ .setTime(time)
+ .setQuality(Quality.newBuilder)
+ .build
+ }
+
+ def simplify(m: Measurement): (Long, Long) = {
+ (m.getIntVal, m.getTime)
+ }
+
+ def check(correct: Set[(UUID, (Long, Long))])(op: => Seq[(UUID, Measurement)]) {
+ val results = op
+ results.size should equal(correct.size)
+ results.map(tup => (tup._1, simplify(tup._2))).toSet should equal(correct)
+ }
+
+ def checkHist(correct: Seq[(Long, Long)])(op: => Seq[Measurement]) {
+ val results = op
+ results.size should equal(correct.size)
+ results.map(simplify).toList should equal(correct)
+ }
+
+ test("Current value") {
+
+ val mstore = new SqlCurrentValueAndHistorian(dbConnection)
+
+ val point01 = UUID.randomUUID()
+ val point02 = UUID.randomUUID()
+
+ mstore.declare(Seq(point01, point02))
+
+ mstore.get(Seq(point01)).size should equal(0)
+
+ mstore.put(Seq((point01, buildMeas(1, 44))))
+
+ check(Set((point01, (1, 44)))) {
+ mstore.get(Seq(point01))
+ }
+
+ mstore.put(Seq((point02, buildMeas(2, 55))))
+
+ check(Set((point02, (2, 55)))) {
+ mstore.get(Seq(point02))
+ }
+ check(Set((point01, (1, 44)), (point02, (2, 55)))) {
+ mstore.get(Seq(point01, point02))
+ }
+
+ mstore.put(Seq((point01, buildMeas(3, 33)), (point02, buildMeas(4, 66))))
+
+ check(Set((point01, (3, 33)), (point02, (4, 66)))) {
+ mstore.get(Seq(point01, point02))
+ }
+ }
+
+ test("Historical values") {
+
+ val mstore = new SqlCurrentValueAndHistorian(dbConnection)
+
+ val point01 = UUID.randomUUID()
+ val point02 = UUID.randomUUID()
+
+ mstore.getHistory(point01, None, None, 100, false).size should equal(0)
+
+ mstore.put(Seq((point01, buildMeas(100, 1))))
+
+ checkHist(Seq((100, 1))) {
+ mstore.getHistory(point01, None, None, 100, false)
+ }
+
+ mstore.put(Seq((point01, buildMeas(101, 2))))
+ mstore.put(Seq((point02, buildMeas(200, 1))))
+ mstore.put(Seq((point01, buildMeas(102, 3))))
+ mstore.put(Seq((point02, buildMeas(201, 2))))
+ mstore.put(Seq((point01, buildMeas(104, 4))))
+
+ checkHist(Seq((100, 1), (101, 2), (102, 3), (104, 4))) {
+ mstore.getHistory(point01, None, None, 100, false)
+ }
+ checkHist(Seq((200, 1), (201, 2))) {
+ mstore.getHistory(point02, None, None, 100, false)
+ }
+ }
+
+ test("Historical paging") {
+
+ val mstore = new SqlCurrentValueAndHistorian(dbConnection)
+
+ val point01 = UUID.randomUUID()
+
+ mstore.put(Seq((point01, buildMeas(100, 1))))
+ mstore.put(Seq((point01, buildMeas(101, 2))))
+ mstore.put(Seq((point01, buildMeas(102, 3))))
+ mstore.put(Seq((point01, buildMeas(103, 4))))
+ mstore.put(Seq((point01, buildMeas(104, 5))))
+ mstore.put(Seq((point01, buildMeas(105, 6))))
+
+ checkHist(Seq((100, 1), (101, 2))) {
+ mstore.getHistory(point01, None, None, 2, false)
+ }
+ checkHist(Seq((102, 3), (103, 4))) {
+ mstore.getHistory(point01, Some(2), None, 2, false)
+ }
+ checkHist(Seq((104, 5), (105, 6))) {
+ mstore.getHistory(point01, Some(4), None, 2, false)
+ }
+ }
+
+ test("Historical paging backwards") {
+
+ val mstore = new SqlCurrentValueAndHistorian(dbConnection)
+
+ val point01 = UUID.randomUUID()
+
+ mstore.put(Seq((point01, buildMeas(100, 1))))
+ mstore.put(Seq((point01, buildMeas(101, 2))))
+ mstore.put(Seq((point01, buildMeas(102, 3))))
+ mstore.put(Seq((point01, buildMeas(103, 4))))
+ mstore.put(Seq((point01, buildMeas(104, 5))))
+ mstore.put(Seq((point01, buildMeas(105, 6))))
+
+ // window end is >=, otherwise we lose when multi per ms
+ checkHist(Seq((104, 5), (105, 6))) {
+ mstore.getHistory(point01, None, None, 2, true)
+ }
+ checkHist(Seq((102, 3), (103, 4), (104, 5))) {
+ mstore.getHistory(point01, None, Some(5), 3, true)
+ }
+ checkHist(Seq((100, 1), (101, 2), (102, 3))) {
+ mstore.getHistory(point01, None, Some(3), 3, true)
+ }
+ }
+
+ test("Historical limit justification") {
+
+ val mstore = new SqlCurrentValueAndHistorian(dbConnection)
+
+ val point01 = UUID.randomUUID()
+
+ mstore.put(Seq((point01, buildMeas(100, 1))))
+ mstore.put(Seq((point01, buildMeas(101, 2))))
+ mstore.put(Seq((point01, buildMeas(102, 3))))
+ mstore.put(Seq((point01, buildMeas(103, 4))))
+ mstore.put(Seq((point01, buildMeas(104, 5))))
+ mstore.put(Seq((point01, buildMeas(105, 6))))
+
+ checkHist(Seq((100, 1), (101, 2))) {
+ mstore.getHistory(point01, None, None, 2, false)
+ }
+ checkHist(Seq((104, 5), (105, 6))) {
+ mstore.getHistory(point01, None, None, 2, true)
+ }
+ checkHist(Seq((102, 3), (103, 4))) {
+ mstore.getHistory(point01, Some(2), Some(5), 2, false)
+ }
+ checkHist(Seq((103, 4), (104, 5))) {
+ mstore.getHistory(point01, Some(2), Some(5), 2, true)
+ }
+ }
+
+}
diff --git a/pom.xml b/pom.xml
new file mode 100755
index 0000000..2f129b5
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,310 @@
+
+
+
+ 4.0.0
+ io.greenbus
+ greenbus-parent
+ pom
+ 3.0.0
+
+
+ UTF-8
+ 2.10.1
+ 2.10
+ 3.1.0
+ 1.7.2
+ 1.0.1
+ 1.9.1
+ 0.1.4
+ 1.1
+ 2.2
+ 1.9.0
+ 2.3
+ 2.8.1
+ 0.1.11
+ 0.20
+
+ 3.0.0
+ 0.9.5-6
+ 2.2
+ 1.4
+ 1.5.4
+ 1.5
+ 2.2.2
+ 3.1.0
+ 1.8.5
+ 0.9.27
+
+ 1.1
+ 1.3
+ 2.2
+ 9.0-801.jdbc4
+ 2.0.4-OSGI
+
+
+ ../
+
+
+
+ scala-base
+ util
+ client
+ app-framework
+ loading
+ loader-xml
+ cli
+ simulator
+ calculation
+ sql
+ mstore-sql
+ processing
+ services
+ integration
+
+
+
+
+ totalgrid-release
+ https://repo.totalgrid.org-releases
+ https://repo.totalgrid.org/artifactory/totalgrid-release
+
+ false
+
+
+
+
+ totalgrid-snapshot
+ https://repo.totalgrid.org-snapshots
+ https://repo.totalgrid.org/artifactory/totalgrid-snapshot
+
+ true
+
+
+
+
+ third-party-release
+ https://repo.totalgrid.org-releases
+ https://repo.totalgrid.org/artifactory/third-party-release
+
+ false
+
+
+
+
+ third-party-snapshot
+ https://repo.totalgrid.org-snapshots
+ https://repo.totalgrid.org/artifactory/third-party-snapshot
+
+ true
+
+
+
+
+ scala-tools-releases
+ Scala-tools Maven2 Repository
+ http://scala-tools.org/repo-releases
+
+ false
+
+
+
+
+ Akka
+ Akka Maven2 Repository
+ http://akka.io/repository/
+
+ false
+
+
+
+
+ maven2-repository.dev.java.net
+ Java.net Repository for Maven
+ http://download.java.net/maven/2/
+ default
+
+
+
+
+
+ totalgrid-release
+ https://repo.totalgrid.org-releases
+ https://repo.totalgrid.org/artifactory/totalgrid-release
+
+ false
+
+
+
+ totalgrid-snapshot
+ https://repo.totalgrid.org-snapshot
+ https://repo.totalgrid.org/artifactory/totalgrid-snapshot
+
+ true
+
+
+
+
+ third-party-release
+ https://repo.totalgrid.org-releases
+ https://repo.totalgrid.org/artifactory/third-party-release
+
+ false
+
+
+
+
+ scala-tools-releases
+ http://scala-tools.org/repo-releases
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ ${maven-javadoc-plugin.version}
+
+
+
+
+
+
+
+ org.codehaus.mojo
+ cobertura-maven-plugin
+ 2.4
+
+ xml
+
+
+
+
+ com.mycila.maven-license-plugin
+ maven-license-plugin
+ ${maven-license-plugin.version}
+
+ true
+ true
+ true
+
+
+
+ check-headers
+ process-sources
+
+ format
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.7.1
+
+
+
+ **/*.class
+
+ brief
+ false
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.1.2
+
+
+
+ attach-sources
+
+ jar
+
+
+
+
+
+
+
+
+ com.googlecode.maven-java-formatter-plugin
+ maven-java-formatter-plugin
+ 0.3.1
+
+
+
+
+ format
+
+
+
+
+
+
+ **/*.java
+ **/*.xml
+
+ ${main.basedir}/CodeFormat.xml
+ LF
+
+
+
+
+
+
+
+ slf4j-simple
+
+
+ org.slf4j
+ slf4j-simple
+ ${slf4j-api.version}
+
+
+
+
+ javadoc
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ ${maven-javadoc-plugin.version}
+
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+
+
+
+
+
+
+
+ junit
+ junit
+ 4.8.2
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ ${slf4j-api.version}
+ test
+
+
+
+
diff --git a/processing/pom.xml b/processing/pom.xml
new file mode 100755
index 0000000..4170472
--- /dev/null
+++ b/processing/pom.xml
@@ -0,0 +1,74 @@
+
+ 4.0.0
+
+
+ io.greenbus
+ greenbus-scala-base
+ 3.0.0
+ ../scala-base
+
+
+ greenbus-processing
+ jar
+
+
+
+ AGPLv3
+ http://www.gnu.org/licenses/agpl-3.0.txt
+
+
+
+
+
+
+
+ com.mycila.maven-license-plugin
+ maven-license-plugin
+
+
+
+
+
+
+
+
+
+ io.greenbus
+ greenbus-client
+ 3.0.0
+
+
+ io.greenbus
+ greenbus-services
+ 3.0.0
+
+
+ io.greenbus
+ greenbus-mstore-sql
+ 3.0.0
+
+
+ io.greenbus
+ greenbus-app-framework
+ 3.0.0
+
+
+ io.greenbus.msg
+ greenbus-msg-qpid
+ 1.0.0
+
+
+ com.typesafe.akka
+ akka-actor_2.10
+ 2.2.0
+
+
+ com.typesafe.akka
+ akka-slf4j_2.10
+ 2.2.0
+
+
+
+
+
diff --git a/processing/src/main/scala/io/greenbus/measproc/ActorForwardingServiceHandler.scala b/processing/src/main/scala/io/greenbus/measproc/ActorForwardingServiceHandler.scala
new file mode 100644
index 0000000..bc8dc8b
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/ActorForwardingServiceHandler.scala
@@ -0,0 +1,33 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc
+
+import akka.actor.ActorRef
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.msg.service.ServiceHandler
+import io.greenbus.measproc.ActorForwardingServiceHandler.TypeFactory
+
+object ActorForwardingServiceHandler {
+ type TypeFactory[A] = (Array[Byte], (Array[Byte]) => Unit) => A
+}
+class ActorForwardingServiceHandler[A](ref: ActorRef, factory: TypeFactory[A]) extends ServiceHandler with Logging {
+ def handleMessage(msg: Array[Byte], responseHandler: (Array[Byte]) => Unit) {
+ ref ! factory(msg, responseHandler)
+ }
+}
diff --git a/processing/src/main/scala/io/greenbus/measproc/EndpointProcessor.scala b/processing/src/main/scala/io/greenbus/measproc/EndpointProcessor.scala
new file mode 100644
index 0000000..87ebc7a
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/EndpointProcessor.scala
@@ -0,0 +1,171 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc
+
+import java.util.UUID
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.client.service.proto.EventRequests.EventTemplate
+import io.greenbus.client.service.proto.Measurements.Measurement
+import io.greenbus.client.service.proto.Model.{ Point, ModelUUID }
+import io.greenbus.client.service.proto.Processing.{ MeasOverride, TriggerSet }
+import io.greenbus.jmx.MetricsManager
+import io.greenbus.measproc.pipeline.ProcessingPipeline
+import io.greenbus.measproc.processing.MapCache
+import io.greenbus.mstore.MeasurementValueStore
+
+class EndpointProcessor(endpointName: String,
+ points: Seq[(ModelUUID, String)],
+ overrides: Seq[MeasOverride],
+ triggerSets: Seq[(ModelUUID, TriggerSet)],
+ store: MeasurementValueStore,
+ notifications: Seq[(ModelUUID, String, Measurement)] => Unit,
+ publishEvents: Seq[EventTemplate.Builder] => Unit) extends Logging {
+
+ private val endpointMetrics = new EndpointMetrics(endpointName)
+ import endpointMetrics._
+
+ private var pointMap: PointMap = new PointMap(points)
+
+ private val service = new ProcessorService(endpointName, metrics)
+
+ private val pipeline = new ProcessingPipeline(
+ endpointName,
+ points.map(_._1.getValue),
+ postMeasurements,
+ getMeasurement,
+ publishEvents,
+ new MapCache[Boolean],
+ new MapCache[(Option[Measurement], Option[Measurement])])
+
+ overrides.foreach(pipeline.addOverride)
+ triggerSets.foreach { case (uuid, ts) => pipeline.addTriggerSet(uuid, ts) }
+ store.declare(points.map(_._1).map(ru => UUID.fromString(ru.getValue)))
+
+ private def postMeasurements(mlist: Seq[(String, Measurement)]) {
+ logger.trace("Posting measurements: " + mlist)
+ val withIds = mlist map { case (key, m) => (UUID.fromString(key), m) }
+
+ val count = mlist.size
+ putSize(count)
+
+ storePuts(1)
+ storePutTime {
+ store.put(withIds)
+ }
+
+ val measWithIds = mlist.flatMap {
+ case (key, m) =>
+ val modelId = ModelUUID.newBuilder().setValue(key).build()
+ pointMap.idToName.get(key).map { name =>
+ (modelId, name, m)
+ }
+ }
+
+ notifications(measWithIds)
+ }
+ private def getMeasurement(key: String): Option[Measurement] = {
+ currentValueGets(1)
+ currentValueGetTime {
+ store.get(Seq(UUID.fromString(key))).headOption.map(_._2)
+ }
+ }
+
+ def modifyLastValues(measurements: Seq[(String, Measurement)]): Unit = {
+ pipeline.modifyLastValues(measurements)
+ }
+
+ def handle(msg: Array[Byte], response: Array[Byte] => Unit): Unit = {
+ service.handle(msg, response, pipeline.process, pointMap)
+ }
+
+ def pointSet: Seq[UUID] = {
+ pointMap.all.map(tup => UUID.fromString(tup._1.getValue))
+ }
+
+ def pointAdded(point: (ModelUUID, String), measOverride: Option[MeasOverride], triggerSet: Option[TriggerSet]): Unit = {
+ val pointList = pointMap.all :+ point
+ store.declare(List(UUID.fromString(point._1.getValue)))
+ pointMap = new PointMap(pointList)
+ pipeline.updatePointsList(pointList.map(_._1.getValue))
+ measOverride.foreach(pipeline.addOverride)
+ triggerSet.foreach(t => pipeline.addTriggerSet(point._1, t))
+
+ logger.debug(s"Endpoint $endpointName points updated: " + pointMap.nameToId.keySet.size)
+ }
+
+ def pointRemoved(point: ModelUUID): Unit = {
+ val pointList = pointMap.all.filterNot { case (uuid, _) => uuid == point }
+
+ pointMap = new PointMap(pointList)
+ pipeline.updatePointsList(pointList.map(_._1.getValue))
+ pipeline.removeOverride(point)
+ pipeline.removeTriggerSet(point)
+ }
+
+ def pointModified(point: Point): Unit = {
+ val pointTups = pointMap.all.filterNot { case (uuid, _) => uuid == point.getUuid } ++ Seq((point.getUuid, point.getName))
+ pointMap = new PointMap(pointTups)
+ }
+
+ def overridePut(v: MeasOverride): Unit = {
+ logger.debug(s"Endpoint $endpointName override added: " + pointMap.idToName.get(v.getPointUuid.getValue))
+ pipeline.addOverride(v)
+ }
+ def overrideDeleted(v: MeasOverride): Unit = {
+ logger.debug(s"Endpoint $endpointName override removed: " + pointMap.idToName.get(v.getPointUuid.getValue))
+ pipeline.removeOverride(v)
+ }
+
+ def triggerSetPut(uuid: ModelUUID, v: TriggerSet): Unit = {
+ logger.debug(s"Endpoint $endpointName trigger set added: " + pointMap.idToName.get(uuid.getValue))
+ pipeline.addTriggerSet(uuid, v)
+ }
+
+ def triggerSetDeleted(uuid: ModelUUID): Unit = {
+ logger.debug(s"Endpoint $endpointName trigger set removed: " + pointMap.idToName.get(uuid.getValue))
+ pipeline.removeTriggerSet(uuid)
+ }
+
+ def register() {
+ metricsMgr.register()
+ }
+
+ def unregister() {
+ metricsMgr.unregister()
+ }
+}
+
+class EndpointMetrics(endpointName: String) {
+ val metricsMgr = MetricsManager("io.greenbus.measproc." + endpointName)
+ val metrics = metricsMgr.metrics("Processor")
+
+ val currentValueGets = metrics.counter("CurrentValueGets")
+ val currentValueGetTime = metrics.timer("CurrentValueGetTime")
+ val storePuts = metrics.counter("StorePuts")
+ val storePutTime = metrics.timer("StorePutTime")
+ val putSize = metrics.average("StorePutSize")
+}
+
+class PointMap(points: Seq[(ModelUUID, String)]) {
+ val nameToId: Map[String, ModelUUID] = points.map(tup => (tup._2, tup._1)).toMap
+ val idToName: Map[String, String] = points.map(tup => (tup._1.getValue, tup._2)).toMap
+
+ def all: Seq[(ModelUUID, String)] = points
+}
\ No newline at end of file
diff --git a/processing/src/main/scala/io/greenbus/measproc/EndpointStreamBootstrapper.scala b/processing/src/main/scala/io/greenbus/measproc/EndpointStreamBootstrapper.scala
new file mode 100644
index 0000000..c1ccf55
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/EndpointStreamBootstrapper.scala
@@ -0,0 +1,90 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc
+
+import akka.actor._
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.msg.Session
+import io.greenbus.app.actor.MessageScheduling
+import io.greenbus.client.service.proto.Model.Endpoint
+import io.greenbus.measproc.EndpointStreamBootstrapper.ChildFactory
+
+import scala.concurrent.duration._
+
+object EndpointStreamBootstrapper {
+ val retryPeriod = Duration(3000, MILLISECONDS)
+
+ type ChildFactory = (Endpoint, EndpointConfiguration) => Props
+
+ private case object DoLookup
+ private case class GotConfiguration(config: EndpointConfiguration)
+ private case class ConfigurationFailure(ex: Throwable)
+
+ def props(endpoint: Endpoint, session: Session, factory: ChildFactory): Props = {
+ Props(classOf[EndpointStreamBootstrapper], endpoint, session, factory)
+ }
+}
+class EndpointStreamBootstrapper(endpoint: Endpoint, session: Session, factory: ChildFactory) extends Actor with MessageScheduling with Logging {
+ import context.dispatcher
+ import io.greenbus.measproc.EndpointStreamBootstrapper._
+ import io.greenbus.measproc.ServiceConfiguration._
+
+ // Restart us if the child restarts, otherwise the restarted child would be with the old data
+ override def supervisorStrategy: SupervisorStrategy = {
+ import akka.actor.SupervisorStrategy._
+ OneForOneStrategy() {
+ case _: Throwable => Escalate
+ }
+ }
+
+ private var child = Option.empty[ActorRef]
+
+ self ! DoLookup
+
+ def receive = {
+
+ case DoLookup => {
+ logger.info(s"Requesting configuration for endpoint ${endpoint.getName}")
+ val configRequest = configForEndpoint(session, endpoint.getUuid)
+
+ configRequest.onSuccess {
+ case config => self ! GotConfiguration(config)
+ }
+ configRequest.onFailure {
+ case ex: Throwable => self ! ConfigurationFailure(ex)
+ }
+ }
+
+ case GotConfiguration(config) => {
+ child = Some(context.actorOf(factory(endpoint, config)))
+ }
+
+ case ConfigurationFailure(exception) => {
+ logger.error(s"Couldn't get configuration for endpoint ${endpoint.getName}: ${exception.getMessage}")
+ scheduleRetry()
+ }
+ }
+
+ private def scheduleRetry() {
+ context.system.scheduler.scheduleOnce(
+ retryPeriod,
+ self,
+ DoLookup)
+ }
+}
diff --git a/processing/src/main/scala/io/greenbus/measproc/EndpointStreamManager.scala b/processing/src/main/scala/io/greenbus/measproc/EndpointStreamManager.scala
new file mode 100644
index 0000000..fe0eb98
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/EndpointStreamManager.scala
@@ -0,0 +1,798 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc
+
+import java.util.UUID
+
+import akka.actor._
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.msg.amqp.{ AmqpMessage, AmqpAddressedMessage, AmqpServiceOperations }
+import io.greenbus.msg.{ Session, SessionUnusableException, SubscriptionBinding }
+import io.greenbus.app.actor.MessageScheduling
+import io.greenbus.client.exception.UnauthorizedException
+import io.greenbus.client.service.FrontEndService
+import io.greenbus.client.service.proto.EventRequests.EventTemplate
+import io.greenbus.client.service.proto.FrontEnd.FrontEndConnectionStatus
+import io.greenbus.client.service.proto.Measurements.Quality.Validity
+import io.greenbus.client.service.proto.Measurements._
+import io.greenbus.client.service.proto.Model.{ Point, Endpoint, ModelUUID }
+import io.greenbus.client.service.proto.Processing.{ TriggerSet, MeasOverride }
+import io.greenbus.measproc.EventPublisher.EventTemplateGroup
+import io.greenbus.measproc.lock.ProcessingLockService
+import io.greenbus.mstore.MeasurementValueStore
+import io.greenbus.services.authz.AuthLookup
+import io.greenbus.services.framework.ModelNotifier
+import io.greenbus.services.model.{ FrontEndModel, UUIDHelpers }
+import io.greenbus.sql.DbConnection
+
+import scala.collection.JavaConversions._
+
+import scala.concurrent.duration._
+
+object EndpointStreamManager extends Logging {
+
+ def mergeableQualityElements(meas: Measurement): (Quality.Builder, DetailQual.Builder) = {
+ if (meas.hasQuality) {
+ val detailBuilder = if (meas.getQuality.hasDetailQual) {
+ meas.getQuality.getDetailQual.toBuilder
+ } else {
+ DetailQual.newBuilder()
+ }
+
+ (meas.getQuality.toBuilder, detailBuilder)
+ } else {
+ (Quality.newBuilder(), DetailQual.newBuilder())
+ }
+ }
+
+ sealed trait ConfigUpdate
+ case class PointAdded(point: (ModelUUID, String), measOverride: Option[MeasOverride], triggerSet: Option[TriggerSet]) extends ConfigUpdate
+ case class PointRemoved(point: ModelUUID) extends ConfigUpdate
+ case class PossiblePointNotification(uuid: ModelUUID) extends ConfigUpdate
+ case class PointMiss(uuid: ModelUUID) extends ConfigUpdate
+ case class PointModified(point: Point) extends ConfigUpdate
+
+ case class OverridePut(v: MeasOverride) extends ConfigUpdate
+ case class OverrideDeleted(v: MeasOverride) extends ConfigUpdate
+ case class TriggerSetPut(uuid: ModelUUID, v: TriggerSet) extends ConfigUpdate
+ case class TriggerSetDeleted(uuid: ModelUUID) extends ConfigUpdate
+
+ case object HeartbeatCheck
+ case object MarkRetry
+ case class RegistrationRequest(msg: Array[Byte], responseHandler: (Array[Byte]) => Unit)
+ case class MeasBatchRequest(msg: Array[Byte], responseHandler: (Array[Byte]) => Unit, bindingSequence: Long)
+ case class HeartbeatRequest(msg: Array[Byte], responseHandler: (Array[Byte]) => Unit, bindingSequence: Long)
+
+ case class StreamRegistered(endpointId: ModelUUID, endpointName: String, address: String)
+
+ case object DoStartup
+ case object DoShutdown
+ case object CheckLock
+
+ sealed trait State
+ case object Down extends State
+ case object Standby extends State
+ case object Available extends State
+ case object LinkUp extends State
+ case object LinkHalfDown extends State
+ case object LinkHalfDownMarkFailed extends State
+
+ sealed trait Data
+ case object NoData extends Data
+ case class StandbyData(processing: ProcessorComponents, config: ConfigComponents) extends Data
+ case class AvailableData(regServiceBinding: SubscriptionBinding, processing: ProcessorComponents, config: ConfigComponents) extends Data
+ case class LinkUpData(lastHeartbeat: Long, nodeId: Option[String], binding: ServiceBindingComponents, availData: AvailableData) extends Data
+ case class LinkHalfDownData(previousValues: Seq[(UUID, Measurement)], nodeId: Option[String], binding: ServiceBindingComponents, availData: AvailableData) extends Data
+
+ case class ServiceBindingComponents(measBinding: SubscriptionBinding, heartbeatBinding: SubscriptionBinding, bindingSeq: Long)
+ case class ConfigComponents(subscriptions: Seq[SubscriptionBinding], pointSet: Seq[(ModelUUID, String)], outstandingPoints: Set[ModelUUID])
+ case class ProcessorComponents(processor: EndpointProcessor, eventPublisher: ActorRef)
+
+ case class StreamResources(session: Session, serviceOps: AmqpServiceOperations, store: MeasurementValueStore)
+
+ def props(endpoint: Endpoint,
+ resources: StreamResources,
+ config: EndpointConfiguration,
+ sql: DbConnection,
+ auth: AuthLookup,
+ frontEndModel: FrontEndModel,
+ statusNotifier: ModelNotifier,
+ standbyLock: ProcessingLockService,
+ nodeId: String,
+ heartbeatTimeout: Long,
+ markRetryPeriodMs: Long,
+ standbyLockRetryPeriodMs: Long,
+ standbyLockExpiryDurationMs: Long): Props = {
+
+ Props(
+ classOf[EndpointStreamManager],
+ endpoint,
+ resources,
+ config,
+ sql,
+ auth,
+ frontEndModel,
+ statusNotifier,
+ standbyLock,
+ nodeId: String,
+ heartbeatTimeout,
+ markRetryPeriodMs,
+ standbyLockRetryPeriodMs,
+ standbyLockExpiryDurationMs)
+ }
+
+ def measNotificationBatch(serviceOps: AmqpServiceOperations, endpoint: Endpoint, meases: Seq[(ModelUUID, String, Measurement)]) {
+ if (meases.nonEmpty) {
+ val exchange = classOf[MeasurementNotification].getSimpleName
+
+ val measNotifications = meases.map {
+ case (uuid, name, m) =>
+ val mn = MeasurementNotification.newBuilder()
+ .setPointUuid(uuid)
+ .setPointName(name)
+ .setValue(m)
+ .build()
+ (uuid, name, mn)
+ }
+
+ val messages = measNotifications.map {
+ case (uuid, name, mn) =>
+ AmqpAddressedMessage(exchange, uuid.getValue + "." + name.replace('.', '_'), AmqpMessage(mn.toByteArray, None, None))
+ }
+
+ serviceOps.publishBatch(messages)
+
+ val batchNotification = MeasurementBatchNotification.newBuilder()
+ .setEndpointUuid(endpoint.getUuid)
+ .addAllValues(measNotifications.map(_._3))
+ .build()
+
+ serviceOps.publishEvent(classOf[MeasurementBatchNotification].getSimpleName, batchNotification.toByteArray, endpoint.getUuid.getValue)
+ }
+ }
+}
+
+import io.greenbus.measproc.EndpointStreamManager._
+class EndpointStreamManager(
+ endpoint: Endpoint,
+ resources: StreamResources,
+ config: EndpointConfiguration,
+ sql: DbConnection,
+ auth: AuthLookup,
+ frontEndModel: FrontEndModel,
+ statusNotifier: ModelNotifier,
+ standbyLock: ProcessingLockService,
+ nodeId: String,
+ heartbeatTimeout: Long,
+ markRetryPeriodMs: Long,
+ standbyLockRetryPeriodMs: Long,
+ standbyLockExpiryDurationMs: Long)
+ extends Actor with FSM[State, Data] with MessageScheduling with Logging {
+
+ import context.dispatcher
+
+ override def supervisorStrategy: SupervisorStrategy = {
+ import akka.actor.SupervisorStrategy._
+ OneForOneStrategy(maxNrOfRetries = 3, withinTimeRange = 30.seconds) {
+ case _: UnauthorizedException => Escalate
+ case _: SessionUnusableException => Escalate
+ case _: Throwable => Escalate
+ }
+ }
+
+ startWith(Down, NoData)
+ self ! DoStartup
+
+ when(Down) {
+ case Event(DoStartup, NoData) => {
+ logger.debug(s"Starting processor for ${endpoint.getName}")
+ val (processor, eventPublisher) = setupProcessor(config)
+
+ self ! CheckLock
+ goto(Standby) using StandbyData(
+ processing = ProcessorComponents(processor, eventPublisher),
+ config = ConfigComponents(Seq(config.overrideSubscription, config.triggerSetSubscription), config.points, Set.empty[ModelUUID]))
+ }
+ }
+
+ when(Standby) {
+ case Event(CheckLock, data: StandbyData) => {
+ try {
+ val haveLock = standbyLock.acquireOrHold(UUIDHelpers.protoUUIDToUuid(endpoint.getUuid), nodeId, standbyLockExpiryDurationMs)
+
+ if (haveLock) {
+ val registrationBinding = setupRegistrationService()
+
+ markEndpointOffline(pointSetUuidConversion(data.config.pointSet), data.processing.processor)
+
+ scheduleMsg(standbyLockRetryPeriodMs, CheckLock)
+ goto(Available) using AvailableData(
+ regServiceBinding = registrationBinding,
+ processing = data.processing,
+ config = data.config)
+ } else {
+ scheduleMsg(standbyLockRetryPeriodMs, CheckLock)
+ stay using data
+ }
+ } catch {
+ case ex: Throwable =>
+ logger.error(s"Endpoint ${endpoint.getName} had error acquiring lock: " + ex)
+ scheduleMsg(standbyLockRetryPeriodMs, CheckLock)
+ stay using data
+ }
+ }
+ case Event(configUpdate: ConfigUpdate, data: AvailableData) => {
+ handleConfigUpdate(configUpdate, data.config, data.processing.processor)
+ stay using data
+ }
+ }
+
+ when(Available) {
+ case Event(configUpdate: ConfigUpdate, data: AvailableData) => {
+ handleConfigUpdate(configUpdate, data.config, data.processing.processor)
+ stay using data
+ }
+ case Event(RegistrationRequest(msgBytes, respFunc), data: AvailableData) => {
+
+ val nextBindingSeq = 0
+ handleRegistrationAttemptWhileDown(msgBytes, respFunc, nextBindingSeq) match {
+ case None =>
+ logger.debug(s"Registration failed on endpoint ${endpoint.getName}")
+ stay using data
+ case Some(result) =>
+ logger.debug(s"Registration succeeded on endpoint ${endpoint.getName}")
+
+ markEndpointOnline() // TODO: handle failure here
+ scheduleHeartbeatCheck()
+ goto(LinkUp) using LinkUpData(
+ System.currentTimeMillis(),
+ nodeId = result.nodeId,
+ binding = ServiceBindingComponents(result.measServiceBinding, result.heartbeatServiceBinding, nextBindingSeq),
+ availData = data)
+ }
+
+ }
+ case Event(HeartbeatCheck, data) => {
+ logger.warn("Got heartbeat check in available state")
+ stay using data
+ }
+ case Event(CheckLock, data: AvailableData) => {
+ val haveLock = standbyLock.acquireOrHold(UUIDHelpers.protoUUIDToUuid(endpoint.getUuid), nodeId, standbyLockExpiryDurationMs)
+ if (haveLock) {
+ scheduleMsg(standbyLockRetryPeriodMs, CheckLock)
+ stay using data
+ } else {
+ logger.info(s"Endpoint ${endpoint.getName} lost lock while available; going to standby")
+ scheduleMsg(standbyLockRetryPeriodMs, CheckLock)
+ data.regServiceBinding.cancel()
+ goto(Standby) using StandbyData(data.processing, data.config)
+ }
+ }
+ }
+
+ when(LinkUp) {
+ case Event(configUpdate: ConfigUpdate, data: LinkUpData) => {
+ handleConfigUpdate(configUpdate, data.availData.config, data.availData.processing.processor)
+ stay using data
+ }
+ case Event(RegistrationRequest(msgBytes, respFunc), data: LinkUpData) => {
+ val nextSeq = data.binding.bindingSeq + 1
+ handleRegistrationAttemptWhileUp(msgBytes, respFunc, nextSeq, data.nodeId) match {
+ case None =>
+ logger.debug(s"Registration failed on endpoint ${endpoint.getName}")
+ stay using data
+ case Some(result) =>
+ logger.debug(s"Registration succeeded on endpoint ${endpoint.getName}")
+
+ data.binding.measBinding.cancel()
+ data.binding.heartbeatBinding.cancel()
+
+ val bind = ServiceBindingComponents(result.measServiceBinding, result.heartbeatServiceBinding, nextSeq)
+
+ scheduleHeartbeatCheck()
+ stay using LinkUpData(System.currentTimeMillis(), nodeId = result.nodeId, binding = bind, availData = data.availData)
+ }
+ }
+ case Event(MeasBatchRequest(msgBytes, respFunc, bindSeq), data: LinkUpData) => {
+ if (bindSeq == data.binding.bindingSeq) {
+ data.availData.processing.processor.handle(msgBytes, respFunc)
+
+ scheduleHeartbeatCheck()
+ stay using data.copy(lastHeartbeat = System.currentTimeMillis())
+ } else {
+ stay using data
+ }
+ }
+ case Event(HeartbeatRequest(msgBytes, respFunc, bindSeq), data: LinkUpData) => {
+ if (bindSeq == data.binding.bindingSeq) {
+ handleHeartbeat(msgBytes, respFunc)
+ scheduleHeartbeatCheck()
+ stay using data.copy(lastHeartbeat = System.currentTimeMillis())
+ } else {
+ stay using data
+ }
+ }
+ case Event(HeartbeatCheck, data: LinkUpData) => {
+ logger.trace(s"Endpoint ${endpoint.getName} checking heartbeat in LinkUp state")
+ val now = System.currentTimeMillis()
+ val elapsed = now - data.lastHeartbeat // TODO: sanity checks on time warping?
+
+ if (elapsed >= heartbeatTimeout) {
+ try {
+ val prevValues: Seq[(UUID, Measurement)] = markEndpointOffline(pointSetUuidConversion(data.availData.config.pointSet), data.availData.processing.processor)
+
+ logger.info(s"Endpoint ${endpoint.getName} accepting failover registrations")
+ goto(LinkHalfDown) using LinkHalfDownData(previousValues = prevValues, nodeId = data.nodeId, data.binding, data.availData)
+
+ } catch {
+ case ex: Throwable =>
+ logger.error(s"Could not mark endpoint ${endpoint.getName} as down: " + ex)
+ scheduleMsg(markRetryPeriodMs, MarkRetry)
+
+ logger.info(s"Endpoint ${endpoint.getName} accepting failover registrations, but marking down failed")
+ goto(LinkHalfDownMarkFailed) using data
+ }
+ } else {
+ // this is likely an old check; an update came in since
+ stay using data
+ }
+ }
+ case Event(CheckLock, data: LinkUpData) => {
+ val haveLock = standbyLock.acquireOrHold(UUIDHelpers.protoUUIDToUuid(endpoint.getUuid), nodeId, standbyLockExpiryDurationMs)
+ if (haveLock) {
+ scheduleMsg(standbyLockRetryPeriodMs, CheckLock)
+ stay using data
+ } else {
+ logger.info(s"Endpoint ${endpoint.getName} lost lock while link up; going to standby")
+ data.binding.measBinding.cancel()
+ data.binding.heartbeatBinding.cancel()
+ data.availData.regServiceBinding.cancel()
+
+ scheduleMsg(standbyLockRetryPeriodMs, CheckLock)
+ goto(Standby) using StandbyData(data.availData.processing, data.availData.config)
+ }
+ }
+ }
+
+ when(LinkHalfDownMarkFailed) {
+ case Event(configUpdate: ConfigUpdate, data: LinkUpData) => {
+ handleConfigUpdate(configUpdate, data.availData.config, data.availData.processing.processor)
+ stay using data
+ }
+ case Event(MarkRetry, data: LinkUpData) => {
+ try {
+ val prevValues: Seq[(UUID, Measurement)] = markEndpointOffline(pointSetUuidConversion(data.availData.config.pointSet), data.availData.processing.processor)
+
+ goto(LinkHalfDown) using LinkHalfDownData(previousValues = prevValues, nodeId = data.nodeId, data.binding, data.availData)
+
+ } catch {
+ case ex: Throwable =>
+ logger.error(s"Could not mark endpoint ${endpoint.getName} as down: " + ex)
+ scheduleMsg(markRetryPeriodMs, MarkRetry)
+ stay using data
+ }
+ }
+ case Event(RegistrationRequest(msgBytes, respFunc), data: LinkUpData) => {
+
+ val nextSeq = data.binding.bindingSeq + 1
+ handleRegistrationAttemptWhileDown(msgBytes, respFunc, nextSeq) match {
+ case None =>
+ logger.debug(s"Registration failed on endpoint ${endpoint.getName}")
+ stay using data
+ case Some(result) =>
+ logger.debug(s"Registration succeeded on endpoint ${endpoint.getName}")
+
+ data.binding.measBinding.cancel()
+ data.binding.heartbeatBinding.cancel()
+
+ val bind = ServiceBindingComponents(result.measServiceBinding, result.heartbeatServiceBinding, nextSeq)
+
+ scheduleHeartbeatCheck()
+ goto(LinkUp) using LinkUpData(System.currentTimeMillis(), nodeId = data.nodeId, binding = bind, availData = data.availData)
+ }
+ }
+ case Event(HeartbeatRequest(msgBytes, respFunc, bindSeq), data: LinkUpData) => {
+ if (bindSeq == data.binding.bindingSeq) {
+ markEndpointOnline()
+ handleHeartbeat(msgBytes, respFunc)
+
+ scheduleHeartbeatCheck()
+ stay using data.copy(lastHeartbeat = System.currentTimeMillis())
+ } else {
+ stay using data
+ }
+ }
+ case Event(MeasBatchRequest(msgBytes, respFunc, bindSeq), data: LinkUpData) => {
+ if (bindSeq == data.binding.bindingSeq) {
+ logger.debug(s"Endpoint ${endpoint.getName} was half down and came back up")
+
+ markEndpointOnline()
+ data.availData.processing.processor.handle(msgBytes, respFunc)
+
+ scheduleHeartbeatCheck()
+ goto(LinkUp) using data.copy(lastHeartbeat = System.currentTimeMillis())
+ } else {
+ stay using data
+ }
+ }
+ case Event(HeartbeatCheck, data: LinkUpData) => {
+ stay using data
+ }
+ case Event(CheckLock, data: LinkUpData) => {
+ val haveLock = standbyLock.acquireOrHold(UUIDHelpers.protoUUIDToUuid(endpoint.getUuid), nodeId, standbyLockExpiryDurationMs)
+ if (haveLock) {
+ scheduleMsg(standbyLockRetryPeriodMs, CheckLock)
+ stay using data
+ } else {
+ logger.info(s"Endpoint ${endpoint.getName} lost lock while link down; going to standby")
+ data.binding.measBinding.cancel()
+ data.binding.heartbeatBinding.cancel()
+ data.availData.regServiceBinding.cancel()
+
+ scheduleMsg(standbyLockRetryPeriodMs, CheckLock)
+ goto(Standby) using StandbyData(data.availData.processing, data.availData.config)
+ }
+ }
+ }
+
+ when(LinkHalfDown) {
+ case Event(configUpdate: ConfigUpdate, data: LinkHalfDownData) => {
+ handleConfigUpdate(configUpdate, data.availData.config, data.availData.processing.processor)
+ stay using data
+ }
+ case Event(RegistrationRequest(msgBytes, respFunc), data: LinkHalfDownData) => {
+
+ val nextSeq = data.binding.bindingSeq + 1
+ handleRegistrationAttemptWhileDown(msgBytes, respFunc, nextSeq) match {
+ case None =>
+ logger.debug(s"Registration failed on endpoint ${endpoint.getName}")
+ stay using data
+ case Some(result) =>
+ logger.debug(s"Registration succeeded on endpoint ${endpoint.getName}")
+
+ data.binding.measBinding.cancel()
+ data.binding.heartbeatBinding.cancel()
+
+ val bind = ServiceBindingComponents(result.measServiceBinding, result.heartbeatServiceBinding, nextSeq)
+
+ scheduleHeartbeatCheck()
+ goto(LinkUp) using LinkUpData(System.currentTimeMillis(), nodeId = result.nodeId, binding = bind, availData = data.availData)
+ }
+ }
+ case Event(HeartbeatRequest(msgBytes, respFunc, bindSeq), data: LinkHalfDownData) => {
+ if (bindSeq == data.binding.bindingSeq) {
+ logger.debug(s"Endpoint ${endpoint.getName} was half down and came back up")
+ val uuidSet = data.availData.config.pointSet.map(_._1).map(UUIDHelpers.protoUUIDToUuid).toSet
+ val stillRelevantSet = data.previousValues.filter(kv => uuidSet.contains(kv._1))
+
+ markEndpointOnlineAndRestoreMeasurements(stillRelevantSet, pointSetUuidConversion(data.availData.config.pointSet), data.availData.processing.processor)
+
+ handleHeartbeat(msgBytes, respFunc)
+
+ scheduleHeartbeatCheck()
+ goto(LinkUp) using LinkUpData(lastHeartbeat = System.currentTimeMillis(), nodeId = data.nodeId, binding = data.binding, availData = data.availData)
+ } else {
+ stay using data
+ }
+ }
+ case Event(MeasBatchRequest(msgBytes, respFunc, bindSeq), data: LinkHalfDownData) => {
+ if (bindSeq == data.binding.bindingSeq) {
+
+ logger.debug(s"Endpoint ${endpoint.getName} was half down and came back up")
+ val uuidSet = data.availData.config.pointSet.map(_._1).map(UUIDHelpers.protoUUIDToUuid).toSet
+ val stillRelevantSet = data.previousValues.filter(kv => uuidSet.contains(kv._1))
+
+ markEndpointOnlineAndRestoreMeasurements(stillRelevantSet, pointSetUuidConversion(data.availData.config.pointSet), data.availData.processing.processor)
+ data.availData.processing.processor.handle(msgBytes, respFunc)
+
+ scheduleHeartbeatCheck()
+ goto(LinkUp) using LinkUpData(lastHeartbeat = System.currentTimeMillis(), nodeId = data.nodeId, binding = data.binding, availData = data.availData)
+ } else {
+ stay using data
+ }
+ }
+ case Event(HeartbeatCheck, data: LinkHalfDownData) => {
+ stay using data
+ }
+ case Event(MarkRetry, data: LinkHalfDownData) => {
+ stay using data
+ }
+ case Event(CheckLock, data: LinkHalfDownData) => {
+ val haveLock = standbyLock.acquireOrHold(UUIDHelpers.protoUUIDToUuid(endpoint.getUuid), nodeId, standbyLockExpiryDurationMs)
+ if (haveLock) {
+ scheduleMsg(standbyLockRetryPeriodMs, CheckLock)
+ stay using data
+ } else {
+ logger.info(s"Endpoint ${endpoint.getName} lost lock while link half down; going to standby")
+ data.binding.measBinding.cancel()
+ data.binding.heartbeatBinding.cancel()
+ data.availData.regServiceBinding.cancel()
+
+ scheduleMsg(standbyLockRetryPeriodMs, CheckLock)
+ goto(Standby) using StandbyData(data.availData.processing, data.availData.config)
+ }
+ }
+ }
+
+ private def commonCleanup(data: AvailableData): Unit = {
+ data.regServiceBinding.cancel()
+ config.edgeSubscription.cancel()
+ config.pointSubscription.cancel()
+ config.overrideSubscription.cancel()
+ config.triggerSetSubscription.cancel()
+ }
+
+ // TODO: dry this up further
+ onTermination {
+ case StopEvent(_, Available, data: AvailableData) =>
+ logger.info(s"Processor for endpoint '${endpoint.getName}' terminating, canceling subscriptions")
+ commonCleanup(data)
+ case StopEvent(_, LinkUp, data: LinkUpData) =>
+ logger.info(s"Processor for endpoint '${endpoint.getName}' terminating, canceling subscriptions")
+ data.binding.measBinding.cancel()
+ data.binding.heartbeatBinding.cancel()
+ commonCleanup(data.availData)
+ case StopEvent(_, LinkHalfDown, data: LinkHalfDownData) =>
+ logger.info(s"Processor for endpoint '${endpoint.getName}' terminating, canceling subscriptions")
+ data.binding.measBinding.cancel()
+ data.binding.heartbeatBinding.cancel()
+ commonCleanup(data.availData)
+ case StopEvent(_, LinkHalfDownMarkFailed, data: LinkUpData) =>
+ logger.info(s"Processor for endpoint '${endpoint.getName}' terminating, canceling subscriptions")
+ data.binding.measBinding.cancel()
+ data.binding.heartbeatBinding.cancel()
+ commonCleanup(data.availData)
+ }
+
+ initialize()
+
+ private def handleConfigUpdate(update: ConfigUpdate, current: ConfigComponents, processor: EndpointProcessor): ConfigComponents = {
+ logger.trace(s"Endpoint ${endpoint.getName} got config update: " + update)
+ update match {
+ case PossiblePointNotification(pointUuid) => {
+ ServiceConfiguration.configForPointAdded(resources.session, pointUuid).map {
+ case None =>
+ self ! PointMiss(pointUuid)
+ case Some((pt, overOpt, triggerOpt)) =>
+ val tup = (pt.getUuid, pt.getName)
+ self ! PointAdded(tup, overOpt, triggerOpt)
+ }
+ current.copy(outstandingPoints = current.outstandingPoints + pointUuid)
+ }
+ case PointMiss(pointUuid) => {
+ current.copy(outstandingPoints = current.outstandingPoints - pointUuid)
+ }
+ case PointAdded(point, over, trigger) =>
+ processor.pointAdded(point, over, trigger)
+ current.copy(outstandingPoints = current.outstandingPoints - point._1, pointSet = current.pointSet :+ point)
+ case PointRemoved(pointUuid) =>
+ processor.pointRemoved(pointUuid)
+ current.copy(outstandingPoints = current.outstandingPoints - pointUuid)
+ case PointModified(point) =>
+ processor.pointModified(point)
+ current
+ case OverridePut(v) =>
+ processor.overridePut(v)
+ current
+ case OverrideDeleted(v) =>
+ processor.overrideDeleted(v)
+ current
+ case TriggerSetPut(uuid, v) =>
+ processor.triggerSetPut(uuid, v)
+ current
+ case TriggerSetDeleted(uuid) =>
+ processor.triggerSetDeleted(uuid)
+ current
+ }
+ }
+
+ private def scheduleHeartbeatCheck(): Unit = {
+ scheduleMsg(heartbeatTimeout, HeartbeatCheck)
+ }
+
+ private def pointSetUuidConversion(points: Seq[(ModelUUID, String)]): Seq[(UUID, String)] = {
+ points.map { case (uuid, name) => (UUIDHelpers.protoUUIDToUuid(uuid), name) }
+ }
+
+ private def setupProcessor(config: EndpointConfiguration): (EndpointProcessor, ActorRef) = {
+ val publisher = context.actorOf(EventPublisher.props(resources.session))
+
+ def publishEvents(events: Seq[EventTemplate.Builder]) { if (events.nonEmpty) publisher ! EventTemplateGroup(events) }
+
+ val processor = new EndpointProcessor(
+ endpoint.getName,
+ config.points,
+ config.overrides,
+ config.triggerSets,
+ resources.store,
+ measNotificationBatch(resources.serviceOps, endpoint, _),
+ publishEvents)
+
+ import io.greenbus.client.proto.Envelope.SubscriptionEventType._
+
+ config.pointSubscription.start { note =>
+ note.getEventType match {
+ case MODIFIED => self ! PointModified(note.getValue)
+ case _ => // let add/removes be picked up by edge sub
+ }
+ }
+
+ config.overrideSubscription.start { note =>
+ note.getEventType match {
+ case ADDED | MODIFIED => self ! OverridePut(note.getValue)
+ case REMOVED => self ! OverrideDeleted(note.getValue)
+ }
+ }
+
+ config.triggerSetSubscription.start { note =>
+ val triggerSetOpt = ServiceConfiguration.keyValueToTriggerSet(note.getValue)
+ triggerSetOpt.foreach { triggerSet =>
+ note.getEventType match {
+ case ADDED | MODIFIED => self ! TriggerSetPut(note.getValue.getUuid, triggerSet)
+ case REMOVED => self ! TriggerSetDeleted(note.getValue.getUuid)
+ }
+ }
+ }
+
+ config.edgeSubscription.start { note =>
+ note.getEventType match {
+ case ADDED | MODIFIED => self ! PossiblePointNotification(note.getValue.getChild)
+ case REMOVED => self ! PointRemoved(note.getValue.getChild)
+ }
+ }
+
+ (processor, publisher)
+ }
+
+ private def setupRegistrationService(): SubscriptionBinding = {
+
+ logger.debug(s"Subscribing to service queue for ${endpoint.getName}")
+ val binding = resources.serviceOps.bindRoutedService(new ActorForwardingServiceHandler(self, RegistrationRequest))
+
+ logger.debug(s"Binding service queue to exchange for ${endpoint.getName}")
+ resources.serviceOps.bindQueue(binding.getId(), FrontEndService.Descriptors.PutFrontEndRegistration.requestId, endpoint.getUuid.getValue)
+
+ binding
+ }
+
+ private def handleRegistrationAttemptWhileDown(msg: Array[Byte], responseHandler: (Array[Byte]) => Unit, sequence: Long): Option[RegistrationResult] = {
+
+ val measService = new ActorForwardingServiceHandler(self, MeasBatchRequest(_, _, sequence))
+ val heartbeatService = new ActorForwardingServiceHandler(self, HeartbeatRequest(_, _, sequence))
+ val resultHolder = new RegistrationResultHolder
+
+ val handler = RegistrationServiceHandler.wrapped(
+ endpoint,
+ sql,
+ auth,
+ resources.serviceOps,
+ frontEndModel,
+ measService,
+ heartbeatService,
+ resultHolder,
+ down = true,
+ currentNodeId = None)
+
+ handler.handleMessage(msg, responseHandler)
+ resultHolder.getOption
+ }
+
+ private def handleRegistrationAttemptWhileUp(msg: Array[Byte], responseHandler: (Array[Byte]) => Unit, sequence: Long, nodeId: Option[String]): Option[RegistrationResult] = {
+
+ val measService = new ActorForwardingServiceHandler(self, MeasBatchRequest(_, _, sequence))
+ val heartbeatService = new ActorForwardingServiceHandler(self, HeartbeatRequest(_, _, sequence))
+ val resultHolder = new RegistrationResultHolder
+
+ val handler = RegistrationServiceHandler.wrapped(
+ endpoint,
+ sql,
+ auth,
+ resources.serviceOps,
+ frontEndModel,
+ measService,
+ heartbeatService,
+ resultHolder,
+ down = false,
+ currentNodeId = nodeId)
+
+ handler.handleMessage(msg, responseHandler)
+ resultHolder.getOption
+ }
+
+ private def handleHeartbeat(msg: Array[Byte], responseHandler: (Array[Byte]) => Unit) = {
+ logger.trace(s"Endpoint ${endpoint.getName} handling heartbeat")
+ val handler = HeartbeatServiceHandler.wrapped(endpoint, sql, frontEndModel, statusNotifier)
+
+ handler.handleMessage(msg, responseHandler)
+ }
+
+ private def markEndpointOffline(points: Seq[(UUID, String)], processor: EndpointProcessor): Seq[(UUID, Measurement)] = {
+ logger.debug(s"Marking Endpoint ${endpoint.getName} offline")
+ sql.transaction {
+
+ val entry = Seq((UUIDHelpers.protoUUIDToUuid(endpoint.getUuid), FrontEndConnectionStatus.Status.COMMS_DOWN))
+ frontEndModel.putFrontEndConnectionStatuses(statusNotifier, entry)
+
+ val uuids = points.map(_._1)
+
+ val currentValues = resources.store.get(uuids)
+ val now = System.currentTimeMillis()
+
+ val markedOld = currentValues.map {
+ case (uuid, meas) =>
+
+ val (qualBuilder, detailQualBuilder) = mergeableQualityElements(meas)
+
+ val marked = meas.toBuilder
+ .setQuality(
+ qualBuilder.setValidity(Validity.INVALID)
+ .setDetailQual(detailQualBuilder.setOldData(true)))
+ .setTime(now)
+ .build()
+
+ (uuid, marked)
+ }
+
+ resources.store.put(markedOld)
+ processor.modifyLastValues(markedOld.map(tup => (tup._1.toString, tup._2)))
+
+ notifyFromStore(markedOld, points)
+
+ currentValues
+ }
+ }
+
+ private def markEndpointOnline(): Unit = {
+ logger.debug(s"Marking Endpoint ${endpoint.getName} online")
+ sql.transaction {
+ val entry = Seq((UUIDHelpers.protoUUIDToUuid(endpoint.getUuid), FrontEndConnectionStatus.Status.COMMS_UP))
+ frontEndModel.putFrontEndConnectionStatuses(statusNotifier, entry)
+ }
+ }
+
+ private def markEndpointOnlineAndRestoreMeasurements(previousValues: Seq[(UUID, Measurement)], points: Seq[(UUID, String)], processor: EndpointProcessor): Unit = {
+ logger.debug(s"Marking Endpoint ${endpoint.getName} online and restoring measurement quality")
+
+ val now = System.currentTimeMillis()
+ sql.transaction {
+
+ val entry = Seq((UUIDHelpers.protoUUIDToUuid(endpoint.getUuid), FrontEndConnectionStatus.Status.COMMS_UP))
+ frontEndModel.putFrontEndConnectionStatuses(statusNotifier, entry)
+
+ val restoredMeases = previousValues.map {
+ case (uuid, meas) =>
+ val updatedTimeMeas = meas.toBuilder.setTime(now).build()
+ (uuid, updatedTimeMeas)
+ }
+
+ resources.store.put(restoredMeases)
+ processor.modifyLastValues(restoredMeases.map(tup => (tup._1.toString, tup._2)))
+
+ notifyFromStore(restoredMeases, points)
+ }
+ }
+
+ private def notifyFromStore(values: Seq[(UUID, Measurement)], pointList: Seq[(UUID, String)]): Unit = {
+ val uuidToName = pointList.map(tup => (tup._1, tup._2)).toMap
+
+ val notifications = values.flatMap { case (uuid, m) => uuidToName.get(uuid).map { name => (UUIDHelpers.uuidToProtoUUID(uuid), name, m) } }
+
+ measNotificationBatch(resources.serviceOps, endpoint, notifications)
+ }
+}
diff --git a/processing/src/main/scala/io/greenbus/measproc/EventPublisher.scala b/processing/src/main/scala/io/greenbus/measproc/EventPublisher.scala
new file mode 100644
index 0000000..5ba6b26
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/EventPublisher.scala
@@ -0,0 +1,112 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc
+
+import io.greenbus.msg.Session
+import akka.actor.{ Actor, Props }
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.client.exception.{ ServiceException, UnauthorizedException }
+import java.util.concurrent.TimeoutException
+import io.greenbus.client.service.EventService
+import scala.concurrent.duration._
+import io.greenbus.client.service.proto.EventRequests.EventTemplate
+
+object EventPublisher {
+
+ val pubRetryMs = 1000
+
+ case class EventTemplateGroup(events: Seq[EventTemplate.Builder])
+
+ def props(session: Session): Props = {
+ Props(classOf[EventPublisher], session)
+ }
+}
+
+class EventPublisher(session: Session) extends Actor with Logging {
+ import EventPublisher._
+
+ private case object PostSuccess
+ private case class PostFailure(ex: Throwable)
+ private case object PublishRetry
+
+ private var queued = List.empty[EventTemplate]
+ private var outstanding = List.empty[EventTemplate]
+
+ def receive = {
+ case EventTemplateGroup(events) =>
+ logger.trace(s"Received events to publish")
+ events.foreach(queued ::= _.build())
+ if (outstanding.isEmpty) {
+ publish()
+ }
+
+ case PostSuccess =>
+ logger.trace(s"Published events")
+ outstanding = Nil
+ if (queued.nonEmpty) {
+ publish()
+ }
+
+ case PostFailure(error) => {
+ error match {
+ case ex: UnauthorizedException => throw ex
+ case ex: ServiceException =>
+ logger.warn(s"Publishing failure: " + ex.getMessage)
+ scheduleRetry()
+ case ex: TimeoutException =>
+ logger.warn(s"Publishing timeout")
+ scheduleRetry()
+ case ex => throw ex
+ }
+ }
+
+ case PublishRetry =>
+ if (queued.nonEmpty || outstanding.nonEmpty) {
+ publish()
+ }
+ }
+
+ private def scheduleRetry() {
+ logger.warn(s"Publishing failure, retrying...")
+ scheduleMsg(pubRetryMs, PublishRetry)
+ }
+
+ private def publish() {
+ val client = EventService.client(session)
+
+ outstanding :::= queued.reverse
+ queued = Nil
+
+ logger.trace("Publishing the following events: " + outstanding)
+
+ val postFut = client.postEvents(outstanding)
+
+ import context.dispatcher
+ postFut.onSuccess { case result => self ! PostSuccess }
+ postFut.onFailure { case ex => self ! PostFailure(ex) }
+ }
+
+ private def scheduleMsg(timeMs: Long, msg: AnyRef) {
+ import context.dispatcher
+ context.system.scheduler.scheduleOnce(
+ Duration(timeMs, MILLISECONDS),
+ self,
+ msg)
+ }
+}
\ No newline at end of file
diff --git a/processing/src/main/scala/io/greenbus/measproc/HeartbeatServiceHandler.scala b/processing/src/main/scala/io/greenbus/measproc/HeartbeatServiceHandler.scala
new file mode 100644
index 0000000..7c127ba
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/HeartbeatServiceHandler.scala
@@ -0,0 +1,85 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc
+
+import io.greenbus.msg.service.ServiceHandler
+import io.greenbus.client.exception.BadRequestException
+import io.greenbus.client.proto.Envelope
+import io.greenbus.client.service.FrontEndService
+import io.greenbus.client.service.proto.FrontEndRequests.{ PutFrontEndConnectionStatusRequest, PutFrontEndConnectionStatusResponse }
+import io.greenbus.client.service.proto.Model.Endpoint
+import io.greenbus.services.framework._
+import io.greenbus.services.model.FrontEndModel
+import io.greenbus.services.model.UUIDHelpers._
+import io.greenbus.sql.DbConnection
+
+import scala.collection.JavaConversions._
+
+object HeartbeatServiceHandler {
+
+ def wrapped(
+ endpoint: Endpoint,
+ sql: DbConnection,
+ frontEndModel: FrontEndModel,
+ notifier: ModelNotifier): ServiceHandler = {
+
+ val handler = new HeartbeatServiceHandler(endpoint, sql, frontEndModel, notifier)
+
+ new EnvelopeParsingHandler(
+ new DecodingServiceHandler(FrontEndService.Descriptors.PutFrontEndConnectionStatuses,
+ new ModelErrorTransformingHandler(handler)))
+ }
+
+}
+
+class HeartbeatServiceHandler(
+ endpoint: Endpoint,
+ sql: DbConnection,
+ frontEndModel: FrontEndModel,
+ notifier: ModelNotifier) extends TypedServiceHandler[PutFrontEndConnectionStatusRequest, PutFrontEndConnectionStatusResponse] {
+
+ def handle(request: PutFrontEndConnectionStatusRequest, headers: Map[String, String], responseHandler: (Response[PutFrontEndConnectionStatusResponse]) => Unit) {
+
+ val results = sql.transaction {
+
+ if (request.getUpdatesCount == 0) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val updates = request.getUpdatesList.toSeq
+
+ val modelUpdates = updates.map { update =>
+ if (!update.hasEndpointUuid) {
+ throw new BadRequestException("Must include endpoint uuid")
+ }
+ if (!update.hasStatus) {
+ throw new BadRequestException("Must include communication status")
+ }
+
+ (protoUUIDToUuid(update.getEndpointUuid), update.getStatus)
+ }
+
+ frontEndModel.putFrontEndConnectionStatuses(notifier, modelUpdates, None)
+
+ }
+
+ val response = PutFrontEndConnectionStatusResponse.newBuilder().addAllResults(results).build
+ responseHandler(Success(Envelope.Status.OK, response))
+ }
+}
diff --git a/processing/src/main/scala/io/greenbus/measproc/MeasurementProcessor.scala b/processing/src/main/scala/io/greenbus/measproc/MeasurementProcessor.scala
new file mode 100644
index 0000000..c0d895a
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/MeasurementProcessor.scala
@@ -0,0 +1,114 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc
+
+import java.util.UUID
+
+import akka.actor._
+import com.typesafe.config.ConfigFactory
+import io.greenbus.app.actor.{ AllEndpointsStrategy, ConnectedApplicationManager, EndpointCollectionManager, EndpointMonitor }
+import io.greenbus.client.service.proto.FrontEnd.FrontEndConnectionStatus
+import io.greenbus.client.service.proto.Model.Endpoint
+import io.greenbus.client.service.{ FrontEndService, MeasurementService }
+import io.greenbus.measproc.EndpointStreamManager.StreamResources
+import io.greenbus.measproc.lock.TransactionPostgresLockModel
+import io.greenbus.msg.Session
+import io.greenbus.msg.amqp.AmqpServiceOperations
+import io.greenbus.mstore.sql.SqlCurrentValueAndHistorian
+import io.greenbus.services.authz.DefaultAuthLookup
+import io.greenbus.services.core.FrontEndConnectionStatusSubscriptionDescriptor
+import io.greenbus.services.framework.{ ModelEventMapper, SimpleModelNotifier, SubscriptionChannelManager }
+import io.greenbus.services.model.SquerylFrontEndModel
+import io.greenbus.sql.{ DbConnection, DbConnector, SqlSettings }
+
+object MeasurementProcessor {
+
+ private def buildModelNotifier(serviceOps: AmqpServiceOperations) = {
+
+ val commsStatus = new SubscriptionChannelManager(FrontEndConnectionStatusSubscriptionDescriptor, serviceOps, classOf[FrontEndConnectionStatus].getSimpleName)
+
+ val eventMapper = new ModelEventMapper
+ eventMapper.register(classOf[FrontEndConnectionStatus], commsStatus)
+
+ new SimpleModelNotifier(eventMapper, serviceOps, (_, _) => {})
+ }
+
+ def buildProcessor(
+ amqpConfigPath: String,
+ sqlConfigPath: String,
+ userConfigPath: String,
+ heartbeatTimeoutMs: Long,
+ nodeId: String,
+ standbyLockRetryPeriodMs: Long = 5000,
+ standbyLockExpiryDurationMs: Long = 12500): Props = {
+
+ val collectionStrategy = new AllEndpointsStrategy
+ val markRetryTimeoutMs = 2000
+
+ def connectSql(): DbConnection = {
+ val config = SqlSettings.load(sqlConfigPath)
+ DbConnector.connect(config)
+ }
+
+ def processFactory(session: Session, serviceOps: AmqpServiceOperations, sql: DbConnection): Props = {
+
+ serviceOps.declareExchange(FrontEndService.Descriptors.PutFrontEndRegistration.requestId)
+ serviceOps.declareExchange(MeasurementService.Descriptors.PostMeasurements.requestId)
+ serviceOps.declareExchange(FrontEndService.Descriptors.PutFrontEndConnectionStatuses.requestId)
+
+ val resources = StreamResources(session, serviceOps, new SqlCurrentValueAndHistorian(sql))
+
+ val notifier = buildModelNotifier(serviceOps)
+
+ val lockService = new TransactionPostgresLockModel(sql)
+
+ def bootstrapperFactory(endpoint: Endpoint, config: EndpointConfiguration): Props = {
+ EndpointStreamManager.props(endpoint, resources, config, sql, DefaultAuthLookup, SquerylFrontEndModel, notifier, lockService,
+ nodeId, heartbeatTimeoutMs, markRetryTimeoutMs, standbyLockRetryPeriodMs, standbyLockExpiryDurationMs)
+ }
+
+ def endMgrFactory(endpoint: Endpoint, session: Session, serviceOps: AmqpServiceOperations): Props = {
+ EndpointStreamBootstrapper.props(endpoint, session, bootstrapperFactory)
+ }
+
+ EndpointCollectionManager.props(collectionStrategy, session, serviceOps, None,
+ EndpointMonitor.props(_, _, _, _, endMgrFactory))
+ }
+
+ ConnectedApplicationManager.props("Measurement Processor", amqpConfigPath, userConfigPath, processFactory(_, _, connectSql()))
+ }
+
+ def main(args: Array[String]) {
+
+ val heartbeatTimeoutMs = 10000
+
+ val rootConfig = ConfigFactory.load()
+ val slf4jConfig = ConfigFactory.parseString("""akka { loggers = ["akka.event.slf4j.Slf4jLogger"] }""")
+ val akkaConfig = slf4jConfig.withFallback(rootConfig)
+ val system = ActorSystem("measproc", akkaConfig)
+
+ val baseDir = Option(System.getProperty("io.greenbus.config.base")).getOrElse("")
+ val amqpConfigPath = Option(System.getProperty("io.greenbus.config.amqp")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.msg.amqp.cfg")
+ val sqlConfigPath = Option(System.getProperty("io.greenbus.config.sql")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.sql.cfg")
+ val userConfigPath = Option(System.getProperty("io.greenbus.config.user")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.user.cfg")
+
+ val mgr = system.actorOf(buildProcessor(amqpConfigPath, sqlConfigPath, userConfigPath, heartbeatTimeoutMs, UUID.randomUUID().toString))
+ }
+}
+
diff --git a/processing/src/main/scala/io/greenbus/measproc/ProcessorService.scala b/processing/src/main/scala/io/greenbus/measproc/ProcessorService.scala
new file mode 100644
index 0000000..f9528a5
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/ProcessorService.scala
@@ -0,0 +1,114 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc
+
+import io.greenbus.client.proto.Envelope.{ ServiceResponse, ServiceRequest }
+import io.greenbus.client.exception.BadRequestException
+import io.greenbus.client.service.proto.MeasurementRequests.PostMeasurementsRequest
+import io.greenbus.client.proto.Envelope
+import com.google.protobuf.InvalidProtocolBufferException
+import com.typesafe.scalalogging.slf4j.Logging
+import scala.collection.JavaConversions._
+import io.greenbus.client.service.proto.Measurements.Measurement
+import io.greenbus.jmx.Metrics
+
+class ProcessorService(endpoint: String, metrics: Metrics) extends Logging {
+
+ private val handleTime = metrics.average("BatchHandleTime")
+ private val batchSize = metrics.average("BatchSize")
+ private val measHandledCount = metrics.counter("MeasHandledCount")
+ private val badRequestCount = metrics.average("BadRequestCount")
+ private val serviceErrorCount = metrics.average("ServiceErrorCount")
+
+ def handle(
+ bytes: Array[Byte],
+ responseHandler: Array[Byte] => Unit,
+ process: (Seq[(String, Measurement)], Option[Long]) => Unit,
+ pointMap: PointMap) {
+
+ val startTime = System.currentTimeMillis()
+
+ try {
+ val requestEnvelope = ServiceRequest.parseFrom(bytes)
+
+ if (!requestEnvelope.hasPayload) {
+ throw new BadRequestException("Must include request payload")
+ }
+ val payload = requestEnvelope.getPayload.toByteArray
+
+ val request = PostMeasurementsRequest.parseFrom(payload)
+ if (!request.hasBatch) {
+ throw new BadRequestException("Must include measurement batch")
+ }
+
+ val batch = request.getBatch
+ if (batch.getNamedMeasurementsCount == 0 && batch.getPointMeasurementsCount == 0) {
+ logger.debug(s"Empty measurement batch for endpoint $endpoint")
+ } else {
+
+ if (batch.getPointMeasurementsList.exists(!_.hasPointUuid)) {
+ throw new BadRequestException("Must include point uuids for all id-based measurements")
+ }
+ if (batch.getNamedMeasurementsList.exists(!_.hasPointName)) {
+ throw new BadRequestException("Must include point name for all name-based measurements")
+ }
+
+ val wallTime = if (batch.hasWallTime) Some(batch.getWallTime) else None
+ val idList = batch.getPointMeasurementsList.map(pm => (pm.getPointUuid.getValue, pm.getValue)).toSeq
+ val nameList = batch.getNamedMeasurementsList.flatMap(nm => pointMap.nameToId.get(nm.getPointName).map(uuid => (uuid.getValue, nm.getValue))).toSeq
+
+ val allMeas = idList ++ nameList
+ val count = allMeas.size
+ batchSize(count)
+ measHandledCount(count)
+
+ process(allMeas, wallTime)
+ }
+
+ responseHandler(ServiceResponse.newBuilder().setStatus(Envelope.Status.OK).build().toByteArray)
+
+ } catch {
+ case ex: BadRequestException =>
+ badRequestCount(1)
+ logger.warn("Bad client request: " + ex.getMessage)
+ val response = buildErrorEnvelope(Envelope.Status.BAD_REQUEST, ex.getMessage)
+ responseHandler(response.toByteArray)
+ case protoEx: InvalidProtocolBufferException =>
+ badRequestCount(1)
+ logger.warn("Error parsing client request: " + protoEx.getMessage)
+ val response = buildErrorEnvelope(Envelope.Status.BAD_REQUEST, "Couldn't parse service request.")
+ responseHandler(response.toByteArray)
+ case ex: Throwable =>
+ serviceErrorCount(1)
+ logger.error("Internal service error: " + ex)
+ val response = buildErrorEnvelope(Envelope.Status.INTERNAL_ERROR, "Internal service error.")
+ responseHandler(response.toByteArray)
+ }
+
+ handleTime((System.currentTimeMillis() - startTime).toInt)
+
+ }
+
+ def buildErrorEnvelope(status: Envelope.Status, message: String): ServiceResponse = {
+ ServiceResponse.newBuilder()
+ .setStatus(status)
+ .setErrorMessage(message)
+ .build()
+ }
+}
diff --git a/processing/src/main/scala/io/greenbus/measproc/RegistrationServiceHandler.scala b/processing/src/main/scala/io/greenbus/measproc/RegistrationServiceHandler.scala
new file mode 100644
index 0000000..b6614be
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/RegistrationServiceHandler.scala
@@ -0,0 +1,174 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc
+
+import java.util.UUID
+
+import io.greenbus.msg.SubscriptionBinding
+import io.greenbus.msg.amqp.AmqpServiceOperations
+import io.greenbus.msg.service.ServiceHandler
+import io.greenbus.client.exception.{ BadRequestException, InternalServiceException }
+import io.greenbus.client.proto.Envelope
+import io.greenbus.client.proto.Envelope.ServiceResponse
+import io.greenbus.client.service.{ FrontEndService, MeasurementService }
+import io.greenbus.client.service.proto.CommandRequests.PostCommandRequestRequest
+import io.greenbus.client.service.proto.FrontEndRequests.{ PutFrontEndRegistrationRequest, PutFrontEndRegistrationResponse, FrontEndRegistrationTemplate }
+import io.greenbus.client.service.proto.Model.Endpoint
+import io.greenbus.services.authz.{ EntityFilter, AuthLookup }
+import io.greenbus.services.core.FrontEndServices
+import io.greenbus.services.framework._
+import io.greenbus.services.model.FrontEndModel
+import io.greenbus.services.model.FrontEndModel.FrontEndConnectionTemplate
+import io.greenbus.services.model.UUIDHelpers._
+import io.greenbus.sql.DbConnection
+
+case class RegistrationResult(address: String, measServiceBinding: SubscriptionBinding, heartbeatServiceBinding: SubscriptionBinding, nodeId: Option[String])
+
+class RegistrationResultHolder {
+ private var resultOpt = Option.empty[RegistrationResult]
+ def set(result: RegistrationResult): Unit = { resultOpt = Some(result) }
+ def getOption: Option[RegistrationResult] = resultOpt
+}
+
+object RegistrationServiceHandler {
+
+ def wrapped(endpoint: Endpoint,
+ sql: DbConnection,
+ auth: AuthLookup,
+ ops: AmqpServiceOperations,
+ frontEndModel: FrontEndModel,
+ measServiceHandler: ServiceHandler,
+ heartbeatServiceHandler: ServiceHandler,
+ resultHolder: RegistrationResultHolder,
+ down: Boolean,
+ currentNodeId: Option[String]): ServiceHandler = {
+
+ val handler = new RegistrationServiceHandler(
+ endpoint,
+ sql,
+ auth,
+ ops,
+ frontEndModel,
+ measServiceHandler,
+ heartbeatServiceHandler,
+ resultHolder,
+ down,
+ currentNodeId)
+
+ new EnvelopeParsingHandler(
+ new DecodingServiceHandler(FrontEndService.Descriptors.PutFrontEndRegistration,
+ new ModelErrorTransformingHandler(handler)))
+ }
+
+ val unavailableResponse = {
+ ServiceResponse.newBuilder()
+ .setStatus(Envelope.Status.LOCKED)
+ .setErrorMessage("FEP already registered")
+ .build()
+ }
+
+ val unavailableResponseBytes = {
+ unavailableResponse.toByteArray
+ }
+
+}
+
+class RegistrationServiceHandler(
+ endpoint: Endpoint,
+ sql: DbConnection,
+ auth: AuthLookup,
+ ops: AmqpServiceOperations,
+ frontEndModel: FrontEndModel,
+ measServiceHandler: ServiceHandler,
+ heartbeatServiceHandler: ServiceHandler,
+ resultHolder: RegistrationResultHolder,
+ down: Boolean,
+ currentNodeId: Option[String])
+ extends TypedServiceHandler[PutFrontEndRegistrationRequest, PutFrontEndRegistrationResponse] {
+
+ def handle(request: PutFrontEndRegistrationRequest, headers: Map[String, String], responseHandler: (Response[PutFrontEndRegistrationResponse]) => Unit): Unit = {
+
+ sql.transaction {
+ val authContext = auth.validateAuth(headers)
+ val filter = authContext.authorize(FrontEndServices.frontEndRegistrationResource, "update")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include at least one front end connection request")
+ }
+
+ val template = request.getRequest
+
+ if (!template.hasEndpointUuid) {
+ throw new BadRequestException("Must include endpoint uuid")
+ }
+
+ if (template.getEndpointUuid != endpoint.getUuid) {
+ throw new BadRequestException("Incorrect endpoint uuid for service address")
+ }
+
+ val nodeIdOpt = if (template.hasFepNodeId) Some(template.getFepNodeId) else None
+
+ val holdingLockOpt = if (template.hasHoldingLock) Some(template.getHoldingLock) else None
+
+ if (!down) {
+ if (nodeIdOpt == currentNodeId || holdingLockOpt == Some(true)) {
+ makeRegistration(template, filter, nodeIdOpt, responseHandler)
+ } else {
+ responseHandler(Failure(Envelope.Status.LOCKED, "FEP already registered"))
+ }
+ } else {
+ makeRegistration(template, filter, nodeIdOpt, responseHandler)
+ }
+ }
+ }
+
+ private def makeRegistration(template: FrontEndRegistrationTemplate, filter: Option[EntityFilter], requestNodeId: Option[String], responseHandler: (Response[PutFrontEndRegistrationResponse]) => Unit): Unit = {
+
+ val cmdAddr = if (template.hasCommandAddress) Some(template.getCommandAddress) else None
+
+ val streamAddress = UUID.randomUUID().toString
+
+ val modelTemplate = FrontEndConnectionTemplate(endpoint.getUuid, streamAddress, cmdAddr)
+
+ val result = frontEndModel.putFrontEndConnections(NullModelNotifier, Seq(modelTemplate), filter).headOption.getOrElse {
+ throw new InternalServiceException("Frontend registration could not be put in database")
+ }
+
+ cmdAddr.foreach { addr =>
+ ops.bindQueue(addr, classOf[PostCommandRequestRequest].getSimpleName, addr)
+ }
+
+ val measStreamBinding = ops.bindRoutedService(measServiceHandler)
+ val heartbeatBinding = ops.bindRoutedService(heartbeatServiceHandler)
+ try {
+ ops.bindQueue(measStreamBinding.getId(), MeasurementService.Descriptors.PostMeasurements.requestId, streamAddress)
+ ops.bindQueue(heartbeatBinding.getId(), FrontEndService.Descriptors.PutFrontEndConnectionStatuses.requestId, streamAddress)
+ } catch {
+ case ex: Throwable =>
+ measStreamBinding.cancel()
+ heartbeatBinding.cancel()
+ throw ex
+ }
+
+ resultHolder.set(RegistrationResult(streamAddress, measStreamBinding, heartbeatBinding, requestNodeId))
+
+ val response = PutFrontEndRegistrationResponse.newBuilder().setResults(result).build
+ responseHandler(Success(Envelope.Status.OK, response))
+ }
+}
diff --git a/processing/src/main/scala/io/greenbus/measproc/ServiceConfiguration.scala b/processing/src/main/scala/io/greenbus/measproc/ServiceConfiguration.scala
new file mode 100644
index 0000000..be10c9b
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/ServiceConfiguration.scala
@@ -0,0 +1,188 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.msg.{ Session, Subscription }
+import io.greenbus.client.service.proto.Model._
+import io.greenbus.client.service.proto.ModelRequests._
+import io.greenbus.client.service.proto.Processing.{ MeasOverride, OverrideNotification, TriggerSet }
+import io.greenbus.client.service.proto.ProcessingRequests.OverrideSubscriptionQuery
+import io.greenbus.client.service.{ ModelService, ProcessingService }
+
+import scala.collection.JavaConversions._
+import scala.concurrent.{ ExecutionContext, Future }
+
+case class EndpointConfiguration(points: Seq[(ModelUUID, String)],
+ pointSubscription: Subscription[PointNotification],
+ overrides: Seq[MeasOverride],
+ overrideSubscription: Subscription[OverrideNotification],
+ triggerSets: Seq[(ModelUUID, TriggerSet)],
+ triggerSetSubscription: Subscription[EntityKeyValueNotification],
+ edgeSubscription: Subscription[EntityEdgeNotification])
+
+object ServiceConfiguration extends Logging {
+
+ val triggerSetKey = "triggerSet"
+
+ def endpointEdgesSubscription(session: Session, endpointId: ModelUUID)(implicit context: ExecutionContext): Future[Subscription[EntityEdgeNotification]] = {
+ val modelClient = ModelService.client(session)
+
+ val edgeQuery = EntityEdgeSubscriptionQuery.newBuilder()
+ .addFilters(EntityEdgeFilter.newBuilder()
+ .setParentUuid(endpointId)
+ .setRelationship("source")
+ .setDistance(1)
+ .build())
+ .build()
+
+ val edgeSubFut = modelClient.subscribeToEdges(edgeQuery)
+
+ edgeSubFut.map(_._2)
+ }
+
+ def keyValueToTriggerSet(kvp: EntityKeyValue): Option[TriggerSet] = {
+ if (kvp.hasValue && kvp.getValue.hasByteArrayValue) {
+ try {
+ Some(TriggerSet.parseFrom(kvp.getValue.getByteArrayValue))
+ } catch {
+ case ex: Throwable =>
+ logger.warn(s"Trigger set for uuid ${kvp.getUuid} couldn't parse")
+ None
+ }
+ } else {
+ logger.warn(s"Trigger set for uuid ${kvp.getUuid} had no byte value")
+ None
+ }
+ }
+ def keyValueToUuidAndTriggerSet(kvp: EntityKeyValue): Option[(ModelUUID, TriggerSet)] = {
+ keyValueToTriggerSet(kvp).map(c => (kvp.getUuid, c))
+ }
+
+ def configForPointAdded(session: Session, uuid: ModelUUID)(implicit context: ExecutionContext): Future[Option[(Point, Option[MeasOverride], Option[TriggerSet])]] = {
+ val modelClient = ModelService.client(session)
+ val procClient = ProcessingService.client(session)
+
+ val keys = EntityKeySet.newBuilder().addUuids(uuid).build()
+
+ val keyPair = EntityKeyPair.newBuilder().setUuid(uuid).setKey(triggerSetKey).build()
+
+ modelClient.getPoints(keys).flatMap { points =>
+ points.headOption match {
+ case None => Future.successful(None)
+ case Some(pt) =>
+ (procClient.getOverrides(keys) zip modelClient.getEntityKeyValues(Seq(keyPair))).map {
+ case (over, trigger) =>
+ Some((pt, over.headOption, trigger.headOption.flatMap(keyValueToTriggerSet)))
+ }
+ }
+ }
+
+ }
+
+ def subsForEndpoint(session: Session, endpointId: ModelUUID)(implicit executor: ExecutionContext): Future[(Subscription[PointNotification], Subscription[OverrideNotification], Subscription[EntityKeyValueNotification])] = {
+ val modelClient = ModelService.client(session)
+ val procClient = ProcessingService.client(session)
+
+ val pointSubQuery = PointSubscriptionQuery.newBuilder().addEndpointUuids(endpointId).build()
+ val overSubQuery = OverrideSubscriptionQuery.newBuilder().addEndpointUuids(endpointId).build()
+ val triggerSubQuery = EntityKeyValueSubscriptionQuery.newBuilder().addEndpointUuids(endpointId).build()
+
+ val pointSubFut = modelClient.subscribeToPoints(pointSubQuery)
+
+ val overSubFut = procClient.subscribeToOverrides(overSubQuery)
+
+ val triggerSubFut = modelClient.subscribeToEntityKeyValues(triggerSubQuery)
+
+ def cancelSub[A, B](result: (A, Subscription[B])) { result._2.cancel() }
+
+ pointSubFut.onFailure { case _ => triggerSubFut.foreach(cancelSub); overSubFut.foreach(cancelSub) }
+ overSubFut.onFailure { case _ => triggerSubFut.foreach(cancelSub); pointSubFut.foreach(cancelSub) }
+ triggerSubFut.onFailure { case _ => overSubFut.foreach(cancelSub); pointSubFut.foreach(cancelSub) }
+
+ (pointSubFut zip (overSubFut zip triggerSubFut)).map {
+ case ((_, pointSub), ((_, overSub), (_, triggerSub))) => (pointSub, overSub, triggerSub)
+ }
+ }
+
+ def currentSetsForEndpoint(session: Session, endpointId: ModelUUID)(implicit executor: ExecutionContext): Future[(Seq[Entity], Seq[MeasOverride], Seq[(ModelUUID, TriggerSet)])] = {
+
+ val modelClient = ModelService.client(session)
+ val procClient = ProcessingService.client(session)
+
+ val pageSize = Int.MaxValue // TODO: Breaks on large endpoints?
+
+ val pointQueryTemplate = EntityRelationshipFlatQuery.newBuilder()
+ .addStartUuids(endpointId)
+ .setRelationship("source")
+ .setDescendantOf(true)
+ .addEndTypes("Point")
+ .setPagingParams(
+ EntityPagingParams.newBuilder()
+ .setPageSize(pageSize))
+ .build()
+
+ val entFuture = modelClient.relationshipFlatQuery(pointQueryTemplate)
+
+ val overFut = entFuture.flatMap { pointEntities =>
+ if (pointEntities.isEmpty) {
+ Future.successful(Nil)
+ } else {
+ val pointSet = EntityKeySet.newBuilder().addAllUuids(pointEntities.map(_.getUuid)).build()
+ procClient.getOverrides(pointSet)
+ }
+ }
+
+ val triggerFut: Future[Seq[(ModelUUID, TriggerSet)]] = entFuture.flatMap { pointEntities =>
+ if (pointEntities.isEmpty) {
+ Future.successful(Nil)
+ } else {
+ val keyValues = pointEntities.map(e => EntityKeyPair.newBuilder().setUuid(e.getUuid).setKey(triggerSetKey).build())
+
+ modelClient.getEntityKeyValues(keyValues).map(_.flatMap(keyValueToUuidAndTriggerSet))
+ }
+ }
+
+ (entFuture zip (overFut zip triggerFut)).map {
+ case (ents, (overs, triggers)) => (ents, overs, triggers)
+ }
+ }
+
+ def configForEndpoint(session: Session, endpointId: ModelUUID)(implicit executor: ExecutionContext): Future[EndpointConfiguration] = {
+ val configSubFut = subsForEndpoint(session, endpointId)
+
+ val edgeSubFut = endpointEdgesSubscription(session, endpointId)
+
+ configSubFut.onFailure { case _ => edgeSubFut.foreach(_.cancel()) }
+ edgeSubFut.onFailure { case _ => configSubFut.foreach { case (pointSub, overSub, triggerSub) => pointSub.cancel(); overSub.cancel(); triggerSub.cancel() } }
+
+ (configSubFut zip edgeSubFut).flatMap {
+ case ((pointSub, overSub, triggerSub), edgeSub) =>
+
+ val currentFut = currentSetsForEndpoint(session, endpointId)
+
+ currentFut.onFailure { case _ => overSub.cancel(); triggerSub.cancel(); edgeSub.cancel() }
+
+ currentFut.map {
+ case (ents, overs, triggers) =>
+ EndpointConfiguration(ents.map(p => (p.getUuid, p.getName)), pointSub, overs, overSub, triggers, triggerSub, edgeSub)
+ }
+ }
+ }
+}
diff --git a/processing/src/main/scala/io/greenbus/measproc/json/JsonEndpointConfiguration.scala b/processing/src/main/scala/io/greenbus/measproc/json/JsonEndpointConfiguration.scala
new file mode 100644
index 0000000..6c652cc
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/json/JsonEndpointConfiguration.scala
@@ -0,0 +1,35 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.json
+
+object JsonEndpointConfiguration {
+ import play.api.libs.json._
+ implicit val writer = Json.writes[JsonEndpointConfiguration]
+ implicit val reader = Json.reads[JsonEndpointConfiguration]
+
+ def load(input: Array[Byte]): Option[JsonEndpointConfiguration] = {
+
+ val jsObj = Json.parse(input)
+
+ val jsonConfigOpt = Json.fromJson(jsObj)(JsonEndpointConfiguration.reader).asOpt
+
+ jsonConfigOpt
+ }
+}
+case class JsonEndpointConfiguration(endpointWhitelist: Option[Seq[String]])
\ No newline at end of file
diff --git a/processing/src/main/scala/io/greenbus/measproc/lock/ProcessingLockService.scala b/processing/src/main/scala/io/greenbus/measproc/lock/ProcessingLockService.scala
new file mode 100644
index 0000000..b7c47c7
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/lock/ProcessingLockService.scala
@@ -0,0 +1,75 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.lock
+
+import java.sql.SQLDataException
+import java.util.UUID
+
+import org.squeryl.PrimitiveTypeMode._
+import io.greenbus.services.data.{ ProcessingLockSchema, ProcessingLock }
+import ProcessingLockSchema._
+import org.squeryl.Session
+import io.greenbus.sql.DbConnection
+
+trait ProcessingLockService {
+ def acquireOrHold(uuid: UUID, nodeId: String, expiryDurationMs: Long): Boolean
+}
+
+object PostgresLockModel extends ProcessingLockService {
+
+ private def selectNow(): Long = {
+ val s = Session.currentSession
+
+ val st = s.connection.prepareStatement("select now()")
+ val rs = st.executeQuery()
+
+ if (!rs.next()) {
+ throw new SQLDataException("select now() failed")
+ }
+ rs.getTimestamp(1).getTime
+ }
+
+ def acquireOrHold(uuid: UUID, nodeId: String, expiryDurationMs: Long): Boolean = {
+
+ val current = processingLocks.where(r => r.id === uuid).forUpdate.toVector
+
+ val dbNow = selectNow()
+
+ current.headOption match {
+ case None =>
+ processingLocks.insert(ProcessingLock(uuid, nodeId, dbNow))
+ true
+ case Some(row) =>
+ if (row.nodeId == nodeId || dbNow - row.lastCheckIn > expiryDurationMs) {
+ processingLocks.update(ProcessingLock(uuid, nodeId, dbNow))
+ true
+ } else {
+ false
+ }
+ }
+ }
+}
+
+class TransactionPostgresLockModel(sql: DbConnection) extends ProcessingLockService {
+ def acquireOrHold(uuid: UUID, nodeId: String, expiryDurationMs: Long): Boolean = {
+ sql.transaction {
+ PostgresLockModel.acquireOrHold(uuid, nodeId, expiryDurationMs)
+ }
+ }
+}
diff --git a/processing/src/main/scala/io/greenbus/measproc/pipeline/ProcessingPipeline.scala b/processing/src/main/scala/io/greenbus/measproc/pipeline/ProcessingPipeline.scala
new file mode 100644
index 0000000..cbe0f85
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/pipeline/ProcessingPipeline.scala
@@ -0,0 +1,181 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.pipeline
+
+import io.greenbus.measproc.processing._
+import io.greenbus.client.service.proto.Measurements.Measurement
+import io.greenbus.jmx.MetricsManager
+import scala.annotation.tailrec
+import io.greenbus.client.service.proto.Processing.{ TriggerSet, MeasOverride }
+import io.greenbus.client.service.proto.EventRequests.EventTemplate
+import io.greenbus.client.service.proto.Model.ModelUUID
+
+class ProcessingPipeline(
+ endpointName: String,
+ pointKeys: Seq[String],
+ postMeasurements: Seq[(String, Measurement)] => Unit,
+ getMeasurement: String => Option[Measurement],
+ eventBatch: Seq[EventTemplate.Builder] => Unit,
+ triggerStateCache: ObjectCache[Boolean],
+ overrideValueCache: ObjectCache[(Option[Measurement], Option[Measurement])]) {
+
+ private val metricsMgr = MetricsManager("io.greenbus.measproc", endpointName)
+
+ private val lastValueCache = new MapCache[Measurement]
+
+ private var eventBuffer = List.empty[EventTemplate.Builder]
+
+ private val whitelistProcessor = new MeasurementWhiteList(pointKeys, metricsMgr.metrics("Whitelist"))
+
+ private val overrideProcessor = new OverrideProcessor(handleRestoreOnOverrideRemove, handleEngFromOverrides, handleTestReplaceValue, overrideValueCache, getCurrentValue, metricsMgr.metrics("Overrides"))
+
+ private val triggerFactory = new TriggerProcessingFactory(eventSink, lastValueCache)
+ private val triggerProcessor = new TriggerProcessor(triggerFactory, triggerStateCache, metricsMgr.metrics("Triggers"))
+
+ private val processingSteps = List(whitelistProcessor, overrideProcessor, triggerProcessor)
+
+ @tailrec
+ private def processSteps(key: String, m: Measurement, steps: List[ProcessingStep]): Option[Measurement] = {
+ steps match {
+ case Nil => Some(m)
+ case head :: tail =>
+ head.process(key, m) match {
+ case None => None
+ case Some(result) => processSteps(key, result, tail)
+ }
+ }
+ }
+
+ def modifyLastValues(measurements: Seq[(String, Measurement)]): Unit = {
+ measurements.foreach { case (key, m) => lastValueCache.put(key, m) }
+ }
+
+ def process(measurements: Seq[(String, Measurement)], batchTime: Option[Long]) {
+
+ val results = measurements.flatMap {
+ case (uuid, measurement) =>
+ processSteps(uuid.toString, measurement, processingSteps).map { result =>
+ (uuid, result)
+ }
+ }
+
+ lazy val now = System.currentTimeMillis()
+ val timeResolved = results.map {
+ case (key, m) =>
+ if (!m.hasTime) {
+ val time = batchTime getOrElse now
+ val withTime = Measurement.newBuilder().mergeFrom(m).setTime(time).build()
+ (key, withTime)
+ } else {
+ (key, m)
+ }
+ }
+
+ postMeasurements(timeResolved)
+ val events = popEvents()
+ eventBatch(events)
+ }
+
+ def updatePointsList(points: Seq[String]) {
+ whitelistProcessor.updatePointList(points.map(_.toString))
+ lastValueCache.reset()
+ }
+
+ def addOverride(over: MeasOverride) {
+ overrideProcessor.add(over)
+ }
+ def removeOverride(over: MeasOverride) {
+ overrideProcessor.remove(over)
+ }
+ def removeOverride(point: ModelUUID) {
+ overrideProcessor.remove(point)
+ }
+
+ def addTriggerSet(uuid: ModelUUID, set: TriggerSet) {
+ triggerProcessor.add(uuid, set)
+ }
+ def removeTriggerSet(point: ModelUUID) {
+ triggerProcessor.remove(point)
+ }
+
+ private def popEvents(): Seq[EventTemplate.Builder] = {
+ val events = eventBuffer.reverse
+ eventBuffer = List.empty[EventTemplate.Builder]
+ events
+ }
+
+ private def eventSink(event: EventTemplate.Builder) {
+ eventBuffer ::= event
+ }
+
+ private def getCurrentValue(pointKey: String): Option[Measurement] = {
+ getMeasurement(pointKey)
+ }
+
+ private def handleTestReplaceValue(pointKey: String, replaceValue: Measurement, previousValueOpt: Option[Measurement], alreadyReplaced: Boolean): Unit = {
+
+ val processedOpt = triggerProcessor.process(pointKey, replaceValue)
+
+ processedOpt match {
+ case None => {
+ if (!alreadyReplaced) {
+ previousValueOpt match {
+ case Some(prev) =>
+ postMeasurements(Seq((pointKey, OverrideProcessor.transformNIS(prev))))
+ case None =>
+ postMeasurements(Seq((pointKey, OverrideProcessor.blankNIS())))
+ }
+ }
+ }
+ case Some(processed) =>
+ postMeasurements(Seq((pointKey, OverrideProcessor.transformSubstituted(processed))))
+ }
+
+ val events = popEvents()
+ if (events.nonEmpty) {
+ eventBatch(events)
+ }
+ }
+
+ private def handleRestoreOnOverrideRemove(pointKey: String, previouslyPublishedOpt: Option[Measurement], latestUpdateOpt: Option[Measurement]) {
+
+ val latestProcessedOpt = latestUpdateOpt.flatMap(m => triggerProcessor.process(pointKey, m))
+
+ latestProcessedOpt match {
+ case None => {
+ // no update or update was suppressed, restore previously published
+ previouslyPublishedOpt.foreach(m => postMeasurements(Seq((pointKey, m))))
+ }
+ case Some(latestProcessed) => {
+ postMeasurements(Seq((pointKey, latestProcessed)))
+ }
+ }
+
+ // fire events even if suppressed
+ val events = popEvents()
+ if (events.nonEmpty) {
+ eventBatch(events)
+ }
+ }
+
+ private def handleEngFromOverrides(pointKey: String, measurement: Measurement) {
+ postMeasurements(Seq((pointKey, measurement)))
+ }
+
+}
diff --git a/processing/src/main/scala/io/greenbus/measproc/processing/Action.scala b/processing/src/main/scala/io/greenbus/measproc/processing/Action.scala
new file mode 100755
index 0000000..b6d4c69
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/processing/Action.scala
@@ -0,0 +1,95 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.processing
+
+import io.greenbus.client.service.proto.Measurements._
+
+object Action {
+ type Evaluation = (Measurement) => Measurement
+
+ sealed abstract class ActivationType {
+ def apply(state: Boolean, prev: Boolean) = activated(state, prev)
+ def activated(state: Boolean, prev: Boolean): Boolean
+ }
+ object High extends ActivationType {
+ def activated(state: Boolean, prev: Boolean) = state
+ }
+ object Low extends ActivationType {
+ def activated(state: Boolean, prev: Boolean) = !state
+ }
+ object Rising extends ActivationType {
+ def activated(state: Boolean, prev: Boolean) = (state && !prev)
+ }
+ object Falling extends ActivationType {
+ def activated(state: Boolean, prev: Boolean) = (!state && prev)
+ }
+ object Transition extends ActivationType {
+ def activated(state: Boolean, prev: Boolean) = (state && !prev) || (!state && prev)
+ }
+}
+
+/**
+ * Container for processor actions, maintains the logic necessary to determine if
+ * an action should be executed.
+ *
+ */
+trait Action {
+ /**
+ * Name of the action
+ */
+ val name: String
+
+ /**
+ * Conditional action evaluation. Actions define what combination of previous state and current state
+ * cause activation.
+ *
+ * @param m Measurement to be acted upon
+ * @param state Current state of the trigger condition this action is associated with
+ * @param prev Previous state of the trigger condition this action is associated with
+ * @return Result of processing
+ */
+ def process(m: Measurement, state: Boolean, prev: Boolean): Option[Measurement]
+}
+
+/**
+ * Implementation of action processing container.
+ */
+class BasicAction(val name: String, disabled: Boolean, activation: Action.ActivationType, eval: Action.Evaluation)
+ extends Action {
+
+ def process(m: Measurement, state: Boolean, prev: Boolean): Option[Measurement] = {
+ if (!disabled && activation(state, prev))
+ Some(eval(m))
+ else
+ Some(m)
+ }
+ override def toString = s"[BasicAction,disabled=$disabled,activation=$activation,eval=$eval]"
+}
+
+class SuppressAction(val name: String, disabled: Boolean, activation: Action.ActivationType) extends Action {
+
+ def process(m: Measurement, state: Boolean, prev: Boolean): Option[Measurement] = {
+ if (!disabled && activation(state, prev))
+ None
+ else
+ Some(m)
+ }
+ override def toString = s"[SuppressAction,disabled=$disabled,activation=$activation]"
+}
+
diff --git a/processing/src/main/scala/io/greenbus/measproc/processing/ActionFactory.scala b/processing/src/main/scala/io/greenbus/measproc/processing/ActionFactory.scala
new file mode 100755
index 0000000..0786903
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/processing/ActionFactory.scala
@@ -0,0 +1,206 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.processing
+
+import io.greenbus.client.service.proto.EventRequests.EventTemplate
+import io.greenbus.client.service.proto.Events._
+import io.greenbus.client.service.proto.Measurements._
+import io.greenbus.client.service.proto.Model.ModelUUID
+import io.greenbus.client.service.proto.Processing.{ Action => ActionProto, ActivationType => TypeProto, LinearTransform => LinearProto }
+
+import scala.collection.JavaConversions._
+
+/**
+ * Trigger/Action factory with constructor dependencies.
+ */
+class TriggerProcessingFactory(protected val publishEvent: EventTemplate.Builder => Unit, protected val lastCache: ObjectCache[Measurement])
+ extends ProcessingResources
+ with TriggerFactory
+ with ActionFactory
+
+/**
+ * Internal interface for Trigger/Action factory dependencies
+ */
+trait ProcessingResources {
+ protected val publishEvent: (EventTemplate.Builder) => Unit
+ protected val lastCache: ObjectCache[Measurement]
+}
+
+/**
+ * Factory for Action implementation objects
+ */
+trait ActionFactory { self: ProcessingResources =>
+ import io.greenbus.measproc.processing.Actions._
+ import io.greenbus.measproc.processing.TriggerFactory._
+
+ /**
+ * Creates Action objects given protobuf representation
+ * @param proto Action configuration proto
+ * @return Constructed action
+ */
+ def buildAction(proto: ActionProto, pointUuid: ModelUUID): Action = {
+ val name = proto.getActionName
+ val typ = convertActivation(proto.getType)
+ val disabled = proto.getDisabled
+
+ if (proto.hasSuppress && proto.getSuppress) {
+ new SuppressAction(name, disabled, typ)
+ } else {
+ val eval = if (proto.hasLinearTransform) {
+ new LinearTransform(proto.getLinearTransform.getScale, proto.getLinearTransform.getOffset, proto.getLinearTransform.getForceToDouble)
+ } else if (proto.hasQualityAnnotation) {
+ new AnnotateQuality(proto.getQualityAnnotation)
+ } else if (proto.hasStripValue && proto.getStripValue) {
+ new StripValue
+ } else if (proto.hasSetBool) {
+ new SetBool(proto.getSetBool)
+ } else if (proto.hasEvent) {
+ new EventGenerator(publishEvent, proto.getEvent.getEventType, pointUuid)
+ } else if (proto.hasBoolTransform) {
+ new BoolEnumTransformer(proto.getBoolTransform.getFalseString, proto.getBoolTransform.getTrueString)
+ } else if (proto.hasIntTransform) {
+ import scala.collection.JavaConversions._
+ val otherwiseValOpt = if (proto.getIntTransform.hasDefaultValue) Some(proto.getIntTransform.getDefaultValue) else None
+ val intMapping = proto.getIntTransform.getMappingsList.toList.map { vm => vm.getValue -> vm.getString }.toMap
+ new IntegerEnumTransformer(intMapping, otherwiseValOpt)
+ } else {
+ throw new Exception("Must specify at least one action")
+ }
+
+ new BasicAction(name, disabled, typ, eval)
+ }
+ }
+}
+
+object Actions {
+
+ class AnnotateQuality(qual: Quality) extends Action.Evaluation {
+ def apply(m: Measurement): Measurement = {
+ Measurement.newBuilder(m).setQuality(Quality.newBuilder(m.getQuality).mergeFrom(qual)).build
+ }
+ }
+ class StripValue extends Action.Evaluation {
+ def apply(m: Measurement): Measurement = {
+ Measurement.newBuilder(m)
+ .clearDoubleVal
+ .clearIntVal
+ .clearStringVal
+ .clearBoolVal
+ .setType(Measurement.Type.NONE)
+ .build
+ }
+ }
+ class SetBool(b: Boolean) extends Action.Evaluation {
+ def apply(m: Measurement): Measurement = {
+ Measurement.newBuilder(m)
+ .clearDoubleVal
+ .clearIntVal
+ .clearStringVal
+ .setBoolVal(b)
+ .setType(Measurement.Type.BOOL)
+ .build
+ }
+ }
+ class LinearTransform(scale: Double, offset: Double, forceToDouble: Boolean) extends Action.Evaluation {
+ def apply(m: Measurement): Measurement = {
+ m.getType match {
+ case Measurement.Type.DOUBLE =>
+ if (m.hasDoubleVal) Measurement.newBuilder(m).setDoubleVal(m.getDoubleVal * scale + offset).build else m
+ case Measurement.Type.INT =>
+ if (m.hasIntVal) {
+ val scaledValue = m.getIntVal * scale + offset
+ if (!forceToDouble) Measurement.newBuilder(m).setIntVal(scaledValue.toLong).build
+ else Measurement.newBuilder(m).setDoubleVal(scaledValue).setType(Measurement.Type.DOUBLE).build
+ } else m
+ case _ => m
+ }
+ }
+ }
+
+ private def attribute[A](name: String, obj: A): Attribute = {
+ val b = Attribute.newBuilder().setName(name)
+ obj match {
+ case v: Int => b.setValueSint64(v)
+ case v: Long => b.setValueSint64(v)
+ case v: Double => b.setValueDouble(v)
+ case v: Boolean => b.setValueBool(v)
+ case v: String => b.setValueString(v)
+ }
+ b.build()
+ }
+
+ class EventGenerator(out: EventTemplate.Builder => Unit, eventType: String, pointUuid: ModelUUID)
+ extends Action.Evaluation {
+
+ def apply(m: Measurement): Measurement = {
+
+ val validity = attribute("validity", m.getQuality.getValidity.toString)
+
+ val valueAttr = m.getType match {
+ case Measurement.Type.INT => Some(attribute("value", m.getIntVal))
+ case Measurement.Type.DOUBLE => Some(attribute("value", m.getDoubleVal))
+ case Measurement.Type.BOOL => Some(attribute("value", m.getBoolVal))
+ case Measurement.Type.STRING => Some(attribute("value", m.getStringVal))
+ case Measurement.Type.NONE => None
+ }
+
+ val attrs = Seq(validity) ++ Seq(valueAttr).flatten
+
+ val builder = EventTemplate.newBuilder
+ .setEventType(eventType)
+ .setEntityUuid(pointUuid)
+ .addAllArgs(attrs)
+ if (m.getIsDeviceTime) builder.setDeviceTime(m.getTime)
+ out(builder)
+ m
+ }
+ }
+
+ class BoolEnumTransformer(falseString: String, trueString: String) extends Action.Evaluation {
+ def apply(m: Measurement): Measurement = {
+ if (!m.hasBoolVal) {
+ // TODO: handle non boolean measurements in enum transform
+ m
+ } else {
+ m.toBuilder.setType(Measurement.Type.STRING)
+ .setStringVal(if (m.getBoolVal) trueString else falseString).build
+ }
+ }
+ }
+
+ // integer as in "not floating point", not integer as in 2^32, values on measurements are actually Longs
+ class IntegerEnumTransformer(mapping: Map[Long, String], otherWiseOpt: Option[String]) extends Action.Evaluation {
+ def apply(m: Measurement): Measurement = {
+
+ def buildStr(str: String) = m.toBuilder.setStringVal(str).setType(Measurement.Type.STRING).build
+
+ if (!m.hasIntVal) {
+ // TODO: handle non int measurements in int transform
+ m
+ } else {
+ mapping.get(m.getIntVal) match {
+ case Some(s) => buildStr(s)
+ case None => otherWiseOpt.map(buildStr).getOrElse(m)
+ }
+
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/processing/src/main/scala/io/greenbus/measproc/processing/FilterTrigger.scala b/processing/src/main/scala/io/greenbus/measproc/processing/FilterTrigger.scala
new file mode 100644
index 0000000..416351b
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/processing/FilterTrigger.scala
@@ -0,0 +1,110 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.processing
+
+import io.greenbus.client.service.proto.Measurements.{ DetailQual, Quality, Measurement }
+import io.greenbus.client.service.proto.Processing.{ Filter => FilterProto }
+import FilterProto.{ FilterType => Type }
+import io.greenbus.util.Optional._
+
+object FilterTrigger {
+
+ def apply(config: FilterProto, cache: ObjectCache[Measurement]) = {
+ val cond = config.getType match {
+ case Type.DUPLICATES_ONLY => new NoDuplicates
+ case Type.DEADBAND => new Deadband(optGet(config.hasDeadbandValue, config.getDeadbandValue).getOrElse(0))
+ }
+ new FilterTrigger(cache, cond)
+ }
+
+ sealed trait Filter {
+ def allow(m: Measurement, current: Measurement): Boolean
+ }
+
+ class NoDuplicates extends Filter {
+ def allow(m: Measurement, current: Measurement) = {
+ optGet(m.hasIntVal, m.getIntVal) != optGet(current.hasIntVal, current.getIntVal) ||
+ optGet(m.hasDoubleVal, m.getDoubleVal) != optGet(current.hasDoubleVal, current.getDoubleVal) ||
+ optGet(m.hasBoolVal, m.getBoolVal) != optGet(current.hasBoolVal, current.getBoolVal) ||
+ optGet(m.hasStringVal, m.getStringVal) != optGet(current.hasStringVal, current.getStringVal)
+ }
+ }
+
+ class Deadband(band: Double) extends Filter {
+ def allow(m: Measurement, current: Measurement) = {
+ if (m.hasDoubleVal && current.hasDoubleVal) {
+ math.abs(m.getDoubleVal - current.getDoubleVal) > band
+ } else if (m.hasIntVal && current.hasIntVal) {
+ math.abs(m.getIntVal - current.getIntVal).asInstanceOf[Double] > band
+ } else {
+ true
+ }
+ }
+ }
+
+ def sameDetailQuality(lhs: Option[DetailQual], rhs: Option[DetailQual]): Boolean = {
+ (lhs, rhs) match {
+ case (None, None) => true
+ case (Some(l), Some(r)) => sameDetailQuality(l, r)
+ case _ => false
+ }
+ }
+
+ def sameDetailQuality(lhs: DetailQual, rhs: DetailQual): Boolean = {
+ optGet(lhs.hasOverflow, lhs.getOverflow) == optGet(rhs.hasOverflow, rhs.getOverflow) &&
+ optGet(lhs.hasOutOfRange, lhs.getOutOfRange) == optGet(rhs.hasOutOfRange, rhs.getOutOfRange) &&
+ optGet(lhs.hasBadReference, lhs.getBadReference) == optGet(rhs.hasBadReference, rhs.getBadReference) &&
+ optGet(lhs.hasOscillatory, lhs.getOscillatory) == optGet(rhs.hasOscillatory, rhs.getOscillatory) &&
+ optGet(lhs.hasFailure, lhs.getFailure) == optGet(rhs.hasFailure, rhs.getFailure) &&
+ optGet(lhs.hasOldData, lhs.getOldData) == optGet(rhs.hasOldData, rhs.getOldData) &&
+ optGet(lhs.hasInconsistent, lhs.getInconsistent) == optGet(rhs.hasInconsistent, rhs.getInconsistent) &&
+ optGet(lhs.hasInaccurate, lhs.getInaccurate) == optGet(rhs.hasInaccurate, rhs.getInaccurate)
+ }
+
+ def sameQuality(lhs: Quality, rhs: Quality): Boolean = {
+ optGet(lhs.hasValidity, lhs.getValidity) == optGet(rhs.hasValidity, rhs.getValidity) &&
+ optGet(lhs.hasSource, lhs.getSource) == optGet(rhs.hasSource, rhs.getSource) &&
+ optGet(lhs.hasTest, lhs.getTest) == optGet(rhs.hasTest, rhs.getTest) &&
+ optGet(lhs.hasOperatorBlocked, lhs.getOperatorBlocked) == optGet(rhs.hasOperatorBlocked, rhs.getOperatorBlocked) &&
+ sameDetailQuality(optGet(lhs.hasDetailQual, lhs.getDetailQual), optGet(rhs.hasDetailQual, rhs.getDetailQual))
+ }
+}
+
+class FilterTrigger(cache: ObjectCache[Measurement], band: FilterTrigger.Filter) extends Trigger.KeyedCondition {
+ import FilterTrigger.sameQuality
+
+ def apply(pointKey: String, m: Measurement, prev: Boolean): Boolean = {
+ cache.get(pointKey) match {
+ case None => {
+ cache.put(pointKey, m)
+ true
+ }
+ case Some(current) => {
+ if (!sameQuality(m.getQuality, current.getQuality) ||
+ band.allow(m, current)) {
+
+ cache.put(pointKey, m)
+ true
+ } else {
+ false
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/processing/src/main/scala/io/greenbus/measproc/processing/MeasurementWhiteList.scala b/processing/src/main/scala/io/greenbus/measproc/processing/MeasurementWhiteList.scala
new file mode 100755
index 0000000..6f8082e
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/processing/MeasurementWhiteList.scala
@@ -0,0 +1,54 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.processing
+
+import io.greenbus.client.service.proto.Measurements.Measurement
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.jmx.Metrics
+
+/**
+ * checks to see if the measurements are on the whitelist provided with the endpoint and filters
+ * out the unexpected measurements and adds a log message indicating what is being ignored.
+ */
+class MeasurementWhiteList(expectedKeys: Seq[String], metrics: Metrics)
+ extends ProcessingStep with Logging {
+
+ private var whiteList: Set[String] = expectedKeys.toSet
+ private var ignored = Set.empty[String]
+
+ private val ignoredMeasurements = metrics.counter("ignoredMeasurements")
+
+ def process(pointKey: String, measurement: Measurement): Option[Measurement] = {
+ if (whiteList.contains(pointKey)) {
+ Some(measurement)
+ } else {
+ ignoredMeasurements(1)
+ if (!ignored.contains(pointKey)) {
+ ignored += pointKey
+ logger.info("Ignoring unexpected measurement: " + pointKey)
+ }
+ None
+ }
+ }
+
+ def updatePointList(expectedPoints: Seq[String]) {
+ whiteList = expectedPoints.toSet
+ ignored = Set.empty[String]
+ }
+}
\ No newline at end of file
diff --git a/processing/src/main/scala/io/greenbus/measproc/processing/ObjectCache.scala b/processing/src/main/scala/io/greenbus/measproc/processing/ObjectCache.scala
new file mode 100644
index 0000000..940aad8
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/processing/ObjectCache.scala
@@ -0,0 +1,51 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.processing
+
+trait ObjectCache[A] {
+
+ def put(values: Seq[(String, A)]): Unit = values.foreach { case (k, v) => put(k, v) }
+
+ def put(key: String, value: A): Unit
+
+ def get(name: String): Option[A]
+
+ def delete(name: String)
+}
+
+class MapCache[A] extends ObjectCache[A] {
+
+ private var map = Map.empty[String, A]
+
+ def put(key: String, value: A) {
+ map += (key -> value)
+ }
+
+ def get(name: String): Option[A] = {
+ map.get(name)
+ }
+
+ def delete(name: String) {
+ map -= name
+ }
+
+ def reset() {
+ map = Map.empty[String, A]
+ }
+}
\ No newline at end of file
diff --git a/processing/src/main/scala/io/greenbus/measproc/processing/OverrideProcessor.scala b/processing/src/main/scala/io/greenbus/measproc/processing/OverrideProcessor.scala
new file mode 100755
index 0000000..fd8dbce
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/processing/OverrideProcessor.scala
@@ -0,0 +1,178 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.processing
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.client.service.proto.Processing.MeasOverride
+import io.greenbus.client.service.proto.Measurements.{ DetailQual, Quality, Measurement }
+import io.greenbus.jmx.Metrics
+import io.greenbus.client.service.proto.Model.ModelUUID
+
+object OverrideProcessor {
+ def transformSubstituted(meas: Measurement): Measurement = {
+ val q = Quality.newBuilder(meas.getQuality).setSource(Quality.Source.SUBSTITUTED).setOperatorBlocked(true)
+ val now = System.currentTimeMillis
+ Measurement.newBuilder(meas).setQuality(q).setTime(now).setSystemTime(now).build
+ }
+
+ def transformNIS(meas: Measurement): Measurement = {
+ val dq = DetailQual.newBuilder(meas.getQuality.getDetailQual).setOldData(true)
+ val q = Quality.newBuilder(meas.getQuality).setDetailQual(dq).setOperatorBlocked(true)
+ val now = System.currentTimeMillis
+ Measurement.newBuilder(meas).setQuality(q).setTime(now).setSystemTime(now).build
+ }
+
+ def blankNIS(): Measurement = {
+ val dq = DetailQual.newBuilder().setOldData(true)
+ val q = Quality.newBuilder().setDetailQual(dq).setOperatorBlocked(true)
+ val now = System.currentTimeMillis
+ Measurement.newBuilder().setType(Measurement.Type.NONE).setQuality(q).setTime(now).setSystemTime(now).build
+ }
+}
+
+// TODO: OLD should not be set until a new field measurement comes in (61850-7-3).
+class OverrideProcessor(
+ handleRemove: (String, Option[Measurement], Option[Measurement]) => Unit,
+ publishEng: (String, Measurement) => Unit,
+ handleTest: (String, Measurement, Option[Measurement], Boolean) => Unit,
+ maskedValueCache: ObjectCache[(Option[Measurement], Option[Measurement])],
+ current: String => Option[Measurement],
+ metrics: Metrics)
+ extends ProcessingStep with Logging {
+
+ import OverrideProcessor._
+
+ private var blockedPointMap = scala.collection.immutable.Map[String, Option[Measurement]]()
+
+ private val measSupressed = metrics.counter("measSupressed")
+ private val overrideCurrentValueMiss = metrics.counter("overrideCurrentValueMiss")
+ private val overridenCacheMiss = metrics.counter("overridenCacheMiss")
+ private val overridesActive = metrics.gauge("overridesActive")
+
+ // cache is (published, latest subsequently arriving unpublished)
+ // if the later arrival is suppressed by triggers, the previously published value is restored
+
+ def process(pointKey: String, measurement: Measurement): Option[Measurement] = {
+ blockedPointMap.get(pointKey) match {
+ case None => Some(measurement)
+ case Some(_) => {
+ measSupressed(1)
+ maskedValueCache.get(pointKey) match {
+ case None => maskedValueCache.put(pointKey, (None, Some(measurement)))
+ case Some((publishedOpt, _)) => maskedValueCache.put(pointKey, (publishedOpt, Some(measurement)))
+ }
+ None
+ }
+ }
+ }
+
+ def add(over: MeasOverride) {
+ val pointUuid = over.getPointUuid
+ val pointKey = pointUuid.getValue
+ val currentlyNIS = blockedPointMap.contains(pointKey)
+ val replaceMeas = if (over.hasMeasurement) Some(over.getMeasurement) else None
+ val isTest = over.hasTestValue && over.getTestValue
+
+ logger.debug("Adding measurement override on: " + pointKey)
+
+ (currentlyNIS, replaceMeas) match {
+
+ // new NIS request, no replace specified
+ case (false, None) => {
+ val updateValue = cacheCurrent(pointKey).map(transformNIS).getOrElse(blankNIS())
+ blockedPointMap += (pointKey -> None)
+ publishEng(pointKey, updateValue)
+ }
+
+ // new NIS request, replace specified
+ case (false, Some(repl)) => {
+ val currentOpt = cacheCurrent(pointKey)
+ blockedPointMap += (pointKey -> replaceMeas)
+ if (isTest) {
+ handleTest(pointKey, repl, currentOpt, false)
+ } else {
+ publishEng(pointKey, transformSubstituted(repl))
+ }
+ }
+
+ // point already NIS, no replace specified
+ case (true, None) => logger.debug("NIS to point already NIS, ignoring")
+
+ // point already NIS, replace specified, treat as simple replace
+ case (true, Some(repl)) => {
+ blockedPointMap += (pointKey -> replaceMeas)
+ if (isTest) {
+ handleTest(pointKey, repl, None, true)
+ } else {
+ publishEng(pointKey, transformSubstituted(repl))
+ }
+ }
+ }
+
+ updateMetrics()
+ }
+
+ def remove(uuid: ModelUUID) {
+ val pointKey = uuid.getValue
+
+ logger.debug("Removing measurement override on: " + pointKey)
+
+ blockedPointMap -= pointKey
+ updateMetrics()
+ maskedValueCache.get(pointKey) match {
+ case None =>
+ overridenCacheMiss(1)
+ case Some((publishedOpt, latestOpt)) => {
+ val now = System.currentTimeMillis
+
+ val publishUpdatedOpt = publishedOpt.map(cached => Measurement.newBuilder(cached).setTime(now).build())
+ val latestUpdatedOpt = latestOpt.map(cached => Measurement.newBuilder(cached).setTime(now).build())
+
+ handleRemove(pointKey, publishUpdatedOpt, latestUpdatedOpt)
+
+ maskedValueCache.delete(pointKey)
+ }
+ }
+ }
+
+ def remove(over: MeasOverride) {
+ remove(over.getPointUuid)
+ }
+
+ def clear() {
+ blockedPointMap = scala.collection.immutable.Map[String, Option[Measurement]]()
+ updateMetrics()
+ }
+
+ private def updateMetrics() = {
+ overridesActive(blockedPointMap.size)
+ }
+
+ private def cacheCurrent(pointKey: String): Option[Measurement] = {
+ current(pointKey) match {
+ case Some(curr) =>
+ maskedValueCache.put(pointKey, (Some(curr), None))
+ Some(curr)
+ case None =>
+ overrideCurrentValueMiss(1)
+ None
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/processing/src/main/scala/io/greenbus/measproc/processing/ProcessingStep.scala b/processing/src/main/scala/io/greenbus/measproc/processing/ProcessingStep.scala
new file mode 100644
index 0000000..ed553f4
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/processing/ProcessingStep.scala
@@ -0,0 +1,25 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.processing
+
+import io.greenbus.client.service.proto.Measurements.Measurement
+
+trait ProcessingStep {
+ def process(pointKey: String, measurement: Measurement): Option[Measurement]
+}
diff --git a/processing/src/main/scala/io/greenbus/measproc/processing/Trigger.scala b/processing/src/main/scala/io/greenbus/measproc/processing/Trigger.scala
new file mode 100755
index 0000000..83a03c5
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/processing/Trigger.scala
@@ -0,0 +1,141 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.processing
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.client.service.proto.Measurements._
+import io.greenbus.measproc.processing.Trigger.{ KeyedCondition, SimpleCondition }
+import io.greenbus.util.Optional._
+
+object Trigger extends Logging {
+
+ /**
+ * Super-type for condition logic.
+ */
+ //type Condition = (String, Measurement, Boolean) => Boolean
+
+ sealed trait Condition
+ trait SimpleCondition extends Condition {
+ def apply(m: Measurement, b: Boolean): Boolean
+ }
+ trait KeyedCondition extends Condition {
+ def apply(key: String, m: Measurement, b: Boolean): Boolean
+ }
+
+ /**
+ * Helper function to extract analog values from measurements
+ * @param m Measurement
+ * @return Value in form of Double, or None
+ */
+ def analogValue(m: Measurement): Option[Double] = {
+ optGet(m.hasDoubleVal, m.getDoubleVal) orElse
+ optGet(m.hasIntVal, m.getIntVal.asInstanceOf[Double])
+ }
+
+ /**
+ * Evaluates a list of triggers against a measurement
+ * @param m Measurement input
+ * @param cache Previous trigger state cache
+ * @param triggers Triggers associated with the measurement point.
+ * @return Result of trigger/action processing.
+ */
+ def processAll(pointKey: String, m: Measurement, cache: ObjectCache[Boolean], triggers: List[Trigger]): Option[Measurement] = {
+ val r = triggers.foldLeft(m) { (meas, trigger) =>
+ // Evaluate trigger
+ logger.trace("Applying trigger: " + trigger + " to meas: " + meas)
+
+ trigger.process(pointKey, meas, cache) match {
+ case None => return None
+ case Some((result, true)) => return Some(result)
+ case Some((result, false)) => result
+ }
+ }
+ Some(r)
+ }
+
+}
+
+/**
+ * Interface for conditional measurement processing logic
+ */
+trait Trigger {
+ /**
+ * Conditionally process input measurement. May raise flag to stop further processing.
+ *
+ * @param pointKey Point of measurement
+ * @param m Input measurement.
+ * @param cache Previous trigger state cache.
+ * @return Measurement result, flag to stop further processing.
+ */
+ def process(pointKey: String, m: Measurement, cache: ObjectCache[Boolean]): Option[(Measurement, Boolean)]
+}
+
+/**
+ * Implementation of conditional measurement processing logic. Maintains a condition function
+ * and a list of actions to perform. Additionally may stop processing when condition is in a
+ * given state.
+ */
+class BasicTrigger(
+ cacheId: String,
+ conditions: List[Trigger.Condition],
+ actions: List[Action],
+ stopProcessing: Option[Action.ActivationType])
+ extends Trigger with Logging {
+
+ def process(pointKey: String, m: Measurement, cache: ObjectCache[Boolean]): Option[(Measurement, Boolean)] = {
+
+ // Get the previous state of this trigger
+ logger.trace("CacheId: " + cacheId + ", Prev cache: " + cache.get(cacheId))
+ val prev = cache.get(cacheId) getOrElse false
+
+ // Evaluate the current state
+ //info("Conditions: " + conditions)
+ val state = if (conditions.isEmpty) {
+ true
+ } else {
+ conditions.forall {
+ case cond: SimpleCondition => cond(m, prev)
+ case cond: KeyedCondition => cond(pointKey, m, prev)
+ }
+ }
+ //else conditions.forall(_(m, prev))
+
+ // Store the state in the previous state cache
+ cache.put(cacheId, state)
+
+ // Allow actions to determine if they should evaluate, roll up a result measurement
+ //info("Trigger state: " + state)
+
+ def evalActions(meas: Measurement, a: List[Action]): Option[Measurement] = a match {
+ case Nil => Some(meas)
+ case head :: tail => {
+ head.process(meas, state, prev).flatMap(next => evalActions(next, tail))
+ }
+ }
+
+ evalActions(m, actions).map { result =>
+ // Check stop processing flag (default to continue processing)
+ val stopProc = stopProcessing.map(_(state, prev)) getOrElse false
+ (result, stopProc)
+ }
+ }
+
+ override def toString = cacheId + " (" + actions.mkString(", ") + ")"
+}
+
diff --git a/processing/src/main/scala/io/greenbus/measproc/processing/TriggerFactory.scala b/processing/src/main/scala/io/greenbus/measproc/processing/TriggerFactory.scala
new file mode 100644
index 0000000..d738add
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/processing/TriggerFactory.scala
@@ -0,0 +1,200 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.processing
+
+import java.util.UUID
+
+import io.greenbus.client.service.proto.Measurements._
+import io.greenbus.client.service.proto.Model.ModelUUID
+import io.greenbus.client.service.proto.Processing.{ Action => ActionProto, ActivationType => TypeProto, AnalogLimit => AnalogLimitProto, Trigger => TriggerProto }
+import io.greenbus.util.Optional._
+
+import scala.collection.JavaConversions._
+
+object TriggerFactory {
+ import io.greenbus.measproc.processing.Triggers._
+
+ /**
+ * Converts proto activation type to scala activation type
+ * @param proto Proto activation type
+ * @return Scala activation type
+ */
+ def convertActivation(proto: TypeProto) = proto match {
+ case TypeProto.HIGH => Action.High
+ case TypeProto.LOW => Action.Low
+ case TypeProto.RISING => Action.Rising
+ case TypeProto.FALLING => Action.Falling
+ case TypeProto.TRANSITION => Action.Transition
+ }
+
+ /**
+ * Constructs either an upper,lower or, most commonly, range condition
+ * @param proto Proto configuration
+ * @return Trigger condition
+ */
+ def limitCondition(proto: AnalogLimitProto): Trigger.Condition = {
+ // TODO: do we need non-deadbanded limit checks, does deadband == 0 work for all floats
+ if (proto.hasLowerLimit && proto.hasUpperLimit) {
+ if (proto.hasDeadband)
+ new RangeLimitDeadband(proto.getUpperLimit, proto.getLowerLimit, proto.getDeadband)
+ else
+ new RangeLimit(proto.getUpperLimit, proto.getLowerLimit)
+ } else if (proto.hasUpperLimit()) {
+ if (proto.hasDeadband)
+ new UpperLimitDeadband(proto.getUpperLimit, proto.getDeadband)
+ else
+ new UpperLimit(proto.getUpperLimit)
+ } else if (proto.hasLowerLimit()) {
+ if (proto.hasDeadband)
+ new LowerLimitDeadband(proto.getLowerLimit, proto.getDeadband)
+ else
+ new LowerLimit(proto.getLowerLimit)
+ } else throw new IllegalArgumentException("Upper and Lower limit not set in AnalogLimit")
+ }
+}
+
+/**
+ * Factory for trigger implementation objects.
+ */
+trait TriggerFactory { self: ActionFactory with ProcessingResources =>
+ import io.greenbus.measproc.processing.TriggerFactory._
+ import io.greenbus.measproc.processing.Triggers._
+
+ /**
+ * Creates trigger objects given protobuf configuration.
+ * @param proto Configuration object
+ * @param pointKey Associated measurement point
+ * @return Implementation object
+ */
+ def buildTrigger(proto: TriggerProto, pointKey: String, pointUuid: ModelUUID): Trigger = {
+ val cacheId = UUID.randomUUID().toString //TODO: this won't work for persistent caches
+ val stopProc = proto.hasStopProcessingWhen thenGet convertActivation(proto.getStopProcessingWhen)
+ val conditions = List(
+ optGet(proto.hasAnalogLimit, proto.getAnalogLimit).map(limitCondition),
+ optGet(proto.hasQuality, proto.getQuality).map(new QualityCondition(_)),
+ optGet(proto.hasValueType, proto.getValueType).map(new TypeCondition(_)),
+ optGet(proto.hasBoolValue, proto.getBoolValue).map(new BoolValue(_)),
+ optGet(proto.hasIntValue, proto.getIntValue).map(new IntegerValue(_)),
+ optGet(proto.hasStringValue, proto.getStringValue).map(new StringValue(_)),
+ optGet(proto.hasFilter, proto.getFilter).map(FilterTrigger(_, lastCache))).flatten
+
+ val actions = proto.getActionsList.toList.map(buildAction(_, pointUuid))
+ new BasicTrigger(cacheId, conditions, actions, stopProc)
+ }
+}
+
+/**
+ * Implementations of corresponding proto Trigger types (see Triggers.proto)
+ */
+object Triggers {
+ class BoolValue(b: Boolean) extends Trigger.SimpleCondition {
+ def apply(m: Measurement, prev: Boolean): Boolean = {
+ (m.getType == Measurement.Type.BOOL) &&
+ (m.getBoolVal == b)
+ }
+ }
+
+ // integer as in "not floating point", not integer as in 2^32, values on measurements are actually Longs
+ class IntegerValue(i: Long) extends Trigger.SimpleCondition {
+ def apply(m: Measurement, prev: Boolean): Boolean = {
+ (m.getType == Measurement.Type.INT) &&
+ (m.getIntVal == i)
+ }
+ }
+
+ class StringValue(s: String) extends Trigger.SimpleCondition {
+ def apply(m: Measurement, prev: Boolean): Boolean = {
+ (m.getType == Measurement.Type.STRING) &&
+ (m.getStringVal == s)
+ }
+ }
+
+ class RangeLimit(upper: Double, lower: Double) extends Trigger.SimpleCondition {
+ def apply(m: Measurement, prev: Boolean): Boolean = {
+ val x = Trigger.analogValue(m) getOrElse { return false }
+ x <= lower || x >= upper
+ }
+ }
+
+ class RangeLimitDeadband(upper: Double, lower: Double, deadband: Double) extends Trigger.SimpleCondition {
+ def apply(m: Measurement, prev: Boolean): Boolean = {
+ val x = Trigger.analogValue(m) getOrElse { return false }
+ (prev && (x <= (lower + deadband) || x >= (upper - deadband))) || (!prev && (x <= lower || x >= upper))
+ }
+ }
+
+ class UpperLimitDeadband(limit: Double, deadband: Double) extends Trigger.SimpleCondition {
+ def apply(m: Measurement, prev: Boolean): Boolean = {
+ val x = Trigger.analogValue(m) getOrElse { return false }
+ (prev && x >= (limit - deadband)) || (!prev && x >= limit)
+ }
+ }
+ class UpperLimit(limit: Double) extends Trigger.SimpleCondition {
+ def apply(m: Measurement, prev: Boolean): Boolean = {
+ val x = Trigger.analogValue(m) getOrElse { return false }
+ x >= limit
+ }
+ }
+
+ class LowerLimitDeadband(limit: Double, deadband: Double) extends Trigger.SimpleCondition {
+ def apply(m: Measurement, prev: Boolean): Boolean = {
+ val x = Trigger.analogValue(m) getOrElse { return false }
+ (prev && x <= (limit + deadband)) || (!prev && x <= limit)
+ }
+ }
+ class LowerLimit(limit: Double) extends Trigger.SimpleCondition {
+ def apply(m: Measurement, prev: Boolean): Boolean = {
+ val x = Trigger.analogValue(m) getOrElse { return false }
+ x <= limit
+ }
+ }
+
+ def detailQualityMatches(lhs: DetailQual, rhs: DetailQual): Boolean = {
+ lhs.hasOverflow && rhs.hasOverflow && (lhs.getOverflow == rhs.getOverflow) ||
+ lhs.hasOutOfRange && rhs.hasOutOfRange && (lhs.getOutOfRange == rhs.getOutOfRange) ||
+ lhs.hasBadReference && rhs.hasBadReference && (lhs.getBadReference == rhs.getBadReference) ||
+ lhs.hasOscillatory && rhs.hasOscillatory && (lhs.getOscillatory == rhs.getOscillatory) ||
+ lhs.hasFailure && rhs.hasFailure && (lhs.getFailure == rhs.getFailure) ||
+ lhs.hasOldData && rhs.hasOldData && (lhs.getOldData == rhs.getOldData) ||
+ lhs.hasInconsistent && rhs.hasInconsistent && (lhs.getInconsistent == rhs.getInconsistent) ||
+ lhs.hasInaccurate && rhs.hasInaccurate && (lhs.getInaccurate == rhs.getInaccurate)
+ }
+
+ def qualityMatches(qual: Quality, q: Quality): Boolean = {
+ qual.hasValidity && q.hasValidity && (qual.getValidity == q.getValidity) ||
+ qual.hasSource && q.hasSource && (qual.getSource == q.getSource) ||
+ qual.hasTest && q.hasTest && (qual.getTest == q.getTest) ||
+ qual.hasOperatorBlocked && q.hasOperatorBlocked && (qual.getOperatorBlocked == q.getOperatorBlocked) ||
+ qual.hasDetailQual && q.hasDetailQual && detailQualityMatches(qual.getDetailQual, q.getDetailQual)
+ }
+
+ class QualityCondition(qual: Quality) extends Trigger.SimpleCondition {
+
+ def apply(m: Measurement, prev: Boolean): Boolean = {
+ qualityMatches(qual, m.getQuality)
+ }
+ }
+
+ class TypeCondition(typ: Measurement.Type) extends Trigger.SimpleCondition {
+ def apply(m: Measurement, prev: Boolean): Boolean = {
+ m.getType == typ
+ }
+ }
+}
+
diff --git a/processing/src/main/scala/io/greenbus/measproc/processing/TriggerProcessor.scala b/processing/src/main/scala/io/greenbus/measproc/processing/TriggerProcessor.scala
new file mode 100644
index 0000000..7592af2
--- /dev/null
+++ b/processing/src/main/scala/io/greenbus/measproc/processing/TriggerProcessor.scala
@@ -0,0 +1,78 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.processing
+
+import collection.JavaConversions._
+import collection.immutable
+
+import com.typesafe.scalalogging.slf4j.Logging
+
+import io.greenbus.client.service.proto.Measurements.Measurement
+import io.greenbus.client.service.proto.Processing.TriggerSet
+import io.greenbus.jmx.Metrics
+import io.greenbus.client.service.proto.Model.ModelUUID
+
+class TriggerProcessor(
+ protected val factory: TriggerFactory,
+ protected val stateCache: ObjectCache[Boolean],
+ metrics: Metrics)
+ extends ProcessingStep
+ with Logging {
+
+ protected var map = immutable.Map[String, List[Trigger]]()
+
+ private val triggersActive = metrics.gauge("triggersActive")
+
+ def process(pointKey: String, measurement: Measurement): Option[Measurement] = {
+ val triggerList = map.get(pointKey)
+
+ triggerList match {
+ case Some(triggers) =>
+ logger.trace("Applying triggers: " + triggers.size + " to meas: " + measurement)
+ val res = Trigger.processAll(pointKey, measurement, stateCache, triggers)
+ logger.trace("Trigger result: " + res)
+ res
+ case None =>
+ Some(measurement)
+ }
+ }
+
+ def add(uuid: ModelUUID, set: TriggerSet) {
+ logger.trace("TriggerSet received: " + set)
+ val pointKey = uuid.getValue
+ val trigList = set.getTriggersList.toList.map(proto => factory.buildTrigger(proto, pointKey, uuid))
+ map += (pointKey -> trigList)
+ updateMetrics()
+ }
+
+ def remove(uuid: ModelUUID) {
+ logger.trace("TriggerSet removed: " + uuid.getValue)
+ map -= uuid.getValue
+ updateMetrics()
+ }
+
+ def clear() {
+ map = immutable.Map[String, List[Trigger]]()
+ updateMetrics()
+ }
+
+ private def updateMetrics() {
+ triggersActive(map.size)
+ }
+}
diff --git a/processing/src/test/scala/io/greenbus/measproc/pipeline/ProcessingPipelineTest.scala b/processing/src/test/scala/io/greenbus/measproc/pipeline/ProcessingPipelineTest.scala
new file mode 100755
index 0000000..b2d8845
--- /dev/null
+++ b/processing/src/test/scala/io/greenbus/measproc/pipeline/ProcessingPipelineTest.scala
@@ -0,0 +1,339 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.pipeline
+
+import org.scalatest.FunSuite
+import org.scalatest.matchers.ShouldMatchers
+import org.scalatest.junit.JUnitRunner
+import org.junit.runner.RunWith
+
+import scala.collection.mutable
+
+import io.greenbus.client.service.proto.Measurements.{ Quality, Measurement }
+import io.greenbus.measproc.processing.MockObjectCache
+import io.greenbus.measproc.processing.ProtoHelper._
+import io.greenbus.client.service.proto.EventRequests.EventTemplate
+
+@RunWith(classOf[JUnitRunner])
+class ProcessingPipelineTest extends FunSuite with ShouldMatchers {
+
+ class TestRig {
+ val measQueue = mutable.Queue[(String, Measurement)]()
+ val eventQueue = mutable.Queue[EventTemplate.Builder]()
+ val measCache = new MockObjectCache[Measurement]
+ val overCache = new MockObjectCache[(Option[Measurement], Option[Measurement])]
+ val stateCache = new MockObjectCache[Boolean]
+
+ def publish(meases: Seq[(String, Measurement)]) {
+
+ }
+
+ val proc = new ProcessingPipeline(
+ "endpoint01",
+ List("meas01"),
+ measQueue.enqueue,
+ measCache.get,
+ eventQueue.enqueue,
+ stateCache,
+ overCache)
+
+ def process(key: String, m: Measurement) {
+ proc.process(Seq((key, m)), None)
+ }
+ }
+
+ test("Typical") {
+ val r = new TestRig
+ r.proc.addTriggerSet(fakeUuid("meas01"), triggerSet)
+
+ val m = makeAnalog(5.3, 0)
+ r.process("meas01", m)
+
+ r.measQueue.length should equal(1)
+ val (key, meas) = r.measQueue.dequeue
+ key should equal("meas01")
+ checkGood(meas)
+
+ r.eventQueue.length should equal(0)
+ r.stateCache.putQueue.length should equal(2)
+ }
+
+ test("Failure") {
+ val r = new TestRig
+ r.proc.addTriggerSet(fakeUuid("meas01"), triggerSet)
+
+ val m = makeAnalog(-5.3, 0)
+
+ // First bad RLC check
+ r.process("meas01", m)
+ r.measQueue.length should equal(1)
+ checkStripped(r.measQueue.dequeue._2)
+ r.eventQueue.length should equal(1)
+ r.eventQueue.dequeue.getEventType should equal("event01")
+
+ // Check second bad value doesn't generate event
+ r.process("meas01", m)
+ r.measQueue.length should equal(1)
+ checkStripped(r.measQueue.dequeue._2)
+ r.eventQueue.length should equal(0)
+
+ // Check deadband still bad
+ val m2 = makeAnalog(4.2, 0)
+ r.process("meas01", m2)
+ r.measQueue.length should equal(1)
+ checkStripped(r.measQueue.dequeue._2)
+ r.eventQueue.length should equal(0)
+
+ // Check return to normal
+ val m3 = makeAnalog(5.3, 0)
+ r.process("meas01", m3)
+ r.measQueue.length should equal(1)
+ checkGood(r.measQueue.dequeue._2)
+ r.eventQueue.length should equal(1)
+ r.eventQueue.dequeue.getEventType should equal("event02")
+ }
+
+ def checkPrevInPutQueue(r: TestRig, key: String, meas: Measurement): Unit = {
+ val (overKey, (pubMeasOpt, latestMeasOpt)) = r.overCache.putQueue.dequeue
+ pubMeasOpt.isEmpty should equal(false)
+ pubMeasOpt.get should equal(meas)
+ overKey should equal(key)
+ latestMeasOpt.isEmpty should equal(true)
+ }
+
+ def checkLatestInPutQueue(r: TestRig, key: String, published: Measurement, latest: Measurement): Unit = {
+ val (overKey, (pubMeasOpt, latestMeasOpt)) = r.overCache.putQueue.dequeue
+ overKey should equal(key)
+
+ pubMeasOpt.isEmpty should equal(false)
+ pubMeasOpt.get should equal(published)
+
+ latestMeasOpt.isEmpty should equal(false)
+ latestMeasOpt.get should equal(latest)
+ }
+
+ test("NIS") {
+ val r = new TestRig
+ r.proc.addTriggerSet(fakeUuid("meas01"), triggerSet)
+
+ val m = makeAnalog(5.3, 0)
+ r.process("meas01", m)
+
+ r.measQueue.length should equal(1)
+ val (key, meas) = r.measQueue.dequeue()
+ checkGood(meas)
+
+ r.measCache.put("meas01", meas)
+
+ r.proc.addOverride(makeNIS("meas01"))
+
+ {
+ r.overCache.putQueue.length should equal(1)
+ checkPrevInPutQueue(r, key, meas)
+ }
+
+ r.measQueue.length should equal(1)
+ val (nisedKey, nised) = r.measQueue.dequeue
+ nised.getQuality.getOperatorBlocked should equal(true)
+ nised.getQuality.getDetailQual.getOldData should equal(true)
+
+ val m2 = makeAnalog(48, 0)
+ r.process("meas01", m2)
+ r.measQueue.length should equal(0)
+
+ {
+ r.overCache.putQueue.length should equal(1)
+ checkLatestInPutQueue(r, key, meas, m2)
+ }
+
+ r.proc.removeOverride(makeNIS("meas01"))
+ r.measQueue.length should equal(1)
+ checkGood(r.measQueue.dequeue._2, 48)
+ r.overCache.delQueue.length should equal(1)
+ r.overCache.delQueue.dequeue should equal("meas01")
+ }
+
+ test("un-NIS same meas") {
+ val r = new TestRig
+ r.proc.addTriggerSet(fakeUuid("meas01"), triggerSet)
+
+ val m = makeAnalog(5.3, 0)
+ r.process("meas01", m)
+
+ r.measQueue.length should equal(1)
+ val (key, meas) = r.measQueue.dequeue()
+ checkGood(meas)
+
+ r.measCache.put("meas01", meas)
+
+ r.proc.addOverride(makeNIS("meas01"))
+
+ {
+ r.overCache.putQueue.length should equal(1)
+ checkPrevInPutQueue(r, key, meas)
+ }
+
+ r.measQueue.length should equal(1)
+ val (nisedKey, nised) = r.measQueue.dequeue
+ checkTransformedValue(nised, 5.3)
+ nised.getQuality.getOperatorBlocked should equal(true)
+ nised.getQuality.getDetailQual.getOldData should equal(true)
+
+ r.proc.removeOverride(makeNIS("meas01"))
+ r.measQueue.length should equal(1)
+ checkGood(r.measQueue.dequeue._2, 5.3)
+ r.overCache.delQueue.length should equal(1)
+ r.overCache.delQueue.dequeue should equal("meas01")
+ }
+
+ test("replace") {
+ val r = new TestRig
+ r.proc.addTriggerSet(fakeUuid("meas01"), triggerSet)
+
+ val m = makeAnalog(5.3, 0)
+ r.process("meas01", m)
+
+ r.measQueue.length should equal(1)
+ val (key, meas) = r.measQueue.dequeue()
+ checkGood(meas)
+
+ r.measCache.put("meas01", meas)
+
+ r.proc.addOverride(makeOverride("meas01", 33.0))
+
+ {
+ r.overCache.putQueue.length should equal(1)
+ checkPrevInPutQueue(r, key, meas)
+ }
+
+ r.measQueue.length should equal(1)
+ val (replacedKey, replaced) = r.measQueue.dequeue
+ replaced.getDoubleVal should equal(33.0)
+ replaced.getQuality.getOperatorBlocked should equal(true)
+ replaced.getQuality.getSource should equal(Quality.Source.SUBSTITUTED)
+
+ val m2 = makeAnalog(48, 0)
+ r.process("meas01", m2)
+ r.measQueue.length should equal(0)
+
+ {
+ r.overCache.putQueue.length should equal(1)
+ checkLatestInPutQueue(r, key, meas, m2)
+ }
+
+ r.proc.removeOverride(makeNIS("meas01"))
+ r.measQueue.length should equal(1)
+ checkGood(r.measQueue.dequeue._2, 48)
+ r.overCache.delQueue.length should equal(1)
+ r.overCache.delQueue.dequeue should equal("meas01")
+ }
+
+ test("un-replace same meas") {
+ val r = new TestRig
+ r.proc.addTriggerSet(fakeUuid("meas01"), triggerSet)
+
+ val m = makeAnalog(5.3, 0)
+ r.process("meas01", m)
+
+ r.measQueue.length should equal(1)
+ val (key, meas) = r.measQueue.dequeue()
+ checkGood(meas)
+
+ r.measCache.put("meas01", meas)
+
+ r.proc.addOverride(makeOverride("meas01", 33.0))
+
+ {
+ r.overCache.putQueue.length should equal(1)
+ checkPrevInPutQueue(r, key, meas)
+ }
+
+ r.measQueue.length should equal(1)
+ val (replacedKey, replaced) = r.measQueue.dequeue
+ replaced.getDoubleVal should equal(33.0)
+ replaced.getQuality.getOperatorBlocked should equal(true)
+ replaced.getQuality.getSource should equal(Quality.Source.SUBSTITUTED)
+
+ r.proc.removeOverride(makeNIS("meas01"))
+ r.measQueue.length should equal(1)
+ checkGood(r.measQueue.dequeue._2, 5.3)
+ r.overCache.delQueue.length should equal(1)
+ r.overCache.delQueue.dequeue should equal("meas01")
+ }
+
+ test("un-replace publishes event") {
+ val r = new TestRig
+ r.proc.addTriggerSet(fakeUuid("meas01"), triggerSet)
+
+ val m = makeAnalog(5.3, 0)
+ r.process("meas01", m)
+
+ r.measQueue.length should equal(1)
+ val (key, meas) = r.measQueue.dequeue()
+ checkGood(meas)
+
+ r.measCache.put("meas01", meas)
+
+ r.proc.addOverride(makeOverride("meas01", 33.0))
+
+ {
+ r.overCache.putQueue.length should equal(1)
+ checkPrevInPutQueue(r, key, meas)
+ }
+
+ r.measQueue.length should equal(1)
+ val (replacedKey, replaced) = r.measQueue.dequeue
+ replaced.getDoubleVal should equal(33.0)
+ replaced.getQuality.getOperatorBlocked should equal(true)
+ replaced.getQuality.getSource should equal(Quality.Source.SUBSTITUTED)
+
+ val m2 = makeAnalog(-3.0, 0)
+ r.process("meas01", m2)
+ r.measQueue.length should equal(0)
+
+ {
+ r.overCache.putQueue.length should equal(1)
+ checkLatestInPutQueue(r, key, meas, m2)
+ }
+
+ r.proc.removeOverride(makeNIS("meas01"))
+ r.overCache.delQueue.length should equal(1)
+ r.overCache.delQueue.dequeue should equal("meas01")
+
+ r.measQueue.length should equal(1)
+ checkStripped(r.measQueue.dequeue._2)
+ r.eventQueue.length should equal(1)
+ r.eventQueue.dequeue.getEventType should equal("event01")
+ }
+
+ def checkTransformedValue(m: Measurement, value: Double): Unit = {
+ m.getType should equal(Measurement.Type.DOUBLE)
+ m.getDoubleVal should equal(value * 10 + 50000)
+ }
+
+ def checkGood(m: Measurement, value: Double = 5.3) {
+ m.getType should equal(Measurement.Type.DOUBLE)
+ m.getDoubleVal should equal(value * 10 + 50000)
+ }
+ def checkStripped(m: Measurement) {
+ m.getType should equal(Measurement.Type.NONE)
+ m.hasDoubleVal should equal(false)
+ m.getQuality.getValidity should equal(Quality.Validity.QUESTIONABLE)
+ }
+}
diff --git a/processing/src/test/scala/io/greenbus/measproc/processing/ActionFrameworkTest.scala b/processing/src/test/scala/io/greenbus/measproc/processing/ActionFrameworkTest.scala
new file mode 100755
index 0000000..2db0a8f
--- /dev/null
+++ b/processing/src/test/scala/io/greenbus/measproc/processing/ActionFrameworkTest.scala
@@ -0,0 +1,102 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.processing
+
+import org.scalatest.FunSuite
+import org.scalatest.matchers.ShouldMatchers
+import org.scalatest.junit.JUnitRunner
+import org.junit.runner.RunWith
+
+import scala.collection.mutable
+import io.greenbus.client.service.proto.Measurements.Measurement
+import ProtoHelper._
+
+@RunWith(classOf[JUnitRunner])
+class ActionFrameworkTest extends FunSuite with ShouldMatchers {
+ import Action._
+
+ class TestRig {
+ val evalCalls = mutable.Queue[Measurement]()
+
+ def eval(ret: Measurement): Action.Evaluation = (m: Measurement) => {
+ evalCalls enqueue m
+ ret
+ }
+
+ def action(act: ActivationType, disabled: Boolean, ret: Measurement) =
+ new BasicAction("action01", disabled, act, eval(ret))
+ }
+
+ test("Disabled") {
+ val r = new TestRig
+ val input = makeAnalog(5.3)
+ val output = makeAnalog(5300.0)
+ val result = r.action(High, true, output).process(input, true, true).get
+ result should equal(input)
+ r.evalCalls.length should equal(0)
+ }
+
+ def scenario(state: Boolean, prev: Boolean, act: ActivationType, works: Boolean) = {
+ val r = new TestRig
+ val input = makeAnalog(5.3)
+ val output = makeAnalog(5300.0)
+ val result = r.action(act, false, output).process(input, state, prev).get
+ if (works) {
+ result should equal(output)
+ r.evalCalls.length should equal(1)
+ r.evalCalls.dequeue should equal(input)
+ } else {
+ result should equal(input)
+ r.evalCalls.length should equal(0)
+ }
+ }
+
+ def matrix(act: ActivationType, col: Tuple4[Boolean, Boolean, Boolean, Boolean]) = {
+ scenario(true, true, act, col._1)
+ scenario(true, false, act, col._2)
+ scenario(false, true, act, col._3)
+ scenario(false, false, act, col._4)
+ }
+
+ test("High") {
+ // prev: true false true false
+ // now: true true false false
+ matrix(High, (true, true, false, false))
+ }
+ test("Low") {
+ // prev: true false true false
+ // now: true true false false
+ matrix(Low, (false, false, true, true))
+ }
+ test("Rising") {
+ // prev: true false true false
+ // now: true true false false
+ matrix(Rising, (false, true, false, false))
+ }
+ test("Falling") {
+ // prev: true false true false
+ // now: true true false false
+ matrix(Falling, (false, false, true, false))
+ }
+ test("Transition") {
+ // prev: true false true false
+ // now: true true false false
+ matrix(Transition, (false, true, true, false))
+ }
+}
\ No newline at end of file
diff --git a/processing/src/test/scala/io/greenbus/measproc/processing/ActionsTest.scala b/processing/src/test/scala/io/greenbus/measproc/processing/ActionsTest.scala
new file mode 100755
index 0000000..81f39e0
--- /dev/null
+++ b/processing/src/test/scala/io/greenbus/measproc/processing/ActionsTest.scala
@@ -0,0 +1,61 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.processing
+
+import org.scalatest.matchers.ShouldMatchers
+import org.scalatest.FunSuite
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import ProtoHelper._
+
+@RunWith(classOf[JUnitRunner])
+class ActionsTest extends FunSuite with ShouldMatchers {
+
+ test("BoolTransform") {
+ val transformer = new Actions.BoolEnumTransformer("CLOSED", "OPEN")
+
+ transformer.apply(makeBool(true)).getStringVal should equal("OPEN")
+ transformer.apply(makeBool(false)).getStringVal should equal("CLOSED")
+
+ transformer.apply(makeInt(0)).getStringVal should equal("")
+ }
+
+ test("IntTransform") {
+ val map = List((-1).toLong -> "Disabled", 0.toLong -> "Searching").toMap
+ val transformer = new Actions.IntegerEnumTransformer(map, Some("Otherwise"))
+
+ transformer.apply(makeInt(-1)).getStringVal should equal("Disabled")
+ transformer.apply(makeInt(0)).getStringVal should equal("Searching")
+
+ transformer.apply(makeInt(10)).getStringVal should equal("Otherwise")
+ }
+
+ test("Suppression") {
+
+ val suppressor = new SuppressAction("testAction", false, Action.High)
+
+ val m = makeInt(5)
+
+ suppressor.process(m, false, false) should equal(Some(m))
+ suppressor.process(m, true, false) should equal(None)
+ suppressor.process(m, false, true) should equal(Some(m))
+ suppressor.process(m, true, true) should equal(None)
+
+ }
+}
\ No newline at end of file
diff --git a/processing/src/test/scala/io/greenbus/measproc/processing/FilterTriggerTest.scala b/processing/src/test/scala/io/greenbus/measproc/processing/FilterTriggerTest.scala
new file mode 100644
index 0000000..c449a63
--- /dev/null
+++ b/processing/src/test/scala/io/greenbus/measproc/processing/FilterTriggerTest.scala
@@ -0,0 +1,155 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.processing
+
+import java.util.UUID
+
+import org.scalatest.matchers.ShouldMatchers
+import org.scalatest.junit.JUnitRunner
+import org.junit.runner.RunWith
+import org.scalatest.FunSuite
+import io.greenbus.client.service.proto.Measurements.{ DetailQual, Quality, Measurement }
+import io.greenbus.client.service.proto.Measurements.Quality.Validity
+import io.greenbus.client.service.proto.Model.ModelUUID
+import io.greenbus.client.service.proto.Processing.{ Trigger => TriggerProto, Filter => FilterProto, Action => ActionProto, ActivationType }
+import ProtoHelper._
+import io.greenbus.client.service.proto.EventRequests.EventTemplate
+
+@RunWith(classOf[JUnitRunner])
+class FilterTriggerTest extends FunSuite with ShouldMatchers {
+
+ test("Factory") {
+ val cache = new MockObjectCache[Measurement]()
+ def publish(ev: EventTemplate.Builder): Unit = {}
+
+ val fac = new TriggerProcessingFactory(publish, cache)
+
+ val stateCache = new MockObjectCache[Boolean]
+
+ val proto = TriggerProto.newBuilder
+ .setTriggerName("testTrigger")
+ .setFilter(
+ FilterProto.newBuilder
+ .setType(FilterProto.FilterType.DUPLICATES_ONLY))
+ .addActions(
+ ActionProto.newBuilder
+ .setSuppress(true)
+ .setActionName("action01")
+ .setType(ActivationType.LOW))
+ .build
+
+ val trig = fac.buildTrigger(proto, "point01", ModelUUID.newBuilder().setValue(UUID.randomUUID().toString).build())
+
+ val m = makeInt(10)
+ trig.process("test01", m, stateCache) should equal(Some((m, false)))
+
+ trig.process("test01", m, stateCache) should equal(None)
+
+ }
+
+ class Fixture(band: FilterTrigger.Filter) {
+ val cache = new MockObjectCache[Measurement]()
+ val t = new FilterTrigger(cache, band)
+
+ def sendAndBlock(key: String, m: Measurement) {
+ t.apply(key, m, false) should equal(false)
+ cache.putQueue.size should equal(0)
+ }
+ def sendAndReceive(key: String, m: Measurement) {
+ t.apply(key, m, false) should equal(true)
+ cache.putQueue.size should equal(1)
+ cache.putQueue.dequeue should equal(key, m)
+ }
+ }
+
+ test("Duplicates first through") {
+ val f = new Fixture(new FilterTrigger.NoDuplicates)
+
+ val m = makeAnalog(4.234)
+
+ f.sendAndReceive("test01", m)
+
+ f.sendAndBlock("test01", m)
+ }
+
+ test("No duplicates") {
+ val f = new Fixture(new FilterTrigger.NoDuplicates)
+
+ val m = makeAnalog(4.234)
+
+ f.cache.update("test01", m)
+
+ f.sendAndBlock("test01", m)
+
+ f.sendAndReceive("test01", makeAnalog(3.3))
+ }
+
+ test("Duplicates") {
+ duplicateTest(makeAnalog(4.234))
+ duplicateTest(makeInt(10))
+ duplicateTest(makeBool(true))
+ duplicateTest(makeString("string"))
+ }
+
+ def duplicateTest(m: Measurement) {
+ val f = new Fixture(new FilterTrigger.NoDuplicates)
+ f.cache.update("test01", m)
+ f.sendAndBlock("test01", m)
+ }
+
+ test("Isolate quality") {
+ val f = new Fixture(new FilterTrigger.NoDuplicates)
+
+ val m = makeAnalog(4.234)
+ f.cache.update("test01", m)
+
+ val m2 = Measurement.newBuilder(m).setQuality(Quality.newBuilder.setDetailQual(DetailQual.newBuilder).setValidity(Validity.INVALID)).build
+ f.sendAndReceive("test01", m2)
+ }
+
+ test("Ints") {
+ val f = new Fixture(new FilterTrigger.Deadband(2))
+
+ val m = makeInt(10)
+
+ f.sendAndReceive("test01", m)
+
+ f.sendAndBlock("test01", makeInt(11))
+
+ f.sendAndBlock("test01", makeInt(12))
+
+ f.sendAndReceive("test01", makeInt(13))
+
+ f.sendAndReceive("test01", makeInt(10))
+ }
+
+ test("Double") {
+ val f = new Fixture(new FilterTrigger.Deadband(1.5))
+
+ f.sendAndReceive("test01", makeAnalog(10.01))
+
+ f.sendAndBlock("test01", makeAnalog(11.01))
+
+ f.sendAndBlock("test01", makeAnalog(11.51))
+
+ f.sendAndReceive("test01", makeAnalog(11.52))
+
+ f.sendAndReceive("test01", makeAnalog(9.99))
+ }
+}
\ No newline at end of file
diff --git a/processing/src/test/scala/io/greenbus/measproc/processing/MockObjectCache.scala b/processing/src/test/scala/io/greenbus/measproc/processing/MockObjectCache.scala
new file mode 100755
index 0000000..e0c1c05
--- /dev/null
+++ b/processing/src/test/scala/io/greenbus/measproc/processing/MockObjectCache.scala
@@ -0,0 +1,45 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.processing
+
+import scala.collection.mutable
+
+class MockObjectCache[A] extends ObjectCache[A] {
+
+ val delQueue = mutable.Queue[String]()
+ val putQueue = mutable.Queue[(String, A)]()
+
+ val map = mutable.Map[String, A]()
+
+ def update(name: String, obj: A) {
+ map.update(name, obj)
+ }
+
+ def put(name: String, obj: A) {
+ putQueue.enqueue((name, obj))
+ map.update(name, obj)
+ }
+ def get(name: String): Option[A] = {
+ map.get(name)
+ }
+ def delete(name: String) {
+ delQueue.enqueue(name)
+ map -= name
+ }
+}
\ No newline at end of file
diff --git a/processing/src/test/scala/io/greenbus/measproc/processing/OverrideProcessorTest.scala b/processing/src/test/scala/io/greenbus/measproc/processing/OverrideProcessorTest.scala
new file mode 100755
index 0000000..60fac52
--- /dev/null
+++ b/processing/src/test/scala/io/greenbus/measproc/processing/OverrideProcessorTest.scala
@@ -0,0 +1,246 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.processing
+
+import io.greenbus.client.service.proto.Measurements
+import io.greenbus.client.service.proto.Measurements.Measurement
+import io.greenbus.client.service.proto.Model.ModelUUID
+import io.greenbus.client.service.proto.Processing.MeasOverride
+import io.greenbus.jmx.{ Metrics, MetricsContainer }
+import io.greenbus.measproc.processing.ProtoHelper._
+import org.junit.runner.RunWith
+import org.scalatest.FunSuite
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.matchers.ShouldMatchers
+
+import scala.collection.mutable
+
+@RunWith(classOf[JUnitRunner])
+class OverrideProcessorTest extends FunSuite with ShouldMatchers {
+
+ class TestRig {
+
+ val measQueue = new mutable.Queue[(String, Measurement)]()
+
+ //val overCache = new MockObjectCache[(Measurement, Boolean)]
+ val overCache = new MockObjectCache[(Option[Measurement], Option[Measurement])]
+ val measCache = new MockObjectCache[Measurement]
+
+ val metrics = Metrics(MetricsContainer())
+
+ def publish(key: String, m: Measurement): Unit = {
+ measQueue.enqueue((key, m))
+ }
+ def handleRestore(key: String, prevPublishedOpt: Option[Measurement], latestOpt: Option[Measurement]): Unit = {
+ val vOpt = latestOpt orElse prevPublishedOpt
+ vOpt.foreach(m => measQueue.enqueue((key, m)))
+ }
+
+ def handleTestReplaceValue(pointKey: String, replaceValue: Measurement, previousValueOpt: Option[Measurement], alreadyReplaced: Boolean): Unit = {
+ }
+
+ val proc = new OverrideProcessor(handleRestore, publish, handleTestReplaceValue, overCache, measCache.get, metrics)
+
+ def configure(config: List[MeasOverride]) {
+ config.foreach(proc.add)
+ }
+
+ def sendAndCheckMeas(key: String, m: Measurement) {
+ proc.process(key, m) should equal(Some(m))
+ overCache.putQueue.length should equal(0)
+ }
+
+ def sendAndCheckOver(key: String, m: Measurement) {
+ proc.process(key, m) should equal(None)
+ val (qKey, (qPublishedOpt, qLatestOpt)) = overCache.putQueue.dequeue()
+ qKey should equal(key)
+ qLatestOpt.isEmpty should equal(false)
+ checkSame(m, qLatestOpt.get)
+ }
+ def receiveAndCheckMeas(key: String, orig: Measurement) {
+ val (qKey, qMeas) = measQueue.dequeue()
+ qKey should equal(key)
+ checkSameExceptTimeIsGreater(orig, qMeas)
+ }
+ def receiveAndCheckOver(key: String, pubOpt: Option[Measurement], latestOpt: Option[Measurement]) {
+ val (qKey, (qPublishedOpt, qLatestOpt)) = overCache.putQueue.dequeue()
+ qKey should equal(key)
+
+ checkOptMeas(pubOpt, qPublishedOpt)
+ checkOptMeas(latestOpt, qLatestOpt)
+ }
+ def checkNISPublished(key: String, orig: Measurement) {
+ val (qKey, qMeas) = measQueue.dequeue()
+ qKey should equal(key)
+ checkNIS(orig, qMeas)
+ }
+ def checkReplacePublished(key: String, repl: Measurement) {
+ val (qKey, qMeas) = measQueue.dequeue()
+ qKey should equal(key)
+ checkReplaced(repl, qMeas)
+ }
+
+ def checkOptMeas(aOpt: Option[Measurement], bOpt: Option[Measurement]): Unit = {
+ (aOpt, bOpt) match {
+ case (Some(a), Some(b)) => checkSame(a, b)
+ case (None, None) =>
+ case _ => assert(false)
+ }
+ }
+ }
+
+ def makeOverride(name: String): MeasOverride = makeOverride(name, 0)
+
+ def makeNIS(key: String) = {
+ MeasOverride.newBuilder.setPointUuid(ModelUUID.newBuilder().setValue(key)).build
+ }
+ def makeOverride(key: String, value: Double): MeasOverride = {
+ MeasOverride.newBuilder
+ .setPointUuid(ModelUUID.newBuilder().setValue(key))
+ .setMeasurement(Measurement.newBuilder
+ .setTime(85)
+ .setType(Measurement.Type.DOUBLE)
+ .setDoubleVal(value)
+ .setQuality(Measurements.Quality.newBuilder.setDetailQual(Measurements.DetailQual.newBuilder)))
+ .build
+ }
+
+ def checkSame(m: Measurement, meas: Measurement): Unit = {
+ m.getType should equal(meas.getType)
+ m.getDoubleVal should equal(meas.getDoubleVal)
+ m.getTime should equal(meas.getTime)
+ m.getQuality should equal(meas.getQuality)
+ }
+
+ def checkSameExceptTimeIsGreater(orig: Measurement, meas: Measurement): Unit = {
+ meas.getType should equal(orig.getType)
+ meas.getDoubleVal should equal(orig.getDoubleVal)
+ meas.getQuality should equal(orig.getQuality)
+ meas.getTime should be >= (orig.getTime)
+ }
+
+ def sameExceptQualityAndTimeIsGreater(orig: Measurement, pub: Measurement): Unit = {
+ pub.getType should equal(orig.getType)
+ pub.getDoubleVal should equal(orig.getDoubleVal)
+ pub.getTime should be >= (orig.getTime)
+ }
+ def checkNIS(orig: Measurement, pub: Measurement): Unit = {
+ sameExceptQualityAndTimeIsGreater(orig, pub)
+ pub.getQuality.getDetailQual.getOldData should equal(true)
+ pub.getQuality.getOperatorBlocked should equal(true)
+ }
+
+ def checkReplaced(repl: Measurement, pub: Measurement): Unit = {
+ sameExceptQualityAndTimeIsGreater(repl, pub)
+ pub.getQuality.getDetailQual.getOldData should equal(false)
+ pub.getQuality.getSource should equal(Measurements.Quality.Source.SUBSTITUTED)
+ }
+
+ test("NullOverride") {
+ val r = new TestRig
+ r.configure(Nil)
+ r.sendAndCheckMeas("meas01", makeAnalog(5.3))
+ }
+
+ test("Preconfig") {
+ val r = new TestRig
+ val config = List(makeOverride("meas01", 89))
+ r.configure(config)
+ r.checkReplacePublished("meas01", config(0).getMeasurement)
+ r.sendAndCheckOver("meas01", makeAnalog(5.3))
+ }
+
+ test("Multiple") {
+ val config = List(makeOverride("meas01", 89), makeOverride("meas02", 44))
+ val r = new TestRig
+ r.configure(config)
+
+ r.checkReplacePublished("meas01", config(0).getMeasurement)
+ r.checkReplacePublished("meas02", config(1).getMeasurement)
+ r.sendAndCheckOver("meas01", makeAnalog(5.3))
+ r.sendAndCheckOver("meas02", makeAnalog(5.3))
+ r.sendAndCheckMeas("meas03", makeAnalog(5.3))
+ }
+
+ test("NISWithReplace") {
+ val r = new TestRig
+ r.configure(Nil)
+ val orig = makeAnalog(5.3)
+ r.measCache.update("meas01", orig)
+ val over = makeOverride("meas01", 89)
+ r.proc.add(over)
+ r.checkReplacePublished("meas01", over.getMeasurement)
+ r.receiveAndCheckOver("meas01", Some(orig), None)
+ r.sendAndCheckOver("meas01", makeAnalog(44))
+ }
+
+ test("NISThenReplace") {
+ val r = new TestRig
+ r.configure(Nil)
+ val orig = makeAnalog(5.3)
+ r.measCache.update("meas01", orig)
+ val nis = makeNIS("meas01")
+ r.proc.add(nis)
+ r.checkNISPublished("meas01", orig)
+ r.receiveAndCheckOver("meas01", Some(orig), None)
+
+ val replace = makeOverride("meas01", 89)
+ r.proc.add(replace)
+ r.checkReplacePublished("meas01", replace.getMeasurement)
+
+ r.sendAndCheckOver("meas01", makeAnalog(44))
+ }
+
+ test("DoubleAdd") {
+ val r = new TestRig
+ r.configure(Nil)
+
+ val orig = makeAnalog(5.3)
+ r.measCache.update("meas01", orig)
+ val over = makeOverride("meas01", 89)
+ r.proc.add(over)
+ r.checkReplacePublished("meas01", over.getMeasurement)
+ r.receiveAndCheckOver("meas01", Some(orig), None)
+ r.sendAndCheckOver("meas01", makeAnalog(44))
+
+ // On the second add, we shouldn't override the cached "real" value
+ val over2 = makeOverride("meas01", 55)
+ r.proc.add(over2)
+ r.checkReplacePublished("meas01", over2.getMeasurement)
+ r.overCache.putQueue.length should equal(0)
+ r.sendAndCheckOver("meas01", makeAnalog(23))
+ }
+
+ test("Removed") {
+ val config = List(makeOverride("meas01", 89))
+ val r = new TestRig
+ r.configure(config)
+
+ r.checkReplacePublished("meas01", config(0).getMeasurement)
+ val orig = makeAnalog(5.3)
+ r.overCache.update("meas01", (Some(orig), None))
+ val nisRemove = makeNIS("meas01")
+ r.proc.remove(nisRemove)
+ r.receiveAndCheckMeas("meas01", orig)
+ r.overCache.delQueue.length should equal(1)
+ r.overCache.delQueue.dequeue should equal("meas01")
+ r.sendAndCheckMeas("meas01", makeAnalog(33))
+ }
+}
+
diff --git a/processing/src/test/scala/io/greenbus/measproc/processing/ProtoHelper.scala b/processing/src/test/scala/io/greenbus/measproc/processing/ProtoHelper.scala
new file mode 100755
index 0000000..8b61d65
--- /dev/null
+++ b/processing/src/test/scala/io/greenbus/measproc/processing/ProtoHelper.scala
@@ -0,0 +1,154 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.processing
+
+import io.greenbus.client.service.proto.Model.{ ModelUUID, Entity }
+import io.greenbus.client.service.proto.Measurements.{ Quality, DetailQual, Measurement }
+import io.greenbus.client.service.proto.Processing.{ TriggerSet, Trigger => ProtoTrigger, Action => ProtoAction, MeasOverride }
+
+object ProtoHelper {
+
+ def makeAnalog(value: Double, time: Long = System.currentTimeMillis): Measurement = {
+ val m = Measurement.newBuilder
+ m.setTime(time)
+ m.setSystemTime(time)
+ m.setType(Measurement.Type.DOUBLE)
+ m.setDoubleVal(value)
+ m.setQuality(makeNominalQuality)
+ m.build
+ }
+
+ def makeInt(value: Long, time: Long = System.currentTimeMillis): Measurement = {
+ Measurement.newBuilder
+ .setTime(time)
+ .setSystemTime(time)
+ .setType(Measurement.Type.INT)
+ .setIntVal(value)
+ .setQuality(makeNominalQuality)
+ .build
+
+ }
+ def makeBool(value: Boolean, time: Long = System.currentTimeMillis): Measurement = {
+ Measurement.newBuilder
+ .setTime(time)
+ .setSystemTime(time)
+ .setType(Measurement.Type.BOOL)
+ .setBoolVal(value)
+ .setQuality(makeNominalQuality)
+ .build
+
+ }
+
+ def makeString(value: String, time: Long = System.currentTimeMillis): Measurement = {
+ Measurement.newBuilder
+ .setTime(time)
+ .setSystemTime(time)
+ .setType(Measurement.Type.STRING)
+ .setStringVal(value)
+ .setQuality(makeNominalQuality)
+ .build
+ }
+
+ def makeNominalQuality() = {
+ Quality.newBuilder.setDetailQual(DetailQual.newBuilder).build
+ }
+
+ def makeAbnormalQuality() = {
+ Quality.newBuilder.setDetailQual(DetailQual.newBuilder).setValidity(Quality.Validity.INVALID).build
+ }
+
+ def updateQuality(m: Measurement, q: Quality): Measurement = {
+ m.toBuilder.setQuality(q).build
+ }
+
+ def makeNodeById(nodeId: String): Entity = {
+ Entity.newBuilder.setUuid(ModelUUID.newBuilder.setValue(nodeId)).build
+ }
+
+ def makeNodeById(nodeId: ModelUUID): Entity = {
+ Entity.newBuilder.setUuid(nodeId).build
+ }
+
+ def makeNodeByName(name: String): Entity = {
+ Entity.newBuilder.setName(name).build
+ }
+
+ def makeNIS(key: String) = {
+ MeasOverride.newBuilder.setPointUuid(ModelUUID.newBuilder().setValue(key)).build
+ }
+ def makeOverride(key: String, value: Double): MeasOverride = {
+ MeasOverride.newBuilder
+ .setPointUuid(ModelUUID.newBuilder().setValue(key))
+ .setMeasurement(Measurement.newBuilder
+ .setTime(85)
+ .setSystemTime(85)
+ .setType(Measurement.Type.DOUBLE)
+ .setDoubleVal(value)
+ .setQuality(Quality.getDefaultInstance))
+ .build
+ }
+
+ def fakeUuid(str: String) = ModelUUID.newBuilder().setValue(str).build()
+
+ def triggerSet: TriggerSet = {
+ TriggerSet.newBuilder
+ .addTriggers(triggerRlcLow("meas01"))
+ .addTriggers(triggerTransformation("meas01"))
+ .build
+ }
+ def triggerRlcLow(measName: String): ProtoTrigger = {
+ import io.greenbus.client.service.proto.Processing._
+ ProtoTrigger.newBuilder
+ .setTriggerName("rlclow")
+ .setStopProcessingWhen(ActivationType.HIGH)
+ .setAnalogLimit(AnalogLimit.newBuilder.setLowerLimit(0).setDeadband(5))
+ .addActions(
+ ProtoAction.newBuilder
+ .setActionName("strip")
+ .setType(ActivationType.HIGH)
+ .setStripValue(true))
+ .addActions(
+ ProtoAction.newBuilder
+ .setActionName("qual")
+ .setType(ActivationType.HIGH)
+ .setQualityAnnotation(Quality.newBuilder.setValidity(Quality.Validity.QUESTIONABLE)))
+ .addActions(
+ ProtoAction.newBuilder
+ .setActionName("eventrise")
+ .setType(ActivationType.RISING)
+ .setEvent(EventGeneration.newBuilder.setEventType("event01")))
+ .addActions(
+ ProtoAction.newBuilder
+ .setActionName("eventfall")
+ .setType(ActivationType.FALLING)
+ .setEvent(EventGeneration.newBuilder.setEventType("event02")))
+ .build
+ }
+ def triggerTransformation(measName: String): ProtoTrigger = {
+ import io.greenbus.client.service.proto.Processing._
+ ProtoTrigger.newBuilder
+ .setTriggerName("trans")
+ .addActions(
+ ProtoAction.newBuilder
+ .setActionName("linear")
+ .setType(ActivationType.HIGH)
+ .setLinearTransform(LinearTransform.newBuilder.setScale(10).setOffset(50000)))
+ .build
+ }
+}
diff --git a/processing/src/test/scala/io/greenbus/measproc/processing/TriggerFrameworkTest.scala b/processing/src/test/scala/io/greenbus/measproc/processing/TriggerFrameworkTest.scala
new file mode 100755
index 0000000..83b2fdd
--- /dev/null
+++ b/processing/src/test/scala/io/greenbus/measproc/processing/TriggerFrameworkTest.scala
@@ -0,0 +1,178 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.processing
+
+import org.scalatest.FunSuite
+import org.scalatest.matchers.ShouldMatchers
+import org.scalatest.junit.JUnitRunner
+import org.junit.runner.RunWith
+import ProtoHelper._
+
+import scala.collection.mutable
+import io.greenbus.client.service.proto.Measurements.Measurement
+
+@RunWith(classOf[JUnitRunner])
+class TriggerFrameworkTest extends FunSuite with ShouldMatchers {
+
+ class TestRig {
+ val triggerCalls = mutable.Queue[Measurement]()
+ val conditionCalls = mutable.Queue[(Measurement, Boolean)]()
+ val actionCalls = mutable.Queue[(Measurement, Boolean, Boolean)]()
+
+ def trigger(ret: Measurement, stop: Boolean): Trigger = new MockTrigger(ret, stop)
+ class MockTrigger(ret: Measurement, stop: Boolean) extends Trigger {
+ def process(key: String, m: Measurement, cache: ObjectCache[Boolean]) = {
+ triggerCalls enqueue m
+ Some((ret, stop))
+ }
+ }
+
+ def condition(ret: Boolean): Trigger.Condition = new MockCondition(ret)
+ class MockCondition(ret: Boolean) extends Trigger.SimpleCondition {
+ def apply(m: Measurement, prev: Boolean): Boolean = {
+ conditionCalls.enqueue((m, prev))
+ ret
+ }
+ }
+ def action(ret: Measurement): Action = new MockAction(ret)
+ class MockAction(ret: Measurement, var disabled: Boolean = false, val name: String = "action01") extends Action {
+ def process(m: Measurement, state: Boolean, prev: Boolean): Option[Measurement] = {
+ actionCalls.enqueue((m, state, prev))
+ Some(ret)
+ }
+ }
+
+ val cache = new MockObjectCache[Boolean]
+ }
+
+ test("Test Cache Lookup") {
+ val r = new TestRig
+
+ val input = makeAnalog(5.3)
+ val trigger = new BasicTrigger("meas01.trig01", List(r.condition(true)), Nil, None)
+ r.cache.update("meas01.trig01", true)
+ val (result, stop) = trigger.process("meas01", input, r.cache).get
+
+ r.conditionCalls.length should equal(1)
+ r.conditionCalls.dequeue should equal((input, true))
+ r.cache.putQueue.length should equal(1)
+ r.cache.putQueue.dequeue should equal(("meas01.trig01", true))
+ }
+
+ test("testMultipleTriggers") {
+ val r = new TestRig
+
+ val input = makeAnalog(5.3)
+ val output1 = makeAnalog(5300.0)
+ val output2 = makeAnalog(0.053)
+
+ val triggers = List(r.trigger(output1, false), r.trigger(output2, false))
+
+ val result = Trigger.processAll("meas01", input, r.cache, triggers).get
+
+ r.triggerCalls.length should equal(2)
+ r.triggerCalls.dequeue should equal(input)
+ r.triggerCalls.dequeue should equal(output1)
+ result should equal(output2)
+ }
+
+ test("testMultipleTriggerShortCircuit") {
+ val r = new TestRig
+
+ val input = makeAnalog(5.3)
+ val output1 = makeAnalog(5300.0)
+ val output2 = makeAnalog(0.053)
+
+ val triggers = List(r.trigger(output1, true), r.trigger(output2, false))
+
+ val result = Trigger.processAll("meas01", input, r.cache, triggers).get
+
+ r.triggerCalls.length should equal(1)
+ r.triggerCalls.dequeue should equal(input)
+ result should equal(output1)
+ }
+
+ test("testMultipleConditions") {
+ val r = new TestRig
+
+ val input = makeAnalog(5.3)
+ val output = makeAnalog(5300.0)
+ val trigger = new BasicTrigger("meas01.trig01", List(r.condition(true), r.condition(true)), List(r.action(output)), None)
+ val (result, stop) = trigger.process("meas01", input, r.cache).get
+
+ r.conditionCalls.length should equal(2)
+ r.conditionCalls.dequeue should equal((input, false))
+ r.conditionCalls.dequeue should equal((input, false))
+
+ r.actionCalls.length should equal(1)
+
+ result should equal(output)
+ }
+ test("testMultipleConditionAnd") {
+ val r = new TestRig
+
+ val input = makeAnalog(5.3)
+ val output = makeAnalog(5300.0)
+ val trigger = new BasicTrigger("meas01.trig01", List(r.condition(true), r.condition(false)), List(r.action(output)), None)
+ val (result, stop) = trigger.process("meas01", input, r.cache).get
+
+ r.conditionCalls.length should equal(2)
+ r.conditionCalls.dequeue should equal((input, false))
+ r.conditionCalls.dequeue should equal((input, false))
+
+ r.actionCalls.length should equal(1)
+ r.actionCalls.dequeue should equal((input, false, false))
+ }
+
+ test("testMultipleConditionShortCircuit") {
+ val r = new TestRig
+
+ val input = makeAnalog(5.3)
+ val output = makeAnalog(5300.0)
+ val trigger = new BasicTrigger("meas01.trig01", List(r.condition(false), r.condition(true)), List(r.action(output)), None)
+ val (result, stop) = trigger.process("meas01", input, r.cache).get
+
+ r.conditionCalls.length should equal(1)
+ r.conditionCalls.dequeue should equal((input, false))
+
+ r.actionCalls.length should equal(1)
+ r.actionCalls.dequeue should equal((input, false, false))
+ }
+
+ test("testMultipleActions") {
+ val r = new TestRig
+
+ val input = makeAnalog(5.3)
+ val output1 = makeAnalog(5300.0)
+ val output2 = makeAnalog(0.053)
+
+ val trigger = new BasicTrigger("meas01.trig01", List(r.condition(true)), List(r.action(output1), r.action(output2)), None)
+ val (result, stop) = trigger.process("meas01", input, r.cache).get
+
+ r.conditionCalls.length should equal(1)
+ r.conditionCalls.dequeue should equal((input, false))
+ r.cache.putQueue.length should equal(1)
+ r.cache.putQueue.dequeue should equal(("meas01.trig01", true))
+
+ r.actionCalls.length should equal(2)
+ r.actionCalls.dequeue should equal((input, true, false))
+ r.actionCalls.dequeue should equal((output1, true, false))
+ result should equal(output2)
+ }
+}
diff --git a/processing/src/test/scala/io/greenbus/measproc/processing/WhitelistTest.scala b/processing/src/test/scala/io/greenbus/measproc/processing/WhitelistTest.scala
new file mode 100755
index 0000000..433e24f
--- /dev/null
+++ b/processing/src/test/scala/io/greenbus/measproc/processing/WhitelistTest.scala
@@ -0,0 +1,47 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.measproc.processing
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.FunSuite
+import org.scalatest.matchers.ShouldMatchers
+import scala.collection.mutable
+import ProtoHelper._
+import io.greenbus.jmx.{ MetricsContainer, Metrics }
+
+@RunWith(classOf[JUnitRunner])
+class WhitelistTest extends FunSuite with ShouldMatchers {
+ test("Ignores meases") {
+ val metrics = Metrics(MetricsContainer())
+
+ val filter = new MeasurementWhiteList(List("ok1", "ok2"), metrics)
+
+ val m = makeAnalog(100)
+
+ filter.process("ok1", m) should equal(Some(m))
+ filter.process("ok2", m) should equal(Some(m))
+
+ filter.process("bad1", m) should equal(None)
+ filter.process("bad2", m) should equal(None)
+ filter.process("bad1", m) should equal(None)
+
+ filter.process("ok1", m) should equal(Some(m))
+ }
+}
\ No newline at end of file
diff --git a/protoc-gen-msgdoc b/protoc-gen-msgdoc
new file mode 100755
index 0000000..5ae0cea
--- /dev/null
+++ b/protoc-gen-msgdoc
@@ -0,0 +1,5 @@
+#! /bin/sh
+
+JAR=~/.m2/repository/io/greenbus/msg/greenbus-msg-compiler/1.0.0/greenbus-msg-compiler-1.0.0-jar-with-dependencies.jar
+
+java -Dapi.version="3.0.0" -cp $JAR io.greenbus.msg.compiler.doc.Compiler 2> msgdocprotoc.log
diff --git a/protoc-gen-msgjava b/protoc-gen-msgjava
new file mode 100755
index 0000000..6fa05a2
--- /dev/null
+++ b/protoc-gen-msgjava
@@ -0,0 +1,5 @@
+#! /bin/sh
+
+JAR=~/.m2/repository/io/greenbus/msg/greenbus-msg-compiler/1.0.0/greenbus-msg-compiler-1.0.0-jar-with-dependencies.jar
+
+java -DgenTarget=java -jar ${JAR}
diff --git a/protoc-gen-msgscala b/protoc-gen-msgscala
new file mode 100755
index 0000000..ff64671
--- /dev/null
+++ b/protoc-gen-msgscala
@@ -0,0 +1,5 @@
+#! /bin/sh
+
+JAR=~/.m2/repository/io/greenbus/msg/greenbus-msg-compiler/1.0.0/greenbus-msg-compiler-1.0.0-jar-with-dependencies.jar
+
+java -DgenTarget=scala -jar ${JAR}
diff --git a/scala-base/pom.xml b/scala-base/pom.xml
new file mode 100755
index 0000000..6044280
--- /dev/null
+++ b/scala-base/pom.xml
@@ -0,0 +1,98 @@
+
+
+ 4.0.0
+ greenbus-scala-base
+ pom
+
+
+ io.greenbus
+ greenbus-parent
+ 3.0.0
+ ../
+
+
+
+
+
+
+ net.alchim31.maven
+ scala-maven-plugin
+ ${scala-maven-plugin.version}
+
+
+
+ scala-compile-first
+ process-resources
+
+ add-source
+ compile
+
+
+
+ scala-test-compile
+ process-test-resources
+
+ testCompile
+
+
+
+
+
+
+ -Xmx1024m
+
+ false
+ true
+
+
+
+
+ org.scalariform
+ scalariform-maven-plugin
+ ${scalariform-maven-plugin.version}
+
+
+
+ format
+
+
+
+
+
+
+
+
+
+ org.scala-lang
+ scala-library
+ ${scala.version}
+ compile
+
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j-api.version}
+ compile
+
+
+
+ com.typesafe
+ scalalogging-slf4j_${scala.annotation}
+ ${scalalogging.version}
+ compile
+
+
+
+ org.scalatest
+ scalatest_${scala.annotation}
+ ${scalatest.version}
+ test
+
+
+
+
+
+
+
diff --git a/services/pom.xml b/services/pom.xml
new file mode 100755
index 0000000..6461016
--- /dev/null
+++ b/services/pom.xml
@@ -0,0 +1,93 @@
+
+ 4.0.0
+
+
+ io.greenbus
+ greenbus-scala-base
+ 3.0.0
+ ../scala-base
+
+
+ greenbus-services
+ jar
+
+
+
+ AGPLv3
+ http://www.gnu.org/licenses/agpl-3.0.txt
+
+
+
+
+
+
+
+ com.mycila.maven-license-plugin
+ maven-license-plugin
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ ../
+
+
+
+
+
+
+
+ io.greenbus
+ greenbus-client
+ 3.0.0
+
+
+ io.greenbus
+ greenbus-mstore-sql
+ 3.0.0
+
+
+ io.greenbus
+ greenbus-sql
+ 3.0.0
+
+
+ io.greenbus.msg
+ greenbus-msg-qpid
+ 1.0.0
+
+
+ com.typesafe.akka
+ akka-actor_2.10
+ 2.2.0
+
+
+ com.typesafe.akka
+ akka-slf4j_2.10
+ 2.2.0
+
+
+ commons-cli
+ commons-cli
+ 1.2
+
+
+ commons-codec
+ commons-codec
+ ${commons-codec.version}
+
+
+ io.greenbus
+ greenbus-sql
+ 3.0.0
+ test-jar
+ test
+
+
+
+
+
diff --git a/services/src/main/scala/io/greenbus/services/AuthenticationModule.scala b/services/src/main/scala/io/greenbus/services/AuthenticationModule.scala
new file mode 100644
index 0000000..8a179c9
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/AuthenticationModule.scala
@@ -0,0 +1,32 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services
+
+import java.util.UUID
+
+import scala.concurrent.Future
+
+sealed trait AuthenticationModule
+
+trait SqlAuthenticationModule extends AuthenticationModule {
+ def authenticate(name: String, password: String): (Boolean, Option[UUID])
+}
+trait AsyncAuthenticationModule extends AuthenticationModule {
+ def authenticate(name: String, password: String): Future[Boolean]
+}
\ No newline at end of file
diff --git a/services/src/main/scala/io/greenbus/services/CoreServices.scala b/services/src/main/scala/io/greenbus/services/CoreServices.scala
new file mode 100644
index 0000000..04f6958
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/CoreServices.scala
@@ -0,0 +1,141 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services
+
+import java.util.concurrent.ExecutorService
+
+import akka.actor.ActorSystem
+import com.typesafe.config.ConfigFactory
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.client.ServiceConnection
+import io.greenbus.client.proto.Envelope.SubscriptionEventType
+import io.greenbus.client.service.proto.Events.{ Alarm, AlarmNotification, Event, EventNotification }
+import io.greenbus.client.service.proto.FrontEnd.FrontEndConnectionStatus
+import io.greenbus.client.service.proto.Measurements.{ MeasurementBatchNotification, MeasurementNotification }
+import io.greenbus.client.service.proto.Model._
+import io.greenbus.client.service.proto.Processing.OverrideNotification
+import io.greenbus.jmx.MetricsManager
+import io.greenbus.mstore.sql.{ SimpleInTransactionCurrentValueStore, SimpleInTransactionHistoryStore }
+import io.greenbus.services.authz.{ AuthLookup, DefaultAuthLookup }
+import io.greenbus.services.core._
+import io.greenbus.services.framework._
+import io.greenbus.services.model._
+import io.greenbus.sql.DbConnection
+
+object CoreServices extends Logging {
+
+ def main(args: Array[String]) {
+ val rootConfig = ConfigFactory.load()
+ val slf4jConfig = ConfigFactory.parseString("""akka { loggers = ["akka.event.slf4j.Slf4jLogger"] }""")
+ val akkaConfig = slf4jConfig.withFallback(rootConfig)
+ val system = ActorSystem("services", akkaConfig)
+
+ val baseDir = Option(System.getProperty("io.greenbus.config.base")).getOrElse("")
+ val amqpConfigPath = Option(System.getProperty("io.greenbus.config.amqp")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.msg.amqp.cfg")
+ val sqlConfigPath = Option(System.getProperty("io.greenbus.config.sql")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.sql.cfg")
+
+ val mgr = system.actorOf(ServiceManager.props(amqpConfigPath, sqlConfigPath, runServices))
+ }
+
+ def runServices(sql: DbConnection, conn: ServiceConnection, exe: ExecutorService) {
+
+ val marshaller = new ServiceMarshaller {
+ def run[U](runner: => U) {
+ exe.submit(new Runnable {
+ def run() {
+ runner
+ }
+ })
+ }
+ }
+
+ val authModule: AuthenticationModule = new SquerylAuthModule
+ val authLookup: AuthLookup = DefaultAuthLookup
+
+ val metricsMgr = MetricsManager("io.greenbus.services")
+
+ val measChannelMgr = new SubscriptionChannelBinderOnly(conn.serviceOperations, classOf[MeasurementNotification].getSimpleName)
+ val measBatchChannelMgr = new SubscriptionChannelBinderOnly(conn.serviceOperations, classOf[MeasurementBatchNotification].getSimpleName)
+
+ val entitySubMgr = new SubscriptionChannelManager(EntitySubscriptionDescriptor, conn.serviceOperations, classOf[EntityNotification].getSimpleName)
+ val entityEdgeSubMgr = new SubscriptionChannelManager(EntityEdgeSubscriptionDescriptor, conn.serviceOperations, classOf[EntityEdgeNotification].getSimpleName)
+ val keyValueSubMgr = new SubscriptionChannelManager(EntityKeyValueSubscriptionDescriptor, conn.serviceOperations, classOf[EntityKeyValueNotification].getSimpleName)
+
+ val overSubMgr = new SubscriptionChannelManager(OverrideSubscriptionDescriptor, conn.serviceOperations, classOf[OverrideNotification].getSimpleName)
+
+ val endpointMgr = new SubscriptionChannelManager(EndpointSubscriptionDescriptor, conn.serviceOperations, classOf[EndpointNotification].getSimpleName)
+ val pointMgr = new SubscriptionChannelManager(PointSubscriptionDescriptor, conn.serviceOperations, classOf[PointNotification].getSimpleName)
+ val commandMgr = new SubscriptionChannelManager(CommandSubscriptionDescriptor, conn.serviceOperations, classOf[CommandNotification].getSimpleName)
+ val commStatusMgr = new SubscriptionChannelManager(FrontEndConnectionStatusSubscriptionDescriptor, conn.serviceOperations, classOf[FrontEndConnectionStatus].getSimpleName)
+
+ val eventMgr = new SubscriptionChannelManager(EventSubscriptionDescriptor, conn.serviceOperations, classOf[EventNotification].getSimpleName)
+ val alarmMgr = new SubscriptionChannelManager(AlarmSubscriptionDescriptor, conn.serviceOperations, classOf[AlarmNotification].getSimpleName)
+
+ val eventMapper = new ModelEventMapper
+ eventMapper.register(classOf[Entity], entitySubMgr)
+ eventMapper.register(classOf[EntityEdge], entityEdgeSubMgr)
+ eventMapper.register(classOf[EntityKeyValueWithEndpoint], keyValueSubMgr)
+ eventMapper.register(classOf[OverrideWithEndpoint], overSubMgr)
+ eventMapper.register(classOf[Endpoint], endpointMgr)
+ eventMapper.register(classOf[Point], pointMgr)
+ eventMapper.register(classOf[Command], commandMgr)
+ eventMapper.register(classOf[Event], eventMgr)
+ eventMapper.register(classOf[Alarm], alarmMgr)
+ eventMapper.register(classOf[FrontEndConnectionStatus], commStatusMgr)
+
+ val transSource = ServiceTransactionSource(sql, conn.serviceOperations, authLookup, eventMapper)
+ val registry = new FullServiceRegistry(sql, conn.serviceOperations, authLookup, eventMapper, metricsMgr)
+
+ val simpleModelNotifier = new SimpleModelNotifier(eventMapper, conn.serviceOperations, ServiceTransactionSource.notificationObserver)
+
+ val loginServices = new LoginServices(registry, sql, authModule, SquerylAuthModel, SquerylEventAlarmModel, simpleModelNotifier)
+
+ val entityServices = new EntityServices(registry, SquerylEntityModel, SquerylFrontEndModel, SquerylEventAlarmModel, entitySubMgr, keyValueSubMgr, entityEdgeSubMgr, endpointMgr, pointMgr, commandMgr)
+
+ val authServices = new AuthServices(registry, SquerylAuthModel)
+
+ val frontEndServices = new FrontEndServices(registry, conn.serviceOperations, SquerylFrontEndModel, commStatusMgr)
+
+ val processingServices = new ProcessingServices(registry, SquerylProcessingModel, SquerylEventAlarmModel, SquerylFrontEndModel, overSubMgr)
+
+ val measurementServices = new MeasurementServices(registry, SimpleInTransactionCurrentValueStore, SimpleInTransactionHistoryStore, measChannelMgr, measBatchChannelMgr, SquerylFrontEndModel)
+
+ val commandServices = new CommandServices(registry, conn.session, transSource, SquerylCommandModel, SquerylFrontEndModel, SquerylEventAlarmModel)
+
+ val eventAlarmServices = new EventAlarmServices(registry, SquerylEventAlarmModel, SquerylEntityModel, eventMgr, alarmMgr)
+
+ logger.info("Binding services")
+ registry.getRegistered.foreach {
+ case (requestId, handler) =>
+ conn.serviceOperations.bindCompetingService(new MarshallingHandler(marshaller, handler), requestId)
+ }
+
+ metricsMgr.register()
+
+ logger.info("Services bound")
+ }
+}
+
+object NotificationConversions {
+ def protoType: ModelEvent => SubscriptionEventType = {
+ case Created => SubscriptionEventType.ADDED
+ case Updated => SubscriptionEventType.MODIFIED
+ case Deleted => SubscriptionEventType.REMOVED
+ }
+}
diff --git a/services/src/main/scala/io/greenbus/services/ResetDatabase.scala b/services/src/main/scala/io/greenbus/services/ResetDatabase.scala
new file mode 100644
index 0000000..47d4cc3
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/ResetDatabase.scala
@@ -0,0 +1,94 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.sql.{ DbConnector, SqlSettings, DbConnection }
+import io.greenbus.services.model.{ EventSeeding, ModelAuthSeeder, AuthSeedData }
+import io.greenbus.services.data.{ ProcessingLockSchema, ServicesSchema }
+import io.greenbus.mstore.sql.MeasurementStoreSchema
+import org.apache.commons.cli
+import org.apache.commons.cli.{ HelpFormatter, Options }
+
+object ResetDatabase extends Logging {
+
+ val forceLongFlag = "force"
+ val forceFlag = "f"
+
+ def buildOptions: Options = {
+ val opts = new Options
+ opts.addOption("h", "help", false, "Display this help text")
+ opts.addOption(new cli.Option(forceFlag, forceLongFlag, false, "Skip confirmation prompt"))
+ opts
+ }
+
+ def reset(sqlConfigPath: String) {
+ val sqlSettings = SqlSettings.load(sqlConfigPath)
+ val conn = DbConnector.connect(sqlSettings)
+ reset(conn)
+ }
+
+ def reset(conn: DbConnection) {
+ conn.transaction {
+ ServicesSchema.reset()
+ MeasurementStoreSchema.drop
+ MeasurementStoreSchema.create
+
+ ProcessingLockSchema.reset()
+
+ AuthSeedData.seed(ModelAuthSeeder, "system")
+ EventSeeding.seed()
+ }
+ }
+
+ def main(args: Array[String]) {
+
+ val options = buildOptions
+ val parser = new cli.BasicParser
+ val line = parser.parse(options, args)
+
+ def printHelp() {
+ (new HelpFormatter).printHelp("", options)
+ }
+
+ if (line.hasOption("h")) {
+ printHelp()
+ } else {
+
+ val force = line.hasOption(forceLongFlag) || line.hasOption(forceFlag)
+ if (!force) {
+ println("Proceed with modifications? (y/N)")
+ val answer = readLine()
+ if (!Set("y", "yes").contains(answer.trim.toLowerCase)) {
+ System.exit(0)
+ }
+ }
+
+ try {
+ val baseDir = Option(System.getProperty("io.greenbus.config.base")).getOrElse("")
+ val sqlConfigPath = Option(System.getProperty("io.greenbus.config.sql")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.sql.cfg")
+ reset(sqlConfigPath)
+ } catch {
+ case ex: Throwable =>
+ logger.error("Failure: " + ex)
+ println("Error: " + ex.getMessage)
+ }
+ }
+ }
+}
diff --git a/services/src/main/scala/io/greenbus/services/ServiceManager.scala b/services/src/main/scala/io/greenbus/services/ServiceManager.scala
new file mode 100644
index 0000000..e3bed39
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/ServiceManager.scala
@@ -0,0 +1,111 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services
+
+import akka.actor._
+import com.typesafe.scalalogging.slf4j.Logging
+import scala.concurrent.duration._
+import java.util.concurrent.{ ExecutorService, Executors }
+import io.greenbus.msg.amqp.AmqpSettings
+import io.greenbus.sql.{ DbConnector, SqlSettings, DbConnection }
+import scala.concurrent.ExecutionContext.Implicits.global
+import io.greenbus.msg.qpid.QpidBroker
+import io.greenbus.client.ServiceConnection
+
+object ServiceManager {
+
+ sealed trait State
+ case object Down extends State
+ case object Running extends State
+
+ sealed trait Data
+ case object NoData extends Data
+ case class Resources(sql: DbConnection, conn: ServiceConnection, exe: ExecutorService) extends Data
+
+ case object AttemptConnect
+ case class LostBrokerConnection(expected: Boolean)
+
+ type ServiceRunner = (DbConnection, ServiceConnection, ExecutorService) => Unit
+
+ def props(amqpConfigPath: String, sqlConfigPath: String, runner: ServiceRunner): Props =
+ Props(classOf[ServiceManager], amqpConfigPath, sqlConfigPath, runner)
+}
+
+import ServiceManager._
+class ServiceManager(amqpConfigPath: String, sqlConfigPath: String, runner: ServiceRunner) extends Actor with FSM[State, Data] with Logging {
+
+ private def connectSql(): DbConnection = {
+ val config = SqlSettings.load(sqlConfigPath)
+ DbConnector.connect(config)
+ }
+
+ startWith(Down, NoData)
+
+ when(Down) {
+ case Event(AttemptConnect, NoData) => {
+ try {
+ val conn = ServiceConnection.connect(AmqpSettings.load(amqpConfigPath), QpidBroker, 5000)
+ val exe = Executors.newCachedThreadPool()
+ val sql = connectSql()
+
+ conn.addConnectionListener { expected =>
+ self ! LostBrokerConnection(expected)
+ }
+
+ runner(sql, conn, exe)
+
+ goto(Running) using Resources(sql, conn, exe)
+
+ } catch {
+ case ex: Throwable => {
+ logger.error("Couldn't initialize services: " + ex.getMessage)
+ scheduleRetry()
+ stay using NoData
+ }
+ }
+ }
+ }
+
+ when(Running) {
+ case Event(LostBrokerConnection(expected), res: Resources) =>
+ res.exe.shutdown()
+ scheduleRetry()
+ goto(Down) using NoData
+ }
+
+ override def preStart() {
+ self ! AttemptConnect
+ }
+
+ onTermination {
+ case StopEvent(_, _, res: Resources) =>
+ res.conn.disconnect()
+ res.exe.shutdown()
+ }
+
+ private def scheduleRetry() {
+ context.system.scheduler.scheduleOnce(
+ Duration(3000, MILLISECONDS),
+ self,
+ AttemptConnect)
+ }
+
+ initialize()
+}
+
diff --git a/services/src/main/scala/io/greenbus/services/authz/AuthLookup.scala b/services/src/main/scala/io/greenbus/services/authz/AuthLookup.scala
new file mode 100644
index 0000000..f3ab66e
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/authz/AuthLookup.scala
@@ -0,0 +1,185 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.authz
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.client.exception.{ ForbiddenException, UnauthorizedException }
+import io.greenbus.services.model.SquerylAuthModel
+import io.greenbus.client.service.proto.Auth.{ EntitySelector, Permission, PermissionSet }
+import com.google.protobuf.InvalidProtocolBufferException
+import scala.collection.JavaConversions._
+import java.util.UUID
+
+trait AuthLookup {
+ def validateAuth(headers: Map[String, String]): AuthContext
+}
+
+object DefaultAuthLookup extends AuthLookup with Logging {
+
+ def validateAuth(headers: Map[String, String]): AuthContext = {
+
+ val token = headers.getOrElse("AUTH_TOKEN", throw new UnauthorizedException("Must include AUTH_TOKEN header"))
+
+ val (agentId, agentName, permBytes) = SquerylAuthModel.authLookup(token).getOrElse {
+ throw new UnauthorizedException("Invalid auth token")
+ }
+
+ val permissions = try {
+ permBytes.map(PermissionSet.parseFrom)
+ } catch {
+ case protoEx: InvalidProtocolBufferException =>
+ logger.error(s"Couldn't parse permission sets for agent '$agentName'")
+ throw new ForbiddenException("Invalid permission sets, see system administrator")
+ }
+
+ new DefaultAuthContext(agentId, agentName, permissions)
+ }
+}
+
+trait AuthContext {
+ def authorize(resource: String, action: String): Option[EntityFilter]
+ def authorizeUnconditionallyOnly(resource: String, action: String): Unit
+ def authorizeAndCheckSelfOnly(resource: String, action: String): Boolean
+ def agentName: String
+ def agentId: UUID
+}
+
+class DefaultAuthContext(val agentId: UUID, val agentName: String, sets: Seq[PermissionSet]) extends AuthContext with Logging {
+
+ private lazy val allPermissions: Seq[Permission] = sets.flatMap(_.getPermissionsList.toSeq)
+
+ def authorizeAndCheckSelfOnly(resource: String, action: String): Boolean = {
+
+ val applicable = allPermissions.filter { p =>
+ p.getResourcesList.exists(r => r == "*" || r == resource) &&
+ p.getActionsList.exists(a => a == "*" || a == action)
+ }
+
+ // Deny all by default
+ if (applicable.isEmpty) {
+ throw new ForbiddenException(s"Insufficient permissions for $resource:$action")
+ }
+ val (allows, denies) = applicable.partition(_.getAllow)
+
+ // Allow must be unconditional or 'self', deny is impossible because agent sets can't be defined other than self
+ if (denies.nonEmpty) {
+ throw new ForbiddenException(s"Insufficient permissions for $resource:$action")
+ }
+
+ val blanketAllow = allows.exists(a => a.getSelectorsCount == 0 || a.getSelectorsList.forall(_.getStyle == "*"))
+
+ if (blanketAllow) {
+ false
+ } else {
+ val selfSpecified = allows.forall(p => p.getSelectorsList.forall(s => s.getStyle == "self"))
+
+ // If there is some other selector (and not blanket allow) we can't authorize this
+ if (selfSpecified) true else throw { new ForbiddenException(s"Insufficient permissions for $resource:$action") }
+ }
+ }
+
+ def authorizeUnconditionallyOnly(resource: String, action: String): Unit = {
+
+ val applicable = allPermissions.filter { p =>
+ p.getResourcesList.exists(r => r == "*" || r == resource) &&
+ p.getActionsList.exists(a => a == "*" || a == action)
+ }
+
+ // Deny all by default
+ if (applicable.isEmpty) {
+ throw new ForbiddenException(s"Insufficient permissions for $resource:$action")
+ }
+ val (allows, denies) = applicable.partition(_.getAllow)
+
+ // Allow must be unconditional or 'self', deny is impossible because agent sets can't be defined other than self
+ if (denies.nonEmpty) {
+ throw new ForbiddenException(s"Insufficient permissions for $resource:$action")
+ }
+
+ val blanketAllow = allows.exists(a => a.getSelectorsCount == 0 || a.getSelectorsList.forall(_.getStyle == "*"))
+
+ if (!blanketAllow) {
+ throw new ForbiddenException(s"Insufficient permissions for $resource:$action")
+ }
+ }
+
+ def authorize(resource: String, action: String): Option[EntityFilter] = {
+
+ val applicable = allPermissions.filter { p =>
+ p.getResourcesList.exists(r => r == "*" || r == resource) &&
+ p.getActionsList.exists(a => a == "*" || a == action)
+ }
+
+ // Deny all by default
+ if (applicable.isEmpty) {
+ throw new ForbiddenException(s"Insufficient permissions for $resource:$action")
+ }
+ val (allows, denies) = applicable.partition(_.getAllow)
+
+ // Blanket denial of this resource/action
+ if (denies.exists(p => p.getSelectorsCount == 0 || p.getSelectorsList.exists(_.getStyle == "*"))) {
+ throw new ForbiddenException(s"Insufficient permissions for $resource:$action")
+ }
+
+ val blanketAllow = allows.forall(a => a.getSelectorsCount == 0 || a.getSelectorsList.forall(_.getStyle == "*"))
+
+ // Blanket permission of this resource/action
+ if (denies.isEmpty && blanketAllow) {
+ None
+ } else {
+ // Resource-conditional permissions
+ val denySelectors = denies.flatMap(_.getSelectorsList).map(interpretSelector)
+ val allowSelectors = allows.flatMap(_.getSelectorsList).filterNot(_.getStyle == "*").map(interpretSelector)
+
+ val filter = ResourceSelector.buildFilter(denySelectors, allowSelectors)
+ Some(filter)
+ }
+ }
+
+ private def interpretSelector(selector: EntitySelector): ResourceSelector = {
+
+ // Problems here are a data input problem with the auth system
+ def invalid(msg: String): Throwable = {
+ logger.error(msg)
+ new ForbiddenException("Invalid permissions sets, see system administrator")
+ }
+
+ if (!selector.hasStyle) {
+ throw invalid(s"Permission set for '$agentName' has entity selector missing style")
+ }
+ selector.getStyle.trim match {
+ case "*" => throw new IllegalStateException("Should have filtered out '*' entity selectors")
+ case "type" => {
+ if (selector.getArgumentsCount == 0) {
+ throw invalid(s"Selector for '$agentName' is 'type' but has no arguments")
+ }
+ TypeSelector(selector.getArgumentsList.toSeq)
+ }
+ case "parent" => {
+ if (selector.getArgumentsCount == 0) {
+ throw invalid(s"Selector for '$agentName' is 'parent' but has no arguments")
+ }
+ ParentSelector(selector.getArgumentsList.toSeq)
+ }
+ case other => {
+ throw invalid(s"Unknown selector type for '$agentName': $other")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/src/main/scala/io/greenbus/services/authz/ResourceSelector.scala b/services/src/main/scala/io/greenbus/services/authz/ResourceSelector.scala
new file mode 100644
index 0000000..e0b7ddc
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/authz/ResourceSelector.scala
@@ -0,0 +1,154 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.authz
+
+import java.util.UUID
+import org.squeryl.dsl.ast.{ BinaryOperatorNodeLogicalBoolean, LogicalBoolean }
+import io.greenbus.services.data.EntityRow
+import org.squeryl.PrimitiveTypeMode._
+import io.greenbus.services.data.ServicesSchema._
+import org.squeryl.Query
+
+object EntityFilter {
+ def optional(filter: Option[EntityFilter], ent: EntityRow): LogicalBoolean = {
+ filter match {
+ case None => true === true
+ case Some(f) => f(ent.id)
+ }
+ }
+ def optional(filter: Option[EntityFilter], entId: UUID): LogicalBoolean = {
+ filter match {
+ case None => true === true
+ case Some(f) => f(entId)
+ }
+ }
+ def optional(filter: Option[EntityFilter], entId: Option[UUID]): LogicalBoolean = {
+ filter match {
+ case None => true === true
+ case Some(f) => f(entId)
+ }
+ }
+}
+
+trait EntityFilter {
+
+ protected def filterQuery: Query[EntityRow]
+
+ def apply(id: UUID): LogicalBoolean = {
+ id in from(filterQuery)(ent => select(ent.id))
+ }
+ def apply(optId: Option[UUID]): LogicalBoolean = {
+ optId in from(filterQuery)(ent => select(ent.id))
+ }
+
+ def filter(ids: Seq[UUID]): Seq[UUID] = {
+ from(entities)(ent =>
+ where(ent.id in ids and not(apply(ent.id)))
+ select (ent.id)).toSeq
+ }
+
+ def isOutsideSet(id: UUID): Boolean = {
+ from(entities)(ent =>
+ where(ent.id === id and not(apply(ent.id)))
+ select (ent.id)).page(0, 1).nonEmpty
+ }
+
+ def anyOutsideSet(ids: Seq[UUID]): Boolean = {
+ from(entities)(ent =>
+ where(ent.id in ids and not(apply(ent.id)))
+ select (ent.id)).page(0, 1).nonEmpty
+ }
+
+ def anyOutsideSet(ents: Query[EntityRow]): Boolean = {
+ from(ents)(ent =>
+ where(not(apply(ent.id)))
+ select (ent.id)).page(0, 1).nonEmpty
+ }
+
+}
+
+sealed trait ResourceSelector {
+ def clause: EntityRow => LogicalBoolean
+}
+
+object ResourceSelector {
+
+ def buildFilter(denies: Seq[ResourceSelector], allows: Seq[ResourceSelector]): EntityFilter = {
+ val denyClauses = denies.map(_.clause)
+ val allowClauses = allows.map(_.clause)
+
+ def allDenials(row: EntityRow): LogicalBoolean = {
+ denyClauses.map(f => f(row)).reduceLeft((a, b) => new BinaryOperatorNodeLogicalBoolean(a, b, "and"))
+ }
+ def allAllows(row: EntityRow): LogicalBoolean = {
+ allowClauses.map(f => f(row)).reduceLeft((a, b) => new BinaryOperatorNodeLogicalBoolean(a, b, "or"))
+ }
+
+ new EntityFilter {
+
+ protected def filterQuery: Query[EntityRow] = {
+ (allowClauses.nonEmpty, denyClauses.nonEmpty) match {
+ case (false, false) => entities.where(t => true === true)
+
+ case (true, false) =>
+ from(entities)(ent =>
+ where(allAllows(ent))
+ select (ent))
+
+ case (false, true) =>
+ from(entities)(ent =>
+ where(not(allDenials(ent)))
+ select (ent))
+
+ case (true, true) =>
+ from(entities)(ent =>
+ where(allAllows(ent) and not(allDenials(ent)))
+ select (ent))
+ }
+ }
+ }
+ }
+
+}
+
+case class TypeSelector(types: Seq[String]) extends ResourceSelector {
+
+ def clause: EntityRow => LogicalBoolean = {
+ row: EntityRow =>
+ exists(from(entityTypes)(t =>
+ where(t.entityId === row.id and
+ (t.entType in types))
+ select (t.entityId)))
+
+ }
+}
+
+case class ParentSelector(parents: Seq[String]) extends ResourceSelector {
+
+ def clause: EntityRow => LogicalBoolean = {
+ row: EntityRow =>
+ exists(from(entities, edges)((parent, edge) =>
+ where(parent.name in parents and
+ edge.parentId === parent.id and
+ edge.childId === row.id and
+ edge.relationship === "owns")
+ select (parent.id)))
+ }
+}
+
diff --git a/services/src/main/scala/io/greenbus/services/core/AuthServices.scala b/services/src/main/scala/io/greenbus/services/core/AuthServices.scala
new file mode 100644
index 0000000..fa0d37a
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/core/AuthServices.scala
@@ -0,0 +1,312 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.core
+
+import java.util.UUID
+
+import io.greenbus.client.exception.{ ForbiddenException, BadRequestException }
+import io.greenbus.client.proto.Envelope
+import io.greenbus.client.service.AuthService.Descriptors
+import io.greenbus.client.service.proto.Auth.Permission
+import io.greenbus.client.service.proto.AuthRequests._
+import io.greenbus.services.framework.{ ServiceContext, Success, _ }
+import io.greenbus.services.model.AuthModel
+import io.greenbus.services.model.AuthModel.{ AgentInfo, CoreUntypedTemplate, PermissionSetInfo }
+import io.greenbus.services.model.UUIDHelpers._
+
+import scala.collection.JavaConversions._
+
+object AuthServices {
+
+ val defaultPageSize = 200
+
+ val agentResource = "agent"
+ val agentPasswordResource = "agent_password"
+ val permissionSetResource = "agent_permissions"
+
+ def validatePermissionSet(template: PermissionSetTemplate) {
+ if (!template.hasName) {
+ throw new BadRequestException("Permission set must include name")
+ }
+ EntityServices.validateEntityName(template.getName)
+
+ if (template.getPermissionsCount == 0) {
+ throw new BadRequestException("Permission set must include at least one permission")
+ }
+
+ template.getPermissionsList.foreach(validatePermission)
+ }
+
+ def validatePermission(perm: Permission) {
+ if (!perm.hasAllow) {
+ throw new BadRequestException("Permission must specify allow/deny")
+ }
+ if (perm.getResourcesCount == 0) {
+ throw new BadRequestException("Permission must include a resource; add '*' for all")
+ }
+ if (perm.getActionsCount == 0) {
+ throw new BadRequestException("Permission must include an action; add '*' for all")
+ }
+
+ perm.getSelectorsList.foreach { selector =>
+ if (!selector.hasStyle) {
+ throw new BadRequestException("Permission's entity selector must include style")
+ }
+ if ((selector.getStyle == "type" || selector.getStyle == "parent") && selector.getArgumentsCount == 0) {
+ throw new BadRequestException(s"Entity selectors of style ${selector.getStyle} must include arguments")
+ }
+ }
+ }
+}
+
+class AuthServices(services: ServiceRegistry, authModel: AuthModel) {
+ import io.greenbus.services.core.AuthServices._
+
+ services.fullService(Descriptors.GetAgents, getAgents)
+ services.fullService(Descriptors.AgentQuery, agentQuery)
+ services.fullService(Descriptors.PutAgents, putAgents)
+ services.fullService(Descriptors.PutAgentPasswords, putAgentPasswords)
+ services.fullService(Descriptors.DeleteAgents, deleteAgents)
+ services.fullService(Descriptors.GetPermissionSets, getPermissionSets)
+ services.fullService(Descriptors.PermissionSetQuery, permissionSetQuery)
+ services.fullService(Descriptors.PutPermissionSets, putPermissionSets)
+ services.fullService(Descriptors.DeletePermissionSets, deletePermissionSets)
+
+ def getAgents(request: GetAgentsRequest, headers: Map[String, String], context: ServiceContext): Response[GetAgentsResponse] = {
+
+ val selfOnly = context.auth.authorizeAndCheckSelfOnly(agentResource, "read")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val keySet = request.getRequest
+
+ if (keySet.getUuidsCount == 0 && keySet.getNamesCount == 0) {
+ throw new BadRequestException("Must include at least one id or name")
+ }
+
+ val uuids = keySet.getUuidsList.toSeq.map(protoUUIDToUuid)
+ val names = keySet.getNamesList.toSeq
+
+ val results = if (selfOnly) {
+ if (uuids.contains(context.auth.agentId) || names.contains(context.auth.agentName)) {
+ authModel.agentKeyQuery(Seq(context.auth.agentId), Seq())
+ } else {
+ Seq()
+ }
+ } else {
+ authModel.agentKeyQuery(uuids, names)
+ }
+
+ val response = GetAgentsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def agentQuery(request: AgentQueryRequest, headers: Map[String, String], context: ServiceContext): Response[AgentQueryResponse] = {
+
+ val selfOnly = context.auth.authorizeAndCheckSelfOnly(agentResource, "read")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val query = request.getRequest
+
+ val (pageByName, lastUuid, lastName, pageSize) = EntityServices.parsePageParams(query.hasPagingParams, query.getPagingParams)
+
+ val permSetNames = query.getPermissionSetsList.toSeq
+
+ val selfOpt = if (selfOnly) Some(context.auth.agentId) else None
+
+ val results = authModel.agentQuery(selfOpt, permSetNames, lastUuid, lastName, pageSize, pageByName)
+
+ val response = AgentQueryResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def putAgents(request: PutAgentsRequest, headers: Map[String, String], context: ServiceContext): Response[PutAgentsResponse] = {
+
+ context.auth.authorizeUnconditionallyOnly(agentResource, "create")
+ context.auth.authorizeUnconditionallyOnly(agentResource, "update")
+
+ if (request.getTemplatesCount == 0) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val templates = request.getTemplatesList.toSeq
+
+ val modelTemplates = templates.map { template =>
+ if (!template.hasName) {
+ throw new BadRequestException("Must include agent name")
+ }
+
+ EntityServices.validateEntityName(template.getName)
+
+ val passwordOpt = if (template.hasPassword) Some(template.getPassword) else None
+
+ val uuidOpt = if (template.hasUuid) Some(protoUUIDToUuid(template.getUuid)) else None
+
+ CoreUntypedTemplate(uuidOpt, template.getName, AgentInfo(passwordOpt, template.getPermissionSetsList.toSeq))
+ }
+
+ val results = authModel.putAgents(context.notifier, modelTemplates, true)
+
+ val response = PutAgentsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def putAgentPasswords(request: PutAgentPasswordsRequest, headers: Map[String, String], context: ServiceContext): Response[PutAgentPasswordsResponse] = {
+
+ val selfOnly = context.auth.authorizeAndCheckSelfOnly(agentPasswordResource, "update")
+
+ if (request.getUpdatesCount == 0) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val updates = request.getUpdatesList.toSeq.map { update =>
+ val uuid: UUID = if (update.hasUuid) update.getUuid else throw new BadRequestException("Must include UUID of Agent")
+ val password = if (update.hasPassword) update.getPassword else throw new BadRequestException("Must include new password")
+
+ (uuid, password)
+ }
+
+ if (selfOnly) {
+ updates match {
+ case Seq(update) =>
+ if (update._1 != context.auth.agentId) {
+ throw new ForbiddenException("Insufficient permissions")
+ }
+ case _ => throw new ForbiddenException("Insufficient permissions")
+ }
+ }
+
+ val results = authModel.modifyAgentPasswords(updates)
+
+ val response = PutAgentPasswordsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def deleteAgents(request: DeleteAgentsRequest, headers: Map[String, String], context: ServiceContext): Response[DeleteAgentsResponse] = {
+
+ context.auth.authorizeUnconditionallyOnly(agentResource, "delete")
+
+ if (request.getAgentUuidsCount == 0) {
+ throw new BadRequestException("Must include ids to delete")
+ }
+
+ val results = authModel.deleteAgents(context.notifier, request.getAgentUuidsList.map(protoUUIDToUuid))
+
+ val response = DeleteAgentsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def getPermissionSets(request: GetPermissionSetsRequest, headers: Map[String, String], context: ServiceContext): Response[GetPermissionSetsResponse] = {
+
+ val selfOnly = context.auth.authorizeAndCheckSelfOnly(permissionSetResource, "read")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val keySet = request.getRequest
+
+ if (keySet.getIdsCount == 0 && keySet.getNamesCount == 0) {
+ throw new BadRequestException("Must include at least one id or name.")
+ }
+
+ val uuids = keySet.getIdsList.toSeq.map(protoIdToLong)
+ val names = keySet.getNamesList.toSeq
+
+ val results = if (selfOnly) {
+ authModel.permissionSetSelfKeyQuery(context.auth.agentId, uuids, names)
+ } else {
+ authModel.permissionSetKeyQuery(uuids, names)
+ }
+
+ val response = GetPermissionSetsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def permissionSetQuery(request: PermissionSetQueryRequest, headers: Map[String, String], context: ServiceContext): Response[PermissionSetQueryResponse] = {
+
+ val selfOnly = context.auth.authorizeAndCheckSelfOnly(permissionSetResource, "read")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val query = request.getRequest
+
+ val lastId = if (query.hasLastId) Some(protoIdToLong(query.getLastId)) else None
+ val pageSize = if (query.hasPageSize) query.getPageSize else defaultPageSize
+
+ val results = if (selfOnly) {
+ authModel.permissionSetSelfQuery(context.auth.agentId, lastId, pageSize)
+ } else {
+ authModel.permissionSetQuery(lastId, pageSize)
+ }
+
+ val response = PermissionSetQueryResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def putPermissionSets(request: PutPermissionSetsRequest, headers: Map[String, String], context: ServiceContext): Response[PutPermissionSetsResponse] = {
+
+ context.auth.authorizeUnconditionallyOnly(permissionSetResource, "create")
+ context.auth.authorizeUnconditionallyOnly(permissionSetResource, "update")
+
+ if (request.getTemplatesCount == 0) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val templates = request.getTemplatesList.toSeq
+
+ templates.foreach(validatePermissionSet)
+
+ val modelTemplates = templates.map { template =>
+
+ val uuidOpt = if (template.hasId) Some(protoIdToLong(template.getId)) else None
+
+ CoreUntypedTemplate(uuidOpt, template.getName, PermissionSetInfo(template.getPermissionsList.toSeq))
+ }
+
+ val results = authModel.putPermissionSets(context.notifier, modelTemplates, true)
+
+ val response = PutPermissionSetsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def deletePermissionSets(request: DeletePermissionSetsRequest, headers: Map[String, String], context: ServiceContext): Response[DeletePermissionSetsResponse] = {
+
+ context.auth.authorizeUnconditionallyOnly(permissionSetResource, "delete")
+
+ if (request.getPermissionSetIdsCount == 0) {
+ throw new BadRequestException("Must include ids to delete")
+ }
+
+ val uuids = request.getPermissionSetIdsList.map(protoIdToLong)
+
+ val results = authModel.deletePermissionSets(context.notifier, uuids)
+
+ val response = DeletePermissionSetsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+}
\ No newline at end of file
diff --git a/services/src/main/scala/io/greenbus/services/core/CommandServices.scala b/services/src/main/scala/io/greenbus/services/core/CommandServices.scala
new file mode 100644
index 0000000..71ecb8d
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/core/CommandServices.scala
@@ -0,0 +1,251 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.core
+
+import java.util.concurrent.TimeoutException
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.msg.Session
+import io.greenbus.client.exception.{ BadRequestException, LockedException, ServiceException }
+import io.greenbus.client.proto.Envelope
+import io.greenbus.client.service.CommandService
+import io.greenbus.client.service.CommandService.Descriptors
+import io.greenbus.client.service.proto.CommandRequests._
+import io.greenbus.client.service.proto.Commands.{ CommandRequest, CommandResult, CommandStatus }
+import io.greenbus.services.framework._
+import io.greenbus.services.model.EventAlarmModel.SysEventTemplate
+import io.greenbus.services.model.UUIDHelpers._
+import io.greenbus.services.model._
+
+import scala.collection.JavaConversions._
+import scala.concurrent.Future
+
+object CommandServices {
+
+ val selectResource = "command_lock_select"
+ val blockResource = "command_lock_block"
+ val lockResource = "command_lock"
+
+ val commandRequestResource = "user_command_request"
+ val commandRequestUneventedResource = "user_command_request_unevented"
+
+ val defaultExpireTime = 30000
+
+ val defaultLockPageSize = 200
+}
+
+class CommandServices(services: ServiceRegistry, session: Session, trans: ServiceTransactionSource, commandModel: CommandModel, frontEndModel: FrontEndModel, eventModel: EventAlarmModel) extends Logging {
+ import io.greenbus.services.core.CommandServices._
+
+ services.simpleAsync(Descriptors.IssueCommandRequest, issueCommandRequest)
+
+ services.fullService(Descriptors.GetCommandLocks, getCommandLocks)
+ services.fullService(Descriptors.CommandLockQuery, commandLockQuery)
+ services.fullService(Descriptors.SelectCommands, selectCommands)
+ services.fullService(Descriptors.BlockCommands, blockCommands)
+ services.fullService(Descriptors.DeleteCommandLocks, deleteCommandLocks)
+
+ def issueCommandRequest(request: PostCommandRequestRequest, headers: Map[String, String], responseHandler: Response[PostCommandRequestResponse] => Unit) {
+
+ trans.transaction(headers) { context =>
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val cmdReq = request.getRequest
+
+ val unevented = cmdReq.hasUnevented && cmdReq.getUnevented
+
+ val filter = if (unevented) {
+ context.auth.authorize(commandRequestUneventedResource, "create")
+ } else {
+ context.auth.authorize(commandRequestResource, "create")
+ }
+
+ if (!cmdReq.hasCommandUuid) {
+ throw new BadRequestException("Must include command")
+ }
+
+ if (!commandModel.agentCanIssueCommand(cmdReq.getCommandUuid, context.auth.agentId, filter)) {
+ throw new LockedException("Command locked")
+ }
+
+ val client = CommandService.client(session)
+ val resultOpt: Option[(Option[String], String)] = frontEndModel.addressForCommand(cmdReq.getCommandUuid)
+
+ resultOpt match {
+ case None => throw new BadRequestException("Frontend serving command not found")
+ case Some((None, cmdName)) => Future.successful(CommandResult.newBuilder().setStatus(CommandStatus.TIMEOUT).build())
+ case Some((Some(address), cmdName)) =>
+
+ val issued = client.issueCommandRequest(request.getRequest, address)
+ import scala.concurrent.ExecutionContext.Implicits.global
+
+ issued.onSuccess {
+ case cmdResult =>
+ val response = PostCommandRequestResponse.newBuilder().setResult(cmdResult).build
+ responseHandler(Success(Envelope.Status.OK, response))
+ }
+
+ issued.onFailure {
+ case ex: ServiceException => responseHandler(Failure(ex.getStatus, ex.getMessage))
+ case ex: TimeoutException =>
+ logger.warn("Response timeout from front end connection")
+ responseHandler(Failure(Envelope.Status.RESPONSE_TIMEOUT, "Response timeout from front end connection"))
+ case ex: Throwable =>
+ logger.warn("Command service returned unexpected error: " + ex)
+ responseHandler(Failure(Envelope.Status.INTERNAL_ERROR, "Error issuing command request to front end connection"))
+ }
+
+ if (!unevented) {
+ val event = (cmdReq.hasType && cmdReq.getType != CommandRequest.ValType.NONE) match {
+ case false => {
+ val attrs = EventAlarmModel.mapToAttributesList(Seq(("command", cmdName)))
+ SysEventTemplate(context.auth.agentName, EventSeeding.System.controlExe.eventType, Some("services"), None, None, None, attrs)
+ }
+ case true => {
+ val cmdValue = cmdReq.getType match {
+ case CommandRequest.ValType.DOUBLE => cmdReq.getDoubleVal.toString
+ case CommandRequest.ValType.INT => cmdReq.getIntVal.toString
+ case CommandRequest.ValType.STRING => cmdReq.getStringVal
+ case CommandRequest.ValType.NONE => "-" // should not happen
+ }
+ val attrs = EventAlarmModel.mapToAttributesList(Seq(("command", cmdName), ("value", cmdValue)))
+ SysEventTemplate(context.auth.agentName, EventSeeding.System.updatedSetpoint.eventType, Some("services"), None, None, None, attrs)
+
+ }
+ }
+
+ eventModel.postEvents(context.notifier, Seq(event))
+ }
+ }
+ }
+
+ }
+
+ def getCommandLocks(request: GetCommandLockRequest, headers: Map[String, String], context: ServiceContext): Response[GetCommandLockResponse] = {
+
+ val filter = context.auth.authorize(lockResource, "read")
+
+ if (request.getLockIdsCount == 0) {
+ throw new BadRequestException("Must include one or more ids")
+ }
+
+ val lockIds = request.getLockIdsList.toSeq.map(protoIdToLong)
+
+ val results = commandModel.lockKeyQuery(lockIds, filter)
+
+ val response = GetCommandLockResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def commandLockQuery(request: CommandLockQueryRequest, headers: Map[String, String], context: ServiceContext): Response[CommandLockQueryResponse] = {
+
+ val filter = context.auth.authorize(lockResource, "read")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val query = request.getRequest
+
+ val cmds = query.getCommandUuidsList.toSeq map protoUUIDToUuid
+
+ val agents = query.getAgentUuidsList.toSeq map protoUUIDToUuid
+
+ val access = if (query.hasAccess) Some(query.getAccess) else None
+
+ val lastId = if (query.hasLastId) Some(protoIdToLong(query.getLastId)) else None
+
+ val results = commandModel.lockQuery(cmds, agents, access, lastId, defaultLockPageSize, filter)
+
+ val response = CommandLockQueryResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def selectCommands(request: PostCommandSelectRequest, headers: Map[String, String], context: ServiceContext): Response[PostCommandSelectResponse] = {
+
+ val filter = context.auth.authorize(selectResource, "create")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ if (request.getRequest.getCommandUuidsCount == 0) {
+ throw new BadRequestException("Must include one or more commands")
+ }
+
+ val commands = request.getRequest.getCommandUuidsList.toSeq.map(protoUUIDToUuid)
+ val expiryDuration = if (request.getRequest.hasExpireDuration) request.getRequest.getExpireDuration else defaultExpireTime
+ val expireTime = System.currentTimeMillis() + expiryDuration
+ val agentId = context.auth.agentId
+
+ val result = try {
+ commandModel.selectCommands(context.notifier, commands, agentId, expireTime, filter)
+ } catch {
+ case ex: CommandLockException => throw new LockedException(ex.getMessage)
+ }
+
+ val response = PostCommandSelectResponse.newBuilder().setResult(result).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def blockCommands(request: PostCommandBlockRequest, headers: Map[String, String], context: ServiceContext): Response[PostCommandBlockResponse] = {
+
+ val filter = context.auth.authorize(blockResource, "create")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ if (request.getRequest.getCommandUuidsCount == 0) {
+ throw new BadRequestException("Must include one or more commands")
+ }
+
+ val commands = request.getRequest.getCommandUuidsList.toSeq.map(protoUUIDToUuid)
+ val agentId = context.auth.agentId
+
+ val result = try {
+ commandModel.blockCommands(context.notifier, commands, agentId, filter)
+ } catch {
+ case ex: CommandLockException => throw new LockedException(ex.getMessage)
+ }
+
+ val response = PostCommandBlockResponse.newBuilder().setResult(result).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def deleteCommandLocks(request: DeleteCommandLockRequest, headers: Map[String, String], context: ServiceContext): Response[DeleteCommandLockResponse] = {
+
+ val selfOnly = context.auth.authorizeAndCheckSelfOnly(lockResource, "delete")
+
+ if (request.getLockIdsCount == 0) {
+ throw new BadRequestException("Must include one or more ids")
+ }
+
+ val lockIds = request.getLockIdsList.toSeq.map(protoIdToLong)
+
+ val filter = if (selfOnly) Some(context.auth.agentId) else None
+
+ val results = commandModel.deleteLocks(context.notifier, lockIds, filter)
+
+ val response = DeleteCommandLockResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+}
diff --git a/services/src/main/scala/io/greenbus/services/core/EventAlarmServices.scala b/services/src/main/scala/io/greenbus/services/core/EventAlarmServices.scala
new file mode 100644
index 0000000..8192e52
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/core/EventAlarmServices.scala
@@ -0,0 +1,498 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.core
+
+import io.greenbus.client.exception.{ BadRequestException, ForbiddenException }
+import io.greenbus.client.proto.Envelope
+import io.greenbus.client.proto.Envelope.SubscriptionEventType
+import io.greenbus.client.service.EventService.Descriptors
+import io.greenbus.client.service.proto.EventRequests
+import io.greenbus.client.service.proto.EventRequests._
+import io.greenbus.client.service.proto.Events.{ Alarm, AlarmNotification, Event, EventNotification }
+import io.greenbus.services.authz.EntityFilter
+import io.greenbus.services.framework.{ ServiceContext, Success, _ }
+import io.greenbus.services.model.EventAlarmModel.{ EventQueryParams, SysEventTemplate }
+import io.greenbus.services.model.UUIDHelpers._
+import io.greenbus.services.model.{ EntityModel, EventAlarmModel }
+
+import scala.collection.JavaConversions._
+
+object EventAlarmServices {
+
+ val eventConfigResource = "event_config"
+ val eventResource = "event"
+ val alarmResource = "alarm"
+
+ val defaultEventConfigPageSize = 200
+ val defaultEventPageSize = 200
+}
+
+class EventAlarmServices(services: ServiceRegistry, eventAlarmModel: EventAlarmModel, entityModel: EntityModel, eventBinding: SubscriptionChannelBinder, alarmBinding: SubscriptionChannelBinder) {
+ import io.greenbus.services.core.EventAlarmServices._
+
+ services.fullService(Descriptors.EventConfigQuery, eventConfigQuery)
+ services.fullService(Descriptors.GetEventConfigs, getEventConfigs)
+ services.fullService(Descriptors.PutEventConfigs, putEventConfigs)
+ services.fullService(Descriptors.DeleteEventConfigs, deleteEventConfigs)
+
+ services.fullService(Descriptors.EventQuery, eventQuery)
+ services.fullService(Descriptors.GetEvents, getEvents)
+ services.fullService(Descriptors.PostEvents, postEvents)
+
+ services.fullService(Descriptors.AlarmQuery, alarmQuery)
+ services.fullService(Descriptors.PutAlarmState, putAlarmStates)
+
+ services.fullService(Descriptors.SubscribeToEvents, subscribeToEvents)
+ services.fullService(Descriptors.SubscribeToAlarms, subscribeToAlarms)
+
+ def eventConfigQuery(request: EventConfigQueryRequest, headers: Map[String, String], context: ServiceContext): Response[EventConfigQueryResponse] = {
+
+ val filter = context.auth.authorize(eventConfigResource, "read")
+ if (filter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket read permissions on " + eventConfigResource)
+ }
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val query = request.getRequest
+
+ val last = if (query.hasLastEventType) Some(query.getLastEventType) else None
+ val pageSize = if (query.hasPageSize) query.getPageSize else defaultEventConfigPageSize
+
+ val alarmStates = query.getAlarmStateList.toSeq
+ val designations = query.getDesignationList.toSeq
+ val severities = query.getSeverityList.map(_.toInt).toSeq
+ val severityOrHigher = if (query.hasSeverityOrHigher) Some(query.getSeverityOrHigher) else None
+
+ val results = eventAlarmModel.eventConfigQuery(severities, severityOrHigher, designations, alarmStates, last, pageSize)
+
+ val response = EventConfigQueryResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def getEventConfigs(request: GetEventConfigRequest, headers: Map[String, String], context: ServiceContext): Response[GetEventConfigResponse] = {
+
+ val filter = context.auth.authorize(eventConfigResource, "read")
+ if (filter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket read permissions on " + eventConfigResource)
+ }
+
+ if (request.getEventTypeCount == 0) {
+ throw new BadRequestException("Must include at least one event type")
+ }
+
+ val eventTypeList = request.getEventTypeList.toSeq
+
+ val results = eventAlarmModel.getEventConfigs(eventTypeList)
+
+ val response = GetEventConfigResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def putEventConfigs(request: PutEventConfigRequest, headers: Map[String, String], context: ServiceContext): Response[PutEventConfigResponse] = {
+
+ val createFilter = context.auth.authorize(eventConfigResource, "create")
+ if (createFilter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket create permissions on " + eventConfigResource)
+ }
+ val updateFilter = context.auth.authorize(eventConfigResource, "update")
+ if (updateFilter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket update permissions on " + eventConfigResource)
+ }
+
+ if (request.getRequestCount == 0) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val configs = request.getRequestList.toSeq.map { proto =>
+ if (!proto.hasEventType) {
+ throw new BadRequestException("All event configs must include event type")
+ }
+ if (!proto.hasSeverity) {
+ throw new BadRequestException("All event configs must include severity")
+ }
+ if (!proto.hasDesignation) {
+ throw new BadRequestException("All event configs must include designation")
+ }
+ if (!proto.hasResource) {
+ throw new BadRequestException("All event configs must include resource")
+ }
+
+ val alarmState = if (proto.hasAlarmState) proto.getAlarmState else Alarm.State.UNACK_SILENT
+
+ EventAlarmModel.EventConfigTemp(proto.getEventType, proto.getSeverity, proto.getDesignation, alarmState, proto.getResource)
+ }
+
+ val results = eventAlarmModel.putEventConfigs(context.notifier, configs)
+
+ val response = PutEventConfigResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def deleteEventConfigs(request: DeleteEventConfigRequest, headers: Map[String, String], context: ServiceContext): Response[DeleteEventConfigResponse] = {
+
+ val filter = context.auth.authorize(eventConfigResource, "delete")
+ if (filter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket delete permissions on " + eventConfigResource)
+ }
+
+ if (request.getEventTypeCount == 0) {
+ throw new BadRequestException("Must include at least one event type")
+ }
+
+ val eventTypeList = request.getEventTypeList.toSeq
+
+ val results = eventAlarmModel.deleteEventConfigs(context.notifier, eventTypeList)
+
+ val response = DeleteEventConfigResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def getEvents(request: GetEventsRequest, headers: Map[String, String], context: ServiceContext): Response[GetEventsResponse] = {
+
+ val filter = context.auth.authorize(eventResource, "read")
+
+ if (request.getEventIdCount == 0) {
+ throw new BadRequestException("Must include at least one event id")
+ }
+
+ val eventIds = request.getEventIdList.toSeq.map(protoIdToLong)
+
+ val results = eventAlarmModel.getEvents(eventIds, filter)
+
+ val response = GetEventsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def postEvents(request: PostEventsRequest, headers: Map[String, String], context: ServiceContext): Response[PostEventsResponse] = {
+
+ val createFilter = context.auth.authorize(eventResource, "create")
+ if (createFilter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket create permissions on " + eventResource)
+ }
+
+ if (request.getRequestCount == 0) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val protoTemplates = request.getRequestList.toSeq
+
+ val templates = protoTemplates.map { et =>
+ val eventType = if (et.hasEventType) {
+ et.getEventType
+ } else {
+ throw new BadRequestException("Must include event type")
+ }
+
+ val subsystem = if (et.hasSubsystem) Some(et.getSubsystem) else None
+ val deviceTime = if (et.hasDeviceTime) Some(et.getDeviceTime) else None
+
+ val entityUuid = if (et.hasEntityUuid) Some(et.getEntityUuid) else None
+
+ val modelGroup = if (et.hasModelGroup) Some(et.getModelGroup) else None
+
+ val args = et.getArgsList.toSeq
+
+ SysEventTemplate(context.auth.agentName, eventType, subsystem, deviceTime, entityUuid, modelGroup, args)
+ }
+
+ val results = eventAlarmModel.postEvents(context.notifier, templates)
+
+ val response = PostEventsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ private def parseEventQuery(query: EventRequests.EventQueryParams, lastId: Option[Long], pageSize: Int): EventQueryParams = {
+ val eventTypes = query.getEventTypeList.toSeq
+ val timeFrom = if (query.hasTimeFrom) Some(query.getTimeFrom) else None
+ val timeTo = if (query.hasTimeTo) Some(query.getTimeTo) else None
+ val severities = query.getSeverityList.toSeq.map(_.toInt)
+ val severityOrHigher = if (query.hasSeverityOrHigher) Some(query.getSeverityOrHigher) else None
+ val subsystems = query.getSubsystemList.toSeq
+ val agents = query.getAgentList.toSeq
+
+ val modelGroups = query.getModelGroupList.toSeq
+
+ val isAlarm = if (query.hasIsAlarm) Some(query.getIsAlarm) else None
+
+ val (entityUuids, entityNames) = if (query.hasEntities) {
+ (query.getEntities.getUuidsList.toSeq.map(protoUUIDToUuid), query.getEntities.getNamesList.toSeq)
+ } else {
+ (Nil, Nil)
+ }
+
+ val latest = if (query.hasLatest) query.getLatest else true
+
+ EventQueryParams(eventTypes, timeFrom, timeTo, severities, severityOrHigher, subsystems, agents, entityUuids, entityNames, modelGroups, isAlarm, latest, lastId, pageSize)
+ }
+
+ def eventQuery(request: EventQueryRequest, headers: Map[String, String], context: ServiceContext): Response[EventQueryResponse] = {
+
+ val filter = context.auth.authorize(eventResource, "read")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val query = request.getRequest
+
+ val lastId = if (query.hasLastId) Some(query.getLastId.getValue.toLong) else None
+
+ val limit = if (query.hasPageSize) query.getPageSize else defaultEventPageSize
+
+ val params = if (query.hasQueryParams) parseEventQuery(query.getQueryParams, lastId, limit) else EventQueryParams(last = lastId, pageSize = limit)
+
+ val results = eventAlarmModel.eventQuery(params, filter)
+
+ val response = EventQueryResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ private def parseEventSubscriptionQuery(query: EventSubscriptionQuery, filter: Option[EntityFilter]): EventQueryParams = {
+
+ val timeFrom = if (query.hasTimeFrom) Some(query.getTimeFrom) else None
+ val limit = if (query.hasLimit) query.getLimit else defaultEventPageSize
+
+ val eventTypes = query.getEventTypeList.toSeq
+ val severities = query.getSeverityList.toSeq.map(_.toInt)
+ val subsystem = query.getSubsystemList.toSeq
+ val agents = query.getAgentList.toSeq
+
+ val (entityUuids, entityNames) = if (query.hasEntities) {
+ (query.getEntities.getUuidsList.toSeq.map(protoUUIDToUuid), query.getEntities.getNamesList.toSeq)
+ } else {
+ (Nil, Nil)
+ }
+
+ val lookingForEntities = entityUuids.nonEmpty || entityNames.nonEmpty
+
+ if (!lookingForEntities && filter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket read permissions to subscribe without specifying entities")
+ }
+
+ val entUuids = if (lookingForEntities) {
+ entityModel.keyQueryFilter(entityUuids, entityNames, filter)
+ } else {
+ Nil
+ }
+
+ if (lookingForEntities && entUuids.isEmpty) {
+ throw new BadRequestException("Entities to subscribe to not found")
+ }
+
+ EventQueryParams(eventTypes = eventTypes, timeFrom = timeFrom, severities = severities, subsystems = subsystem, agents = agents, entityUuids = entUuids, pageSize = limit, latest = true)
+ }
+
+ def subscribeToEvents(request: SubscribeEventsRequest, headers: Map[String, String], context: ServiceContext): Response[SubscribeEventsResponse] = {
+
+ val filter = context.auth.authorize(eventResource, "read")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val queue = headers.get("__subscription").getOrElse {
+ throw new BadRequestException("Must include subscription queue in headers")
+ }
+
+ val query = request.getRequest
+
+ val timeFrom = if (query.hasTimeFrom) Some(query.getTimeFrom) else None
+ val limit = if (query.hasLimit) query.getLimit else defaultEventPageSize
+
+ val hasParams = query.getEventTypeCount != 0 ||
+ query.getSeverityCount != 0 ||
+ query.getSubsystemCount != 0 ||
+ query.getAgentCount != 0 ||
+ (query.hasEntities && (query.getEntities.getUuidsCount != 0 || query.getEntities.getNamesCount != 0))
+
+ val results = if (!hasParams) {
+
+ if (filter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket read permissions to subscribe without specifying entities")
+ }
+
+ eventBinding.bindAll(queue)
+
+ eventAlarmModel.eventQuery(EventQueryParams(timeFrom = timeFrom, pageSize = limit, latest = true))
+
+ } else {
+
+ val params = parseEventSubscriptionQuery(query, filter)
+
+ eventBinding.bindEach(queue, Seq(params.eventTypes, params.severities.map(_.toString), params.subsystems, params.agents, params.entityUuids.map(_.toString)))
+
+ eventAlarmModel.eventQuery(params)
+ }
+
+ val response = SubscribeEventsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def alarmQuery(request: AlarmQueryRequest, headers: Map[String, String], context: ServiceContext): Response[AlarmQueryResponse] = {
+
+ val filter = context.auth.authorize(alarmResource, "read")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val query = request.getRequest
+
+ val lastId = if (query.hasLastId) Some(query.getLastId.getValue.toLong) else None
+
+ val limit = if (query.hasPageSize) query.getPageSize else defaultEventPageSize
+
+ val params = if (query.hasEventQueryParams) parseEventQuery(query.getEventQueryParams, lastId, limit) else EventQueryParams(last = lastId, pageSize = limit)
+
+ val alarmStates = query.getAlarmStatesList.toSeq
+
+ val results = eventAlarmModel.alarmQuery(alarmStates, params, filter)
+
+ val response = AlarmQueryResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def subscribeToAlarms(request: SubscribeAlarmsRequest, headers: Map[String, String], context: ServiceContext): Response[SubscribeAlarmsResponse] = {
+
+ val filter = context.auth.authorize(alarmResource, "read")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val queue = headers.get("__subscription").getOrElse {
+ throw new BadRequestException("Must include subscription queue in headers")
+ }
+
+ val query = request.getRequest
+
+ val states = query.getAlarmStatesList.toSeq
+
+ val results = if (query.hasEventQuery) {
+ val eventQuery = query.getEventQuery
+
+ val hasEventParams =
+ eventQuery.getEventTypeCount != 0 ||
+ eventQuery.getSeverityCount != 0 ||
+ eventQuery.getSubsystemCount != 0 ||
+ eventQuery.getAgentCount != 0 ||
+ (eventQuery.hasEntities && (eventQuery.getEntities.getUuidsCount != 0 || eventQuery.getEntities.getNamesCount != 0))
+
+ val hasParams = states.nonEmpty || hasEventParams
+
+ if (!hasParams) {
+
+ if (filter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket read permissions to subscribe without specifying entities")
+ }
+
+ alarmBinding.bindAll(queue)
+
+ eventAlarmModel.alarmQuery(states, parseEventSubscriptionQuery(eventQuery, None))
+
+ } else {
+
+ if (!hasEventParams) {
+ throw new BadRequestException("Must include request parameters if not subscribing to all")
+ }
+
+ val params = parseEventSubscriptionQuery(eventQuery, filter)
+
+ alarmBinding.bindEach(queue, Seq(states.map(_.toString), params.eventTypes, params.severities.map(_.toString), params.subsystems, params.agents, params.entityUuids.map(_.toString)))
+
+ eventAlarmModel.alarmQuery(states, params)
+ }
+
+ } else {
+
+ if (states.isEmpty) {
+ throw new BadRequestException("Must include request parameters when not subscribing to all")
+ }
+
+ if (filter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket read permissions to subscribe without specifying entities")
+ }
+
+ alarmBinding.bindEach(queue, Seq(states.map(_.toString), Nil, Nil, Nil, Nil, Nil))
+
+ eventAlarmModel.alarmQuery(states, EventQueryParams(pageSize = defaultEventPageSize))
+ }
+
+ val response = SubscribeAlarmsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def putAlarmStates(request: PutAlarmStateRequest, headers: Map[String, String], context: ServiceContext): Response[PutAlarmStateResponse] = {
+
+ val updateFilter = context.auth.authorize(alarmResource, "update")
+ if (updateFilter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket create permissions on " + eventResource)
+ }
+
+ if (request.getRequestCount == 0) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val updates = request.getRequestList.toSeq.map { update =>
+ if (!update.hasAlarmId) {
+ throw new BadRequestException("Must include alarm ID")
+ }
+ if (!update.hasAlarmState) {
+ throw new BadRequestException("Must include alarm state")
+ }
+ (update.getAlarmId.getValue.toLong, update.getAlarmState)
+ }
+
+ val results = eventAlarmModel.putAlarmStates(context.notifier, updates)
+
+ val response = PutAlarmStateResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+}
+
+object EventSubscriptionDescriptor extends SubscriptionDescriptor[Event] {
+ def keyParts(payload: Event): Seq[String] = {
+ Seq(payload.getEventType, payload.getSeverity.toString, payload.getSubsystem, payload.getAgentName, payload.getEntityUuid.getValue)
+ }
+
+ def notification(eventType: SubscriptionEventType, payload: Event): Array[Byte] = {
+ EventNotification.newBuilder
+ .setValue(payload)
+ .build()
+ .toByteArray
+ }
+}
+
+object AlarmSubscriptionDescriptor extends SubscriptionDescriptor[Alarm] {
+ def keyParts(payload: Alarm): Seq[String] = {
+ Seq(payload.getState.toString, payload.getEvent.getEventType, payload.getEvent.getSeverity.toString, payload.getEvent.getSubsystem, payload.getEvent.getAgentName, payload.getEvent.getEntityUuid.getValue)
+ }
+
+ def notification(eventType: SubscriptionEventType, payload: Alarm): Array[Byte] = {
+ AlarmNotification.newBuilder
+ .setValue(payload)
+ .setEventType(eventType)
+ .build()
+ .toByteArray
+ }
+}
diff --git a/services/src/main/scala/io/greenbus/services/core/FrontEndServices.scala b/services/src/main/scala/io/greenbus/services/core/FrontEndServices.scala
new file mode 100644
index 0000000..937baa0
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/core/FrontEndServices.scala
@@ -0,0 +1,176 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.core
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.msg.amqp.AmqpServiceOperations
+import io.greenbus.client.exception.{ BadRequestException, ForbiddenException }
+import io.greenbus.client.proto.Envelope
+import io.greenbus.client.proto.Envelope.SubscriptionEventType
+import io.greenbus.client.service.FrontEndService
+import io.greenbus.client.service.proto.FrontEnd._
+import io.greenbus.client.service.proto.FrontEndRequests._
+import io.greenbus.client.service.proto.Model._
+import io.greenbus.services.framework.{ ServiceContext, Success, _ }
+import io.greenbus.services.model.FrontEndModel
+import io.greenbus.services.model.UUIDHelpers._
+
+import scala.collection.JavaConversions._
+
+object FrontEndServices {
+
+ val frontEndRegistrationResource = "frontEndRegistration"
+ val frontEndConnectionStatusResource = "frontEndConnectionStatus"
+
+ val defaultPageSize = 200
+}
+
+class FrontEndServices(
+ services: ServiceRegistry,
+ ops: AmqpServiceOperations,
+ frontEndModel: FrontEndModel,
+ frontEndConnectionStatusBinding: SubscriptionChannelBinder) extends Logging {
+ import io.greenbus.services.core.FrontEndServices._
+
+ services.fullService(FrontEndService.Descriptors.GetFrontEndConnectionStatuses, getFrontEndConnectionStatuses)
+ services.fullService(FrontEndService.Descriptors.SubscribeToFrontEndConnectionStatuses, subscribeFrontEndConnectionStatuses)
+
+ def getFrontEndConnectionStatuses(request: GetFrontEndConnectionStatusRequest, headers: Map[String, String], context: ServiceContext): Response[GetFrontEndConnectionStatusResponse] = {
+
+ val filter = context.auth.authorize(frontEndConnectionStatusResource, "read")
+
+ if (!request.hasEndpoints) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val keySet = request.getEndpoints
+
+ if (keySet.getUuidsCount == 0 && keySet.getNamesCount == 0) {
+ throw new BadRequestException("Must include at least one id or name")
+ }
+
+ val uuids = keySet.getUuidsList.toSeq.map(protoUUIDToUuid)
+ val names = keySet.getNamesList.toSeq
+
+ val results = frontEndModel.getFrontEndConnectionStatuses(uuids, names, filter)
+
+ val response = GetFrontEndConnectionStatusResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def subscribeFrontEndConnectionStatuses(request: SubscribeFrontEndConnectionStatusRequest, headers: Map[String, String], context: ServiceContext): Response[SubscribeFrontEndConnectionStatusResponse] = {
+
+ val filter = context.auth.authorize(frontEndConnectionStatusResource, "read")
+
+ if (!request.hasEndpoints) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val queue = headers.get("__subscription").getOrElse {
+ throw new BadRequestException("Must include subscription queue in headers")
+ }
+
+ val keySet = request.getEndpoints
+
+ val results = if (keySet.getUuidsCount != 0 || keySet.getNamesCount != 0) {
+
+ val uuids = keySet.getUuidsList.toSeq.map(protoUUIDToUuid)
+ val names = keySet.getNamesList.toSeq
+
+ val endpoints = frontEndModel.endpointKeyQuery(uuids, names, filter)
+
+ val getResults = frontEndModel.getFrontEndConnectionStatuses(uuids, names, filter)
+
+ val filteredUuids = endpoints.map(_.getUuid)
+
+ frontEndConnectionStatusBinding.bindEach(queue, Seq(filteredUuids.map(_.getValue)))
+
+ getResults
+
+ } else {
+
+ if (filter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket read permissions on entities when subscribing to all")
+ }
+
+ frontEndConnectionStatusBinding.bindAll(queue)
+
+ Nil
+ }
+
+ val response = SubscribeFrontEndConnectionStatusResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+}
+
+object EndpointSubscriptionDescriptor extends SubscriptionDescriptor[Endpoint] {
+ def keyParts(payload: Endpoint): Seq[String] = {
+ Seq(payload.getUuid.getValue, payload.getName, payload.getProtocol)
+ }
+
+ def notification(eventType: SubscriptionEventType, payload: Endpoint): Array[Byte] = {
+ EndpointNotification.newBuilder
+ .setValue(payload)
+ .setEventType(eventType)
+ .build()
+ .toByteArray
+ }
+}
+object PointSubscriptionDescriptor extends SubscriptionDescriptor[Point] {
+ def keyParts(payload: Point): Seq[String] = {
+ val endpointUuid = if (payload.hasEndpointUuid) payload.getEndpointUuid.getValue else ""
+ Seq(endpointUuid, payload.getUuid.getValue, payload.getName)
+ }
+
+ def notification(eventType: SubscriptionEventType, payload: Point): Array[Byte] = {
+ PointNotification.newBuilder
+ .setValue(payload)
+ .setEventType(eventType)
+ .build()
+ .toByteArray
+ }
+}
+object CommandSubscriptionDescriptor extends SubscriptionDescriptor[Command] {
+ def keyParts(payload: Command): Seq[String] = {
+ val endpointUuid = if (payload.hasEndpointUuid) payload.getEndpointUuid.getValue else ""
+ Seq(endpointUuid, payload.getUuid.getValue, payload.getName)
+ }
+
+ def notification(eventType: SubscriptionEventType, payload: Command): Array[Byte] = {
+ CommandNotification.newBuilder
+ .setValue(payload)
+ .setEventType(eventType)
+ .build()
+ .toByteArray
+ }
+}
+
+object FrontEndConnectionStatusSubscriptionDescriptor extends SubscriptionDescriptor[FrontEndConnectionStatus] {
+ def keyParts(payload: FrontEndConnectionStatus): Seq[String] = {
+ Seq(payload.getEndpointUuid.getValue)
+ }
+
+ def notification(eventType: SubscriptionEventType, payload: FrontEndConnectionStatus): Array[Byte] = {
+ FrontEndConnectionStatusNotification.newBuilder
+ .setValue(payload)
+ .setEventType(eventType)
+ .build()
+ .toByteArray
+ }
+}
diff --git a/services/src/main/scala/io/greenbus/services/core/LoginServices.scala b/services/src/main/scala/io/greenbus/services/core/LoginServices.scala
new file mode 100644
index 0000000..4ebabed
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/core/LoginServices.scala
@@ -0,0 +1,253 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.core
+
+import java.util.UUID
+import java.util.concurrent.TimeoutException
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.services.{ AsyncAuthenticationModule, SqlAuthenticationModule, AuthenticationModule }
+import io.greenbus.services.framework._
+import io.greenbus.client.service.proto.LoginRequests._
+import io.greenbus.client.exception.{ UnauthorizedException, BadRequestException }
+import io.greenbus.client.proto.Envelope
+import io.greenbus.services.model.EventAlarmModel.SysEventTemplate
+import io.greenbus.sql.DbConnection
+import io.greenbus.services.model.{ UUIDHelpers, EventSeeding, EventAlarmModel, AuthModel }
+import io.greenbus.client.version.Version
+
+import scala.concurrent.ExecutionContext.Implicits.global
+
+object LoginServices {
+ val defaultExpirationLength = 18144000000L // one month
+
+}
+
+class LoginServices(services: ServiceRegistry, sql: DbConnection, authenticator: AuthenticationModule, authModel: AuthModel, eventModel: EventAlarmModel, modelNotifier: ModelNotifier) extends Logging {
+ import LoginServices._
+ import io.greenbus.client.service.LoginService.Descriptors
+
+ //services.simpleSync(Descriptors.Login, login)
+ services.simpleAsync(Descriptors.Login, loginAsync)
+ services.simpleSync(Descriptors.Logout, logout)
+ services.simpleSync(Descriptors.Validate, validateAuthToken)
+
+ def loginAsync(request: PostLoginRequest, headers: Map[String, String], responseHandler: Response[PostLoginResponse] => Unit): Unit = {
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val login = request.getRequest
+
+ if (!login.hasName) {
+ throw new BadRequestException("Must include user name.")
+ }
+ if (!login.hasPassword) {
+ throw new BadRequestException("Must include password.")
+ }
+
+ val currentTime = System.currentTimeMillis()
+ val expiration = if (login.hasExpirationTime) {
+ if (login.getExpirationTime < currentTime) {
+ throw new BadRequestException("Expiration time cannot be in the past")
+ }
+ login.getExpirationTime
+ } else {
+ currentTime + defaultExpirationLength
+ }
+
+ val clientVersion = if (login.hasClientVersion) login.getClientVersion else "unknown"
+ val location = if (login.hasLoginLocation) login.getLoginLocation else "unknown"
+
+ val error = "Invalid username or password"
+
+ def enterAuthorization(agentId: UUID): Response[PostLoginResponse] = {
+ val token = authModel.createTokenAndStore(agentId, expiration, clientVersion, location)
+
+ val attrs = EventAlarmModel.mapToAttributesList(Seq(("user", login.getName)))
+ val event = SysEventTemplate(login.getName, EventSeeding.System.userLogin.eventType, Some("auth"), None, Some(UUIDHelpers.uuidToProtoUUID(agentId)), None, attrs)
+ eventModel.postEvents(modelNotifier, Seq(event))
+
+ val resp = LoginResponse.newBuilder
+ .setToken(token)
+ .setExpirationTime(expiration)
+ .setServerVersion(Version.clientVersion)
+ .build()
+
+ Success(Envelope.Status.OK, PostLoginResponse.newBuilder().setResponse(resp).build())
+ }
+
+ def postFailureEvent(): Unit = {
+ val attrs = EventAlarmModel.mapToAttributesList(Seq(("reason", error)))
+ val event = SysEventTemplate(login.getName, EventSeeding.System.userLoginFailure.eventType, Some("auth"), None, None, None, attrs)
+ eventModel.postEvents(modelNotifier, Seq(event))
+ }
+
+ authenticator match {
+
+ case sqlModule: SqlAuthenticationModule => {
+
+ val response = sql.transaction {
+ val (authenticated, uuidOpt) = sqlModule.authenticate(login.getName, login.getPassword)
+ (authenticated, uuidOpt) match {
+ case (true, Some(uuid)) => enterAuthorization(uuid)
+ case _ =>
+ postFailureEvent()
+ Failure(Envelope.Status.UNAUTHORIZED, error)
+ }
+ }
+ responseHandler(response)
+ }
+
+ case asyncModule: AsyncAuthenticationModule => {
+
+ val authFut = asyncModule.authenticate(login.getName, login.getPassword)
+
+ authFut.onFailure {
+ case ex: TimeoutException =>
+ logger.warn("Response timeout from authentication service")
+ sql.transaction {
+ val attrs = EventAlarmModel.mapToAttributesList(Seq(("reason", "Authentication service timeout")))
+ val event = SysEventTemplate(login.getName, EventSeeding.System.userLoginFailure.eventType, Some("auth"), None, None, None, attrs)
+ eventModel.postEvents(modelNotifier, Seq(event))
+ }
+ responseHandler(Failure(Envelope.Status.RESPONSE_TIMEOUT, "Response timeout from front end connection"))
+ case ex: Throwable =>
+ logger.warn("Authentication service returned unexpected error: " + ex)
+ sql.transaction {
+ postFailureEvent()
+ }
+ responseHandler(Failure(Envelope.Status.UNAUTHORIZED, error))
+ }
+
+ authFut.onSuccess {
+ case true =>
+ val response = sql.transaction {
+ authModel.agentIdForName(login.getName) match {
+ case None =>
+ postFailureEvent()
+ Failure(Envelope.Status.UNAUTHORIZED, error)
+ case Some(agentId) =>
+ enterAuthorization(agentId)
+ }
+ }
+ responseHandler(response)
+
+ case false =>
+ sql.transaction {
+ postFailureEvent()
+ }
+ responseHandler(Failure(Envelope.Status.UNAUTHORIZED, error))
+ }
+ }
+ }
+
+ }
+
+ def login(request: PostLoginRequest, headers: Map[String, String]): Response[PostLoginResponse] = {
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val login = request.getRequest
+
+ if (!login.hasName) {
+ throw new BadRequestException("Must include user name.")
+ }
+ if (!login.hasPassword) {
+ throw new BadRequestException("Must include password.")
+ }
+
+ val currentTime = System.currentTimeMillis()
+ val expiration = if (login.hasExpirationTime) {
+ if (login.getExpirationTime < currentTime) {
+ throw new BadRequestException("Expiration time cannot be in the past")
+ }
+ login.getExpirationTime
+ } else {
+ currentTime + defaultExpirationLength
+ }
+
+ val clientVersion = if (login.hasClientVersion) login.getClientVersion else "unknown"
+ val location = if (login.hasLoginLocation) login.getLoginLocation else "unknown"
+
+ sql.transaction {
+ val result = authModel.simpleLogin(login.getName, login.getPassword, expiration, clientVersion, location)
+ result match {
+ case Left(error) =>
+
+ val attrs = EventAlarmModel.mapToAttributesList(Seq(("reason", error)))
+ val event = SysEventTemplate(login.getName, EventSeeding.System.userLoginFailure.eventType, Some("auth"), None, None, None, attrs)
+ eventModel.postEvents(modelNotifier, Seq(event))
+
+ throw new UnauthorizedException(error)
+ case Right((token, uuid)) => {
+
+ val attrs = EventAlarmModel.mapToAttributesList(Seq(("user", login.getName)))
+ val event = SysEventTemplate(login.getName, EventSeeding.System.userLogin.eventType, Some("auth"), None, Some(uuid), None, attrs)
+ eventModel.postEvents(modelNotifier, Seq(event))
+
+ val resp = LoginResponse.newBuilder
+ .setToken(token)
+ .setExpirationTime(expiration)
+ .setServerVersion(Version.clientVersion)
+ .build()
+
+ Success(Envelope.Status.OK, PostLoginResponse.newBuilder().setResponse(resp).build())
+ }
+ }
+ }
+ }
+
+ def logout(request: PostLogoutRequest, headers: Map[String, String]): Response[PostLogoutResponse] = {
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val tokenRequest = request.getRequest
+ if (!tokenRequest.hasToken) {
+ throw new BadRequestException("Must include auth token to be logged out")
+ }
+
+ sql.transaction {
+ authModel.simpleLogout(tokenRequest.getToken)
+ val event = SysEventTemplate("-", EventSeeding.System.userLogout.eventType, Some("auth"), None, None, None, Seq())
+ eventModel.postEvents(modelNotifier, Seq(event))
+ }
+
+ Success(Envelope.Status.OK, PostLogoutResponse.newBuilder().build())
+ }
+
+ def validateAuthToken(request: ValidateAuthTokenRequest, headers: Map[String, String]): Response[ValidateAuthTokenResponse] = {
+ if (!request.hasToken) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val result = sql.transaction {
+ authModel.authValidate(request.getToken)
+ }
+
+ result match {
+ case false => Failure(Envelope.Status.UNAUTHORIZED, "Invalid auth token")
+ case true => Success(Envelope.Status.OK, ValidateAuthTokenResponse.newBuilder().build())
+ }
+ }
+}
diff --git a/services/src/main/scala/io/greenbus/services/core/MeasurementServices.scala b/services/src/main/scala/io/greenbus/services/core/MeasurementServices.scala
new file mode 100644
index 0000000..885f81a
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/core/MeasurementServices.scala
@@ -0,0 +1,191 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.core
+
+import io.greenbus.services.framework._
+import io.greenbus.client.service.proto.MeasurementRequests._
+import io.greenbus.client.service.MeasurementService.Descriptors
+import io.greenbus.client.exception.{ ForbiddenException, BadRequestException }
+import io.greenbus.services.model.UUIDHelpers._
+import scala.collection.JavaConversions._
+import io.greenbus.client.proto.Envelope
+import io.greenbus.mstore.{ MeasurementHistorySource, MeasurementValueSource }
+import io.greenbus.client.service.proto.Measurements.{ PointMeasurementValues, PointMeasurementValue }
+import io.greenbus.services.framework.ServiceContext
+import io.greenbus.services.framework.Success
+import java.util.UUID
+import io.greenbus.services.model.FrontEndModel
+
+object MeasurementServices {
+
+ val measurementResource = "measurement"
+
+ val defaultHistoryLimit = 200
+}
+
+class MeasurementServices(services: ServiceRegistry, store: MeasurementValueSource, history: MeasurementHistorySource, measChannel: SubscriptionChannelBinder, measBatchChannel: SubscriptionChannelBinder, frontEndModel: FrontEndModel) {
+ import MeasurementServices._
+
+ services.fullService(Descriptors.GetCurrentValues, getCurrentValue)
+ services.fullService(Descriptors.GetCurrentValuesAndSubscribe, subscribeWithCurrentValue)
+ services.fullService(Descriptors.GetHistory, getHistory)
+ services.fullService(Descriptors.SubscribeToBatches, subscribeToBatches)
+
+ def getCurrentValue(request: GetCurrentValuesRequest, headers: Map[String, String], context: ServiceContext): Response[GetCurrentValuesResponse] = {
+
+ val optFilter = context.auth.authorize(measurementResource, "read")
+
+ if (request.getPointUuidsCount == 0) {
+ throw new BadRequestException("Must include at least one point uuid")
+ }
+
+ val uuids = request.getPointUuidsList.toSeq.map(protoUUIDToUuid)
+
+ val filteredUuids = optFilter match {
+ case None => uuids
+ case Some(filter) => filter.filter(uuids)
+ }
+
+ val results = store.get(filteredUuids)
+
+ val protoResults = results.map {
+ case (id, meas) =>
+ PointMeasurementValue.newBuilder
+ .setPointUuid(uuidToProtoUUID(id))
+ .setValue(meas)
+ .build
+ }
+
+ val response = GetCurrentValuesResponse.newBuilder().addAllResults(protoResults).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def subscribeWithCurrentValue(request: GetCurrentValuesAndSubscribeRequest, headers: Map[String, String], context: ServiceContext): Response[GetCurrentValuesAndSubscribeResponse] = {
+
+ val optFilter = context.auth.authorize(measurementResource, "read")
+
+ val queue = headers.get("__subscription").getOrElse {
+ throw new BadRequestException("Must include subscription queue in headers")
+ }
+
+ val protoResults = if (request.getPointUuidsCount == 0) {
+
+ if (optFilter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket read permissions when subscribing to all")
+ }
+
+ measChannel.bindAll(queue)
+
+ Seq()
+
+ } else {
+
+ val uuids = request.getPointUuidsList.toSeq.map(protoUUIDToUuid)
+
+ val filteredUuids = optFilter match {
+ case None => uuids
+ case Some(filter) => filter.filter(uuids)
+ }
+
+ val results = store.get(filteredUuids)
+
+ val protoResults = results.map {
+ case (id, meas) =>
+ PointMeasurementValue.newBuilder
+ .setPointUuid(uuidToProtoUUID(id))
+ .setValue(meas)
+ .build
+ }
+
+ if (filteredUuids.nonEmpty) {
+ measChannel.bindEach(queue, Seq(filteredUuids.map(_.toString), Nil))
+ }
+
+ protoResults
+ }
+
+ val response = GetCurrentValuesAndSubscribeResponse.newBuilder().addAllResults(protoResults).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def getHistory(request: GetMeasurementHistoryRequest, headers: Map[String, String], context: ServiceContext): Response[GetMeasurementHistoryResponse] = {
+
+ val optFilter = context.auth.authorize(measurementResource, "read")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request query")
+ }
+
+ val query = request.getRequest
+
+ if (!query.hasPointUuid) {
+ throw new BadRequestException("Must include point uuid")
+ }
+
+ val pointId: UUID = query.getPointUuid
+
+ if (!frontEndModel.pointExists(pointId, optFilter)) {
+ throw new BadRequestException("No point exists for uuid")
+ }
+
+ val windowStart = if (query.hasTimeFrom) Some(query.getTimeFrom) else None
+ val windowEnd = if (query.hasTimeTo) Some(query.getTimeTo) else None
+ val limit = if (query.hasLimit) query.getLimit else defaultHistoryLimit
+ val latest = if (query.hasLatest) query.getLatest else false
+
+ val results = history.getHistory(pointId, windowStart, windowEnd, limit, latest)
+
+ val result = PointMeasurementValues.newBuilder()
+ .setPointUuid(query.getPointUuid)
+ .addAllValue(results)
+ .build()
+
+ val response = GetMeasurementHistoryResponse.newBuilder().setResult(result).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def subscribeToBatches(request: SubscribeToBatchesRequest, headers: Map[String, String], context: ServiceContext): Response[SubscribeToBatchesResponse] = {
+
+ val optFilter = context.auth.authorize(measurementResource, "read")
+
+ if (optFilter.nonEmpty) {
+ throw new ForbiddenException(s"Must have blanked read permissions for $measurementResource to subscribe to batches")
+ }
+
+ val queue = headers.get("__subscription").getOrElse {
+ throw new BadRequestException("Must include subscription queue in headers")
+ }
+
+ if (!request.hasQuery) {
+ throw new BadRequestException("Must include request query")
+ }
+
+ val endpointUuids = request.getQuery.getEndpointUuidsList.toSeq
+
+ if (endpointUuids.nonEmpty) {
+ measBatchChannel.bindEach(queue, Seq(endpointUuids.map(_.getValue)))
+ } else {
+ measBatchChannel.bindAll(queue)
+ }
+
+ val response = SubscribeToBatchesResponse.newBuilder().build
+ Success(Envelope.Status.OK, response)
+ }
+
+}
diff --git a/services/src/main/scala/io/greenbus/services/core/ModelServices.scala b/services/src/main/scala/io/greenbus/services/core/ModelServices.scala
new file mode 100644
index 0000000..d6b3eec
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/core/ModelServices.scala
@@ -0,0 +1,1286 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.core
+
+import java.util.UUID
+
+import io.greenbus.client.exception.{ BadRequestException, ForbiddenException }
+import io.greenbus.client.proto.Envelope
+import io.greenbus.client.proto.Envelope.SubscriptionEventType
+import io.greenbus.client.service.proto.Model._
+import io.greenbus.client.service.proto.ModelRequests._
+import io.greenbus.services.framework._
+import io.greenbus.services.model.EntityModel.TypeParams
+import io.greenbus.services.model.EventAlarmModel.SysEventTemplate
+import io.greenbus.services.model.FrontEndModel.{ CommandInfo, EndpointInfo, PointInfo }
+import io.greenbus.services.model.UUIDHelpers._
+import io.greenbus.services.model._
+
+import scala.collection.JavaConversions._
+
+object EntityServices {
+
+ val entityResource = "entity"
+ val edgeResource = "entity_edge"
+
+ val keyValueResource = "entity_key_value"
+
+ val pointResource = "point"
+ val commandResource = "command"
+ val endpointResource = "endpoint"
+
+ val defaultPageSize = 200
+
+ def validateEntityName(name: String) {
+ name.foreach {
+ case ' ' => throw new BadRequestException("Entity names must not include spaces.")
+ case '*' => throw new BadRequestException("Entity names must not include banned character '*'.")
+ case _ =>
+ }
+ }
+
+ def parsePageParams(hasPagingParams: => Boolean, getPagingParams: => EntityPagingParams): (Boolean, Option[UUID], Option[String], Int) = {
+ if (hasPagingParams) {
+ val pageByName = !getPagingParams.hasPageByName || getPagingParams.getPageByName
+ val lastUuid = if (getPagingParams.hasLastUuid) Some(protoUUIDToUuid(getPagingParams.getLastUuid)) else None
+ val lastName = if (getPagingParams.hasLastName) Some(getPagingParams.getLastName) else None
+ val pageSize = if (getPagingParams.hasPageSize) getPagingParams.getPageSize else defaultPageSize
+ (pageByName, lastUuid, lastName, pageSize)
+ } else {
+ (true, None, None, defaultPageSize)
+ }
+ }
+}
+
+class EntityServices(services: ServiceRegistry,
+ entityModel: EntityModel,
+ frontEndModel: FrontEndModel,
+ eventModel: EventAlarmModel,
+ entityBinding: SubscriptionChannelBinder,
+ keyValueBinding: SubscriptionChannelBinder,
+ edgeBinding: SubscriptionChannelBinder,
+ endpointBinding: SubscriptionChannelBinder,
+ pointBinding: SubscriptionChannelBinder,
+ commandBinding: SubscriptionChannelBinder) {
+
+ import io.greenbus.client.service.ModelService.Descriptors
+ import io.greenbus.services.core.EntityServices._
+
+ services.fullService(Descriptors.Get, getEntities)
+ services.fullService(Descriptors.EntityQuery, entityQuery)
+ services.fullService(Descriptors.Subscribe, subscribeToEntities)
+ services.fullService(Descriptors.RelationshipFlatQuery, relationshipFlatQuery)
+ services.fullService(Descriptors.Put, putEntities)
+ services.fullService(Descriptors.Delete, deleteEntities)
+
+ services.fullService(Descriptors.EdgeQuery, edgeQuery)
+ services.fullService(Descriptors.PutEdges, putEntityEdges)
+ services.fullService(Descriptors.DeleteEdges, deleteEntityEdges)
+ services.fullService(Descriptors.SubscribeToEdges, subscribeToEntityEdges)
+
+ services.fullService(Descriptors.GetEntityKeyValues, getEntityKeyValues)
+ services.fullService(Descriptors.GetEntityKeys, getEntityKeys)
+ services.fullService(Descriptors.PutEntityKeyValues, putEntityKeyValues)
+ services.fullService(Descriptors.DeleteEntityKeyValues, deleteEntityKeyValues)
+ services.fullService(Descriptors.SubscribeToEntityKeyValues, subscribeToEntityKeyValues)
+
+ services.fullService(Descriptors.GetPoints, getPoints)
+ services.fullService(Descriptors.PointQuery, pointQuery)
+ services.fullService(Descriptors.PutPoints, putPoints)
+ services.fullService(Descriptors.DeletePoints, deletePoints)
+ services.fullService(Descriptors.SubscribeToPoints, subscribeToPoints)
+ services.fullService(Descriptors.GetCommands, getCommands)
+ services.fullService(Descriptors.CommandQuery, commandQuery)
+ services.fullService(Descriptors.PutCommands, putCommands)
+ services.fullService(Descriptors.DeleteCommands, deleteCommands)
+ services.fullService(Descriptors.SubscribeToCommands, subscribeToCommands)
+ services.fullService(Descriptors.GetEndpoints, getEndpoints)
+ services.fullService(Descriptors.EndpointQuery, endpointQuery)
+ services.fullService(Descriptors.PutEndpoints, putEndpoints)
+ services.fullService(Descriptors.PutEndpointDisabled, putEndpointsDisabled)
+ services.fullService(Descriptors.DeleteEndpoints, deleteEndpoints)
+ services.fullService(Descriptors.SubscribeToEndpoints, subscribeToEndpoints)
+
+ def getEntities(request: GetEntitiesRequest, headers: Map[String, String], context: ServiceContext): Response[GetEntitiesResponse] = {
+ val filter = context.auth.authorize(entityResource, "read")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content.")
+ }
+
+ val keySet = request.getRequest
+
+ if (keySet.getUuidsCount == 0 && keySet.getNamesCount == 0) {
+ throw new BadRequestException("Must include at least one id or name.")
+ }
+
+ val uuids = keySet.getUuidsList.toSeq.map(protoUUIDToUuid)
+ val names = keySet.getNamesList.toSeq
+
+ val results = entityModel.keyQuery(uuids, names, filter)
+
+ val response = GetEntitiesResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def entityQuery(request: EntityQueryRequest, headers: Map[String, String], context: ServiceContext): Response[EntityQueryResponse] = {
+ val filter = context.auth.authorize(entityResource, "read")
+
+ if (!request.hasQuery) {
+ throw new BadRequestException("Must include request content.")
+ }
+
+ val query = request.getQuery
+
+ val typeParams = if (query.hasTypeParams) {
+ TypeParams(
+ query.getTypeParams.getIncludeTypesList.toSeq,
+ query.getTypeParams.getMatchTypesList.toSeq,
+ query.getTypeParams.getFilterOutTypesList.toSeq)
+ } else {
+ TypeParams(Nil, Nil, Nil)
+ }
+
+ val (pageByName, lastUuid, lastName, pageSize) = parsePageParams(query.hasPagingParams, query.getPagingParams)
+
+ val results = entityModel.fullQuery(typeParams, lastUuid, lastName, pageSize, pageByName, filter)
+
+ val response = EntityQueryResponse.newBuilder().addAllResults(results).build
+
+ Success(Envelope.Status.OK, response)
+ }
+
+ def subscribeToEntities(request: SubscribeEntitiesRequest, headers: Map[String, String], context: ServiceContext): Response[SubscribeEntitiesResponse] = {
+ val filter = context.auth.authorize(entityResource, "read")
+
+ if (!request.hasQuery) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val query = request.getQuery
+
+ val queue = headers.get("__subscription").getOrElse {
+ throw new BadRequestException("Must include subscription queue in headers")
+ }
+
+ val immediateResults = if (query.getUuidsCount == 0 && query.getNamesCount == 0) {
+
+ if (filter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket read permissions on entities when subscribing to all entities")
+ }
+
+ entityBinding.bindAll(queue)
+ Nil
+
+ } else {
+
+ val names = query.getNamesList.toSeq
+ val uuids = query.getUuidsList.toSeq.map(protoUUIDToUuid)
+
+ val results = entityModel.keyQuery(uuids, names, filter)
+
+ val filteredUuids = results.map(_.getUuid.getValue)
+
+ if (filteredUuids.nonEmpty) {
+ entityBinding.bindEach(queue, Seq(filteredUuids))
+ }
+ results
+ }
+
+ val resp = SubscribeEntitiesResponse.newBuilder().addAllResults(immediateResults).build()
+
+ Success(Envelope.Status.OK, resp)
+ }
+
+ def relationshipFlatQuery(request: EntityRelationshipFlatQueryRequest, headers: Map[String, String], context: ServiceContext): Response[EntityRelationshipFlatQueryResponse] = {
+
+ val filter = context.auth.authorize(edgeResource, "read")
+
+ val query = request.getQuery
+ val options = Seq(
+ query.getStartUuidsCount > 0,
+ query.getStartNamesCount > 0,
+ query.getStartTypesCount > 0).filter(b => b)
+
+ if (options.size == 0) {
+ throw new BadRequestException("Must include one of ids, names, or types to search for.")
+ }
+ if (options.size > 1) {
+ throw new BadRequestException("Must include only one of ids, names, or types to search for.")
+ }
+
+ if (!query.hasRelationship) {
+ throw new BadRequestException("Must include relationship.")
+ }
+ if (!query.hasDescendantOf) {
+ throw new BadRequestException("Must include direction.")
+ }
+
+ val descendantTypes = query.getEndTypesList.toSeq
+ val depthLimit = if (query.hasDepthLimit) Some(query.getDepthLimit) else None
+
+ val (pageByName, lastUuid, lastName, pageSize) = parsePageParams(query.hasPagingParams, query.getPagingParams)
+
+ val entities = if (query.getStartUuidsCount > 0) {
+ entityModel.idsRelationFlatQuery(
+ query.getStartUuidsList.map(protoUUIDToUuid).toSeq,
+ query.getRelationship,
+ query.getDescendantOf,
+ descendantTypes,
+ depthLimit,
+ lastUuid,
+ lastName,
+ pageSize,
+ pageByName,
+ filter)
+ } else if (query.getStartNamesCount > 0) {
+ entityModel.namesRelationFlatQuery(
+ query.getStartNamesList.toSeq,
+ query.getRelationship,
+ query.getDescendantOf,
+ descendantTypes,
+ depthLimit,
+ lastUuid,
+ lastName,
+ pageSize,
+ pageByName,
+ filter)
+ } else {
+ entityModel.typesRelationFlatQuery(
+ query.getStartTypesList.toSeq,
+ query.getRelationship,
+ query.getDescendantOf,
+ descendantTypes,
+ depthLimit,
+ lastUuid,
+ lastName,
+ pageSize,
+ pageByName,
+ filter)
+ }
+
+ val response = EntityRelationshipFlatQueryResponse.newBuilder().addAllResults(entities).build
+
+ Success(Envelope.Status.OK, response)
+
+ }
+
+ def putEntities(request: PutEntitiesRequest, headers: Map[String, String], context: ServiceContext): Response[PutEntitiesResponse] = {
+
+ val createFilter = context.auth.authorize(entityResource, "create")
+ val updateFilter = context.auth.authorize(entityResource, "update")
+
+ if (request.getEntitiesCount == 0) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val templates = request.getEntitiesList.toSeq
+
+ if (!templates.forall(_.hasName)) {
+ throw new BadRequestException("Must include entity name.")
+ }
+
+ templates.filter(_.hasName).map(_.getName).foreach(validateEntityName)
+
+ val (withIds, withNames) = templates.partition(_.hasUuid)
+
+ val withIdsSimple = withIds.map { ent =>
+ (protoUUIDToUuid(ent.getUuid), ent.getName, ent.getTypesList.toSet)
+ }
+
+ val withNamesSimple = withNames.map { ent =>
+ (ent.getName, ent.getTypesList.toSet)
+ }
+
+ val allowCreates = createFilter.isEmpty
+
+ val entities = try {
+ entityModel.putEntities(context.notifier, withIdsSimple, withNamesSimple, allowCreates, updateFilter)
+ } catch {
+ case ex: ModelPermissionException =>
+ throw new ForbiddenException(ex.getMessage)
+ case inEx: ModelInputException =>
+ throw new BadRequestException(inEx.getMessage)
+ }
+
+ Success(Envelope.Status.OK, PutEntitiesResponse.newBuilder().addAllResults(entities).build())
+ }
+
+ def deleteEntities(request: DeleteEntitiesRequest, headers: Map[String, String], context: ServiceContext): Response[DeleteEntitiesResponse] = {
+
+ val filter = context.auth.authorize(entityResource, "delete")
+
+ if (request.getEntityUuidsCount == 0) {
+ throw new BadRequestException("Must include at least one id to delete.")
+ }
+
+ val ids = request.getEntityUuidsList map protoUUIDToUuid
+
+ val results = try {
+ entityModel.deleteEntities(context.notifier, ids, filter)
+ } catch {
+ case ex: ModelPermissionException => {
+ throw new ForbiddenException(ex.getMessage)
+ }
+ }
+
+ Success(Envelope.Status.OK, DeleteEntitiesResponse.newBuilder().addAllResults(results).build())
+ }
+
+ def getEntityKeyValues(request: GetEntityKeyValuesRequest, headers: Map[String, String], context: ServiceContext): Response[GetEntityKeyValuesResponse] = {
+ val filter = context.auth.authorize(keyValueResource, "read")
+
+ if (request.getRequestCount == 0) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ request.getRequestList.foreach { kvp =>
+ if (!kvp.hasUuid) {
+ throw new BadRequestException("Must include UUID")
+ }
+ if (!kvp.hasKey) {
+ throw new BadRequestException("Must include key")
+ }
+ }
+
+ val idAndKeys = request.getRequestList.map(kvp => (protoUUIDToUuid(kvp.getUuid), kvp.getKey))
+
+ val results = entityModel.getKeyValues2(idAndKeys, filter)
+
+ Success(Envelope.Status.OK, GetEntityKeyValuesResponse.newBuilder().addAllResults(results).build())
+ }
+
+ def getEntityKeys(request: GetKeysForEntitiesRequest, headers: Map[String, String], context: ServiceContext): Response[GetKeysForEntitiesResponse] = {
+ val filter = context.auth.authorize(keyValueResource, "read")
+
+ if (request.getRequestCount == 0) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val uuids = request.getRequestList.map(protoUUIDToUuid)
+
+ val results = entityModel.getKeys(uuids, filter)
+
+ Success(Envelope.Status.OK, GetKeysForEntitiesResponse.newBuilder().addAllResults(results).build())
+ }
+
+ def subscribeToEntityKeyValues(request: SubscribeEntityKeyValuesRequest, headers: Map[String, String], context: ServiceContext): Response[SubscribeEntityKeyValuesResponse] = {
+
+ val filter = context.auth.authorize(keyValueResource, "read")
+
+ if (!request.hasQuery) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val queue = headers.getOrElse("__subscription", throw new BadRequestException("Must include subscription queue in headers"))
+
+ val query = request.getQuery
+
+ val hasKeys = query.getUuidsCount != 0 || query.getKeyPairsCount != 0
+ val hasParams = hasKeys || query.getEndpointUuidsCount != 0
+
+ val results = if (!hasParams) {
+
+ if (filter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket read permissions on entities when subscribing to all")
+ }
+
+ keyValueBinding.bindAll(queue)
+
+ Seq()
+
+ } else if (query.getEndpointUuidsCount != 0) {
+
+ if (hasKeys) {
+ throw new BadRequestException("Must not include key parameters while subscribing by endpoint")
+ }
+ if (filter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket read permissions when subscribing to abstract sets")
+ }
+
+ val endpointUuids = query.getEndpointUuidsList.toSeq
+
+ keyValueBinding.bindEach(queue, Seq(Nil, Nil, endpointUuids.map(_.getValue)))
+
+ Seq()
+
+ } else {
+
+ val uuidSubKeys = query.getUuidsList.toSeq
+
+ val uuidAndKeys = query.getKeyPairsList.toSeq.map { kv =>
+ if (!kv.hasUuid) {
+ throw new BadRequestException("Key pairs must include UUID")
+ }
+ if (!kv.hasKey) {
+ throw new BadRequestException("Key pairs must include key")
+ }
+
+ (protoUUIDToUuid(kv.getUuid), kv.getKey)
+ }
+
+ val uuidAndKeyResults = if (uuidAndKeys.nonEmpty) entityModel.getKeyValues2(uuidAndKeys) else Seq()
+
+ val uuidResults = if (uuidSubKeys.nonEmpty) entityModel.getKeyValuesForUuids(uuidSubKeys.map(protoUUIDToUuid)) else Seq()
+
+ uuidAndKeyResults.foreach { kv =>
+ keyValueBinding.bindTogether(queue, Seq(Seq(Some(kv.getUuid.getValue), Some(kv.getKey), None)))
+ }
+
+ if (uuidResults.nonEmpty) {
+ keyValueBinding.bindEach(queue, Seq(uuidResults.map(uuid => uuid.getUuid.getValue), Seq(), Seq()))
+ }
+
+ uuidResults ++ uuidAndKeyResults
+ }
+
+ val response = SubscribeEntityKeyValuesResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def putEntityKeyValues(request: PutEntityKeyValuesRequest, headers: Map[String, String], context: ServiceContext): Response[PutEntityKeyValuesResponse] = {
+
+ val createFilter = context.auth.authorize(keyValueResource, "create")
+ val updateFilter = context.auth.authorize(keyValueResource, "update")
+
+ if (request.getKeyValuesCount == 0) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val templates = request.getKeyValuesList.toSeq
+
+ templates.foreach { temp =>
+ if (!temp.hasUuid) {
+ throw new BadRequestException("Must include UUID")
+ }
+ if (!temp.hasKey) {
+ throw new BadRequestException("Must include key")
+ }
+ if (!temp.hasValue) {
+ throw new BadRequestException("Must include value")
+ }
+ }
+
+ val set = templates.map(kv => (protoUUIDToUuid(kv.getUuid), kv.getKey, kv.getValue))
+
+ val allowCreates = createFilter.isEmpty
+
+ val results = entityModel.putKeyValues2(context.notifier, set, allowCreates, updateFilter)
+
+ Success(Envelope.Status.OK, PutEntityKeyValuesResponse.newBuilder().addAllResults(results).build())
+ }
+
+ def deleteEntityKeyValues(request: DeleteEntityKeyValuesRequest, headers: Map[String, String], context: ServiceContext): Response[DeleteEntityKeyValuesResponse] = {
+
+ val filter = context.auth.authorize(keyValueResource, "delete")
+
+ if (request.getRequestCount == 0) {
+ throw new BadRequestException("Must include at least one id/key to delete.")
+ }
+
+ request.getRequestList.foreach { kvp =>
+ if (!kvp.hasUuid) {
+ throw new BadRequestException("Must include UUID")
+ }
+ if (!kvp.hasKey) {
+ throw new BadRequestException("Must include key")
+ }
+ }
+
+ val idAndKeys = request.getRequestList.map(kvp => (protoUUIDToUuid(kvp.getUuid), kvp.getKey))
+
+ val results = entityModel.deleteKeyValues2(context.notifier, idAndKeys, filter)
+
+ Success(Envelope.Status.OK, DeleteEntityKeyValuesResponse.newBuilder().addAllResults(results).build())
+ }
+
+ def edgeQuery(request: EntityEdgeQueryRequest, headers: Map[String, String], context: ServiceContext): Response[EntityEdgeQueryResponse] = {
+
+ val filter = context.auth.authorize(edgeResource, "read")
+
+ if (!request.hasQuery) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val query = request.getQuery
+
+ val parents = query.getParentUuidsList.map(protoUUIDToUuid)
+ val children = query.getChildUuidsList.map(protoUUIDToUuid)
+ val relations = query.getRelationshipsList.toSeq
+ val depthLimit = if (query.hasDepthLimit) Some(query.getDepthLimit) else None
+
+ val lastId = if (query.hasLastId) Some(idToLong(query.getLastId)) else None
+ val pageSize = if (query.hasPageSize) query.getPageSize else defaultPageSize
+
+ val results = entityModel.edgeQuery(parents, relations, children, depthLimit, lastId, pageSize, filter)
+
+ Success(Envelope.Status.OK, EntityEdgeQueryResponse.newBuilder().addAllResults(results).build())
+ }
+
+ def subscribeToEntityEdges(request: SubscribeEntityEdgesRequest, headers: Map[String, String], context: ServiceContext): Response[SubscribeEntityEdgesResponse] = {
+ val filter = context.auth.authorize(edgeResource, "read")
+
+ if (!request.hasQuery) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val query = request.getQuery
+
+ val queue = headers.get("__subscription").getOrElse {
+ throw new BadRequestException("Must include subscription queue in headers")
+ }
+
+ if (query.getFiltersCount == 0) {
+
+ if (filter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket read permissions when subscribing to all")
+ }
+
+ edgeBinding.bindAll(queue)
+
+ } else {
+
+ val params = query.getFiltersList.toSeq.map { filt =>
+
+ val optParent = if (filt.hasParentUuid) Some(protoUUIDToUuid(filt.getParentUuid)) else None
+ val optChild = if (filt.hasChildUuid) Some(protoUUIDToUuid(filt.getChildUuid)) else None
+
+ if (optParent.isEmpty && optChild.isEmpty && filter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket read permissions when subscribing without specifying parent or child")
+ }
+
+ val optRelation = if (filt.hasRelationship) Some(filt.getRelationship) else None
+ val optDistance = if (filt.hasDistance) Some(filt.getDistance) else None
+
+ (optParent, optChild, optRelation, optDistance)
+ }
+
+ val filtered = filter match {
+ case Some(filt) =>
+ val allParents = params.flatMap(_._1)
+ val allChildren = params.flatMap(_._2)
+
+ val allEnts = allParents ++ allChildren
+
+ val allowedSet = filt.filter(allEnts).toSet
+
+ val allEntsSet = allEnts.toSet
+
+ val entsFilteredOut = allEntsSet &~ allowedSet
+
+ if (entsFilteredOut.nonEmpty) {
+ throw new ForbiddenException("Missing permissions for entities in edge request")
+ }
+
+ params
+
+ case None =>
+ params
+ }
+
+ val keyParams = filtered.map {
+ case (optParent, optChild, optRelation, optDistance) => List(
+ optParent.map(_.getValue),
+ optChild.map(_.getValue),
+ optRelation,
+ optDistance.map(_.toString))
+ }
+
+ edgeBinding.bindTogether(queue, keyParams)
+ }
+
+ val resp = SubscribeEntityEdgesResponse.newBuilder().build()
+
+ Success(Envelope.Status.OK, resp)
+ }
+
+ private def idToLong(id: ModelID): Long = {
+ if (!id.hasValue) {
+ throw new BadRequestException("ModelID was missing a value")
+ }
+ try {
+ id.getValue.toLong
+ } catch {
+ case ex: NumberFormatException =>
+ throw new BadRequestException("ModelID was an invalid format")
+ }
+ }
+
+ def putEntityEdges(request: PutEntityEdgesRequest, headers: Map[String, String], context: ServiceContext): Response[PutEntityEdgesResponse] = {
+
+ // TODO: Find all affected entities in one query and check them
+ val edgeFilter = context.auth.authorize(edgeResource, "create")
+ if (edgeFilter.nonEmpty) {
+ throw new ForbiddenException(s"Must have blanket create permissions for '$edgeResource'")
+ }
+
+ if (request.getDescriptorsCount == 0) {
+ throw new BadRequestException("Must include at least one descriptor.")
+ }
+
+ val templates = request.getDescriptorsList.toSeq
+
+ templates.foreach { temp =>
+ if (!temp.hasParentUuid) {
+ throw new BadRequestException("Entity edge must have a parent.")
+ }
+ if (!temp.hasChildUuid) {
+ throw new BadRequestException("Entity edge must include a child.")
+ }
+ if (!temp.hasRelationship) {
+ throw new BadRequestException("Entity edge must include relationship.")
+ }
+ }
+
+ val grouped: Seq[(String, Seq[(UUID, UUID)])] = templates.groupBy(temp => temp.getRelationship).mapValues(_.map(temp => (protoUUIDToUuid(temp.getParentUuid), protoUUIDToUuid(temp.getChildUuid)))).toSeq
+
+ val results = grouped.flatMap {
+ case (relation, pairs) => entityModel.putEdges(context.notifier, pairs, relation)
+ }
+
+ Success(Envelope.Status.OK, PutEntityEdgesResponse.newBuilder().addAllResults(results).build())
+ }
+
+ def deleteEntityEdges(request: DeleteEntityEdgesRequest, headers: Map[String, String], context: ServiceContext): Response[DeleteEntityEdgesResponse] = {
+
+ // TODO: Find all affected entities in one query and check them
+ val edgeFilter = context.auth.authorize(edgeResource, "delete")
+ if (edgeFilter.nonEmpty) {
+ throw new ForbiddenException(s"Must have blanket delete permissions for '$edgeResource'")
+ }
+
+ if (request.getDescriptorsCount == 0) {
+ throw new BadRequestException("Must include at least one descriptor.")
+ }
+
+ val templates = request.getDescriptorsList.toSeq
+
+ templates.foreach { temp =>
+ if (!temp.hasParentUuid) {
+ throw new BadRequestException("Entity edge must have a parent.")
+ }
+ if (!temp.hasChildUuid) {
+ throw new BadRequestException("Entity edge must include at least one child.")
+ }
+ if (!temp.hasRelationship) {
+ throw new BadRequestException("Entity edge must include relationship.")
+ }
+ }
+
+ val grouped: Seq[(String, Seq[(UUID, UUID)])] = templates.groupBy(temp => temp.getRelationship).mapValues(_.map(temp => (protoUUIDToUuid(temp.getParentUuid), protoUUIDToUuid(temp.getChildUuid)))).toSeq
+
+ val results = grouped.flatMap {
+ case (relation, pairs) => entityModel.deleteEdges(context.notifier, pairs, relation)
+ }
+
+ Success(Envelope.Status.OK, DeleteEntityEdgesResponse.newBuilder().addAllResults(results).build())
+ }
+
+ def getPoints(request: GetPointsRequest, headers: Map[String, String], context: ServiceContext): Response[GetPointsResponse] = {
+
+ val filter = context.auth.authorize(pointResource, "read")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val keySet = request.getRequest
+
+ if (keySet.getUuidsCount == 0 && keySet.getNamesCount == 0) {
+ throw new BadRequestException("Must include at least one id or name")
+ }
+
+ val uuids = keySet.getUuidsList.toSeq.map(protoUUIDToUuid)
+ val names = keySet.getNamesList.toSeq
+
+ val results = frontEndModel.pointKeyQuery(uuids, names, filter)
+
+ val response = GetPointsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def pointQuery(request: PointQueryRequest, headers: Map[String, String], context: ServiceContext): Response[PointQueryResponse] = {
+
+ val filter = context.auth.authorize(pointResource, "read")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val query = request.getRequest
+
+ val (pageByName, lastUuid, lastName, pageSize) = parsePageParams(query.hasPagingParams, query.getPagingParams)
+
+ val typeParams = if (query.hasTypeParams) {
+ TypeParams(
+ query.getTypeParams.getIncludeTypesList.toSeq,
+ query.getTypeParams.getMatchTypesList.toSeq,
+ query.getTypeParams.getFilterOutTypesList.toSeq)
+ } else {
+ TypeParams(Nil, Nil, Nil)
+ }
+
+ val pointCategories = query.getPointCategoriesList.toSeq
+ val units = query.getUnitsList.toSeq
+
+ val results = frontEndModel.pointQuery(pointCategories, units, typeParams, lastUuid, lastName, pageSize, pageByName, filter)
+
+ val response = PointQueryResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def putPoints(request: PutPointsRequest, headers: Map[String, String], context: ServiceContext): Response[PutPointsResponse] = {
+
+ val createFilter = context.auth.authorize(pointResource, "create")
+ val allowCreate = createFilter.isEmpty
+ val updateFilter = context.auth.authorize(pointResource, "update")
+
+ if (request.getTemplatesCount == 0) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val templates = request.getTemplatesList.toSeq
+
+ val modelTemplates = templates.map { template =>
+
+ if (!template.hasEntityTemplate) {
+ throw new BadRequestException("Must include entity template")
+ }
+
+ if (!template.getEntityTemplate.hasName) {
+ throw new BadRequestException("Must include point name")
+ }
+ EntityServices.validateEntityName(template.getEntityTemplate.getName)
+
+ if (!template.hasPointCategory) {
+ throw new BadRequestException("Must include point type")
+ }
+ if (!template.hasUnit) {
+ throw new BadRequestException("Must include point unit")
+ }
+
+ val uuidOpt = if (template.getEntityTemplate.hasUuid) Some(protoUUIDToUuid(template.getEntityTemplate.getUuid)) else None
+
+ FrontEndModel.CoreTypeTemplate(uuidOpt, template.getEntityTemplate.getName, template.getEntityTemplate.getTypesList.toSet, PointInfo(template.getPointCategory, template.getUnit))
+ }
+
+ val results = frontEndModel.putPoints(context.notifier, modelTemplates, allowCreate, updateFilter)
+
+ val response = PutPointsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def deletePoints(request: DeletePointsRequest, headers: Map[String, String], context: ServiceContext): Response[DeletePointsResponse] = {
+
+ val filter = context.auth.authorize(pointResource, "delete")
+
+ if (request.getPointUuidsCount == 0) {
+ throw new BadRequestException("Must include point ids to delete")
+ }
+
+ val uuids = request.getPointUuidsList.map(protoUUIDToUuid)
+
+ val results = frontEndModel.deletePoints(context.notifier, uuids, filter)
+
+ val response = DeletePointsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def subscribeToPoints(request: SubscribePointsRequest, headers: Map[String, String], context: ServiceContext): Response[SubscribePointsResponse] = {
+
+ val filter = context.auth.authorize(pointResource, "read")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val queue = headers.get("__subscription").getOrElse {
+ throw new BadRequestException("Must include subscription queue in headers")
+ }
+
+ val query = request.getRequest
+
+ val hasKeys = query.getUuidsCount != 0 || query.getNamesCount != 0
+ val hasParams = hasKeys || query.getEndpointUuidsCount != 0
+
+ val results = if (!hasParams) {
+
+ if (filter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket read permissions when subscribing to all")
+ }
+
+ pointBinding.bindAll(queue)
+
+ Nil
+
+ } else if (query.getEndpointUuidsCount != 0 && !hasKeys) {
+
+ if (filter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket read permissions when subscribing to all")
+ }
+
+ val endpointUuids = query.getEndpointUuidsList.toSeq
+
+ pointBinding.bindEach(queue, Seq(endpointUuids.map(_.getValue), Nil, Nil))
+
+ Nil
+
+ } else if (hasKeys && query.getEndpointUuidsCount == 0) {
+
+ val uuids = query.getUuidsList.toSeq
+ val names = query.getNamesList.toSeq
+
+ val results = frontEndModel.pointKeyQuery(uuids map protoUUIDToUuid, names, filter)
+
+ val filteredUuids = results.map(_.getUuid.getValue)
+
+ if (filteredUuids.nonEmpty) {
+ pointBinding.bindEach(queue, Seq(Nil, filteredUuids, Nil))
+ }
+
+ results
+
+ } else {
+
+ throw new BadRequestException("Must not include key parameters while subscribing by endpoint")
+ }
+
+ val response = SubscribePointsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def getCommands(request: GetCommandsRequest, headers: Map[String, String], context: ServiceContext): Response[GetCommandsResponse] = {
+
+ val filter = context.auth.authorize(commandResource, "read")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val keySet = request.getRequest
+
+ if (keySet.getUuidsCount == 0 && keySet.getNamesCount == 0) {
+ throw new BadRequestException("Must include at least one id or name")
+ }
+
+ val uuids = keySet.getUuidsList.toSeq.map(protoUUIDToUuid)
+ val names = keySet.getNamesList.toSeq
+
+ val results = frontEndModel.commandKeyQuery(uuids, names, filter)
+
+ val response = GetCommandsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def commandQuery(request: CommandQueryRequest, headers: Map[String, String], context: ServiceContext): Response[CommandQueryResponse] = {
+
+ val filter = context.auth.authorize(commandResource, "read")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val query = request.getRequest
+
+ val (pageByName, lastUuid, lastName, pageSize) = parsePageParams(query.hasPagingParams, query.getPagingParams)
+
+ val typeParams = if (query.hasTypeParams) {
+ TypeParams(
+ query.getTypeParams.getIncludeTypesList.toSeq,
+ query.getTypeParams.getMatchTypesList.toSeq,
+ query.getTypeParams.getFilterOutTypesList.toSeq)
+ } else {
+ TypeParams(Nil, Nil, Nil)
+ }
+
+ val commandCategories = query.getCommandCategoriesList.toSeq
+
+ val results = frontEndModel.commandQuery(commandCategories, typeParams, lastUuid, lastName, pageSize, pageByName, filter)
+
+ val response = CommandQueryResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def putCommands(request: PutCommandsRequest, headers: Map[String, String], context: ServiceContext): Response[PutCommandsResponse] = {
+
+ val createFilter = context.auth.authorize(commandResource, "create")
+ val allowCreate = createFilter.isEmpty
+ val updateFilter = context.auth.authorize(commandResource, "update")
+
+ if (request.getTemplatesCount == 0) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val templates = request.getTemplatesList.toSeq
+
+ val modelTemplates = templates.map { template =>
+
+ if (!template.hasEntityTemplate || !template.getEntityTemplate.hasName) {
+ throw new BadRequestException("Must include point name")
+ }
+ EntityServices.validateEntityName(template.getEntityTemplate.getName)
+
+ if (!template.hasDisplayName) {
+ throw new BadRequestException("Must include display name")
+ }
+
+ if (!template.hasCategory) {
+ throw new BadRequestException("Must include command type")
+ }
+
+ val uuidOpt = if (template.getEntityTemplate.hasUuid) Some(protoUUIDToUuid(template.getEntityTemplate.getUuid)) else None
+
+ FrontEndModel.CoreTypeTemplate(uuidOpt, template.getEntityTemplate.getName, template.getEntityTemplate.getTypesList.toSet, CommandInfo(template.getDisplayName, template.getCategory))
+ }
+
+ val results = frontEndModel.putCommands(context.notifier, modelTemplates, allowCreate, updateFilter)
+
+ val response = PutCommandsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def deleteCommands(request: DeleteCommandsRequest, headers: Map[String, String], context: ServiceContext): Response[DeleteCommandsResponse] = {
+
+ val filter = context.auth.authorize(commandResource, "delete")
+
+ if (request.getCommandUuidsCount == 0) {
+ throw new BadRequestException("Must include point ids to delete")
+ }
+
+ val uuids = request.getCommandUuidsList.map(protoUUIDToUuid)
+
+ val results = frontEndModel.deleteCommands(context.notifier, uuids, filter)
+
+ val response = DeleteCommandsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def subscribeToCommands(request: SubscribeCommandsRequest, headers: Map[String, String], context: ServiceContext): Response[SubscribeCommandsResponse] = {
+
+ val filter = context.auth.authorize(commandResource, "read")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val queue = headers.get("__subscription").getOrElse {
+ throw new BadRequestException("Must include subscription queue in headers")
+ }
+
+ val query = request.getRequest
+
+ val hasKeys = query.getUuidsCount != 0 || query.getNamesCount != 0
+ val hasParams = hasKeys || query.getEndpointUuidsCount != 0
+
+ val results = if (!hasParams) {
+
+ if (filter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket read permissions on entities when subscribing to all")
+ }
+
+ commandBinding.bindAll(queue)
+
+ Nil
+
+ } else if (query.getEndpointUuidsCount != 0 && !hasKeys) {
+
+ if (filter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket read permissions when subscribing to all")
+ }
+
+ val endpointUuids = query.getEndpointUuidsList.toSeq
+
+ commandBinding.bindEach(queue, Seq(endpointUuids.map(_.getValue), Nil, Nil))
+
+ Nil
+
+ } else if (hasKeys && query.getEndpointUuidsCount == 0) {
+
+ val uuids = query.getUuidsList.toSeq
+ val names = query.getNamesList.toSeq
+
+ val results = frontEndModel.commandKeyQuery(uuids map protoUUIDToUuid, names, filter)
+
+ val filteredUuids = results.map(_.getUuid.getValue)
+
+ if (filteredUuids.nonEmpty) {
+ commandBinding.bindEach(queue, Seq(Nil, filteredUuids, Nil))
+ }
+
+ results
+
+ } else {
+
+ throw new BadRequestException("Must not include key parameters while subscribing by endpoint")
+ }
+
+ val response = SubscribeCommandsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def getEndpoints(request: GetEndpointsRequest, headers: Map[String, String], context: ServiceContext): Response[GetEndpointsResponse] = {
+
+ val filter = context.auth.authorize(endpointResource, "read")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val keySet = request.getRequest
+
+ if (keySet.getUuidsCount == 0 && keySet.getNamesCount == 0) {
+ throw new BadRequestException("Must include at least one id or name")
+ }
+
+ val uuids = keySet.getUuidsList.toSeq.map(protoUUIDToUuid)
+ val names = keySet.getNamesList.toSeq
+
+ val results = frontEndModel.endpointKeyQuery(uuids, names, filter)
+
+ val response = GetEndpointsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def endpointQuery(request: EndpointQueryRequest, headers: Map[String, String], context: ServiceContext): Response[EndpointQueryResponse] = {
+
+ val filter = context.auth.authorize(endpointResource, "read")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val query = request.getRequest
+
+ val typeParams = if (query.hasTypeParams) {
+ TypeParams(
+ query.getTypeParams.getIncludeTypesList.toSeq,
+ query.getTypeParams.getMatchTypesList.toSeq,
+ query.getTypeParams.getFilterOutTypesList.toSeq)
+ } else {
+ TypeParams(Nil, Nil, Nil)
+ }
+
+ val (pageByName, lastUuid, lastName, pageSize) = parsePageParams(query.hasPagingParams, query.getPagingParams)
+
+ val protocols = query.getProtocolsList.toSeq
+ val disabledOpt = if (query.hasDisabled) Some(query.getDisabled) else None
+
+ val results = frontEndModel.endpointQuery(protocols, disabledOpt, typeParams, lastUuid, lastName, pageSize, pageByName, filter)
+
+ val response = EndpointQueryResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def putEndpoints(request: PutEndpointsRequest, headers: Map[String, String], context: ServiceContext): Response[PutEndpointsResponse] = {
+
+ val createFilter = context.auth.authorize(endpointResource, "create")
+ val allowCreate = createFilter.isEmpty
+ val updateFilter = context.auth.authorize(endpointResource, "update")
+
+ if (request.getTemplatesCount == 0) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val templates = request.getTemplatesList.toSeq
+
+ val modelTemplates = templates.map { template =>
+ if (!template.hasEntityTemplate || !template.getEntityTemplate.hasName) {
+ throw new BadRequestException("Must include endpoint name")
+ }
+ EntityServices.validateEntityName(template.getEntityTemplate.getName)
+
+ if (!template.hasProtocol) {
+ throw new BadRequestException("Must include protocol name")
+ }
+
+ val uuidOpt = if (template.getEntityTemplate.hasUuid) Some(protoUUIDToUuid(template.getEntityTemplate.getUuid)) else None
+
+ val disabledOpt = if (template.hasDisabled) Some(template.getDisabled) else None
+
+ FrontEndModel.CoreTypeTemplate(uuidOpt, template.getEntityTemplate.getName, template.getEntityTemplate.getTypesList.toSet, EndpointInfo(template.getProtocol, disabledOpt))
+ }
+
+ val results = frontEndModel.putEndpoints(context.notifier, modelTemplates, allowCreate, updateFilter)
+
+ val response = PutEndpointsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def putEndpointsDisabled(request: PutEndpointDisabledRequest, headers: Map[String, String], context: ServiceContext): Response[PutEndpointDisabledResponse] = {
+
+ val filter = context.auth.authorize(endpointResource, "update")
+
+ if (request.getUpdatesCount == 0) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val updates = request.getUpdatesList.toSeq
+
+ val modelUpdates = updates.map { update =>
+ if (!update.hasEndpointUuid) {
+ throw new BadRequestException("Must include endpoint uuid")
+ }
+ if (!update.hasDisabled) {
+ throw new BadRequestException("Must include requested disable state")
+ }
+
+ FrontEndModel.EndpointDisabledUpdate(protoUUIDToUuid(update.getEndpointUuid), update.getDisabled)
+ }
+
+ val results = frontEndModel.putEndpointsDisabled(context.notifier, modelUpdates, filter)
+
+ val eventTemplates = results.map { endpoint =>
+ val attrs = EventAlarmModel.mapToAttributesList(Seq(("name", endpoint.getName)))
+ val eventType = if (endpoint.getDisabled) {
+ EventSeeding.System.endpointDisabled.eventType
+ } else {
+ EventSeeding.System.endpointEnabled.eventType
+ }
+ SysEventTemplate(context.auth.agentName, eventType, Some("services"), None, None, None, attrs)
+ }
+ eventModel.postEvents(context.notifier, eventTemplates)
+
+ val response = PutEndpointDisabledResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def deleteEndpoints(request: DeleteEndpointsRequest, headers: Map[String, String], context: ServiceContext): Response[DeleteEndpointsResponse] = {
+
+ val filter = context.auth.authorize(endpointResource, "delete")
+
+ if (request.getEndpointUuidsCount == 0) {
+ throw new BadRequestException("Must include point ids to delete")
+ }
+
+ val uuids = request.getEndpointUuidsList.map(protoUUIDToUuid)
+
+ val results = frontEndModel.deleteEndpoints(context.notifier, uuids, filter)
+
+ val response = DeleteEndpointsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def subscribeToEndpoints(request: SubscribeEndpointsRequest, headers: Map[String, String], context: ServiceContext): Response[SubscribeEndpointsResponse] = {
+
+ val filter = context.auth.authorize(endpointResource, "read")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val queue = headers.get("__subscription").getOrElse {
+ throw new BadRequestException("Must include subscription queue in headers")
+ }
+
+ val query = request.getRequest
+
+ val hasKeys = query.getUuidsCount != 0 || query.getNamesCount != 0
+ val hasParams = hasKeys || query.getProtocolsCount != 0
+
+ val results = if (!hasParams) {
+
+ if (filter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket read permissions on entities when subscribing to all")
+ }
+
+ endpointBinding.bindAll(queue)
+
+ Nil
+
+ } else if (query.getProtocolsCount != 0 && !hasKeys) {
+
+ if (filter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket read permissions on entities when subscribing by protocol")
+ }
+
+ val protocols = query.getProtocolsList.toSeq
+
+ endpointBinding.bindEach(queue, Seq(Nil, Nil, protocols))
+
+ Nil
+
+ } else if (hasKeys && query.getProtocolsCount == 0) {
+
+ val uuids = query.getUuidsList.toSeq
+ val names = query.getNamesList.toSeq
+
+ val results = frontEndModel.endpointKeyQuery(uuids map protoUUIDToUuid, names, filter)
+
+ val filteredUuids = results.map(_.getUuid.getValue)
+
+ if (filteredUuids.nonEmpty) {
+ endpointBinding.bindEach(queue, Seq(filteredUuids, Nil, Nil))
+ }
+
+ results
+
+ } else {
+
+ throw new BadRequestException("Must not include key parameters if not subscribing by protocol")
+ }
+
+ val response = SubscribeEndpointsResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+}
+
+object EntitySubscriptionDescriptor extends SubscriptionDescriptor[Entity] {
+ def keyParts(payload: Entity): Seq[String] = {
+ Seq(payload.getUuid.getValue, payload.getName)
+ }
+
+ def notification(eventType: SubscriptionEventType, payload: Entity): Array[Byte] = {
+ EntityNotification.newBuilder
+ .setValue(payload)
+ .setEventType(eventType)
+ .build()
+ .toByteArray
+ }
+}
+
+case class EntityKeyValueWithEndpoint(payload: EntityKeyValue, endpoint: Option[ModelUUID])
+
+object EntityKeyValueSubscriptionDescriptor extends SubscriptionDescriptor[EntityKeyValueWithEndpoint] {
+ def keyParts(payload: EntityKeyValueWithEndpoint): Seq[String] = {
+ Seq(payload.payload.getUuid.getValue, payload.payload.getKey, payload.endpoint.map(_.getValue).getOrElse(""))
+ }
+
+ def notification(eventType: SubscriptionEventType, payload: EntityKeyValueWithEndpoint): Array[Byte] = {
+ EntityKeyValueNotification.newBuilder
+ .setValue(payload.payload)
+ .setEventType(eventType)
+ .build()
+ .toByteArray
+ }
+}
+
+object EntityEdgeSubscriptionDescriptor extends SubscriptionDescriptor[EntityEdge] {
+ def keyParts(payload: EntityEdge): Seq[String] = {
+ Seq(payload.getParent.getValue, payload.getChild.getValue, payload.getRelationship, payload.getDistance.toString)
+ }
+
+ def notification(eventType: SubscriptionEventType, payload: EntityEdge): Array[Byte] = {
+ EntityEdgeNotification.newBuilder
+ .setValue(payload)
+ .setEventType(eventType)
+ .build()
+ .toByteArray
+ }
+}
\ No newline at end of file
diff --git a/services/src/main/scala/io/greenbus/services/core/ProcessingServices.scala b/services/src/main/scala/io/greenbus/services/core/ProcessingServices.scala
new file mode 100644
index 0000000..8f646a7
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/core/ProcessingServices.scala
@@ -0,0 +1,208 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.core
+
+import io.greenbus.client.exception.{ BadRequestException, ForbiddenException }
+import io.greenbus.client.proto.Envelope
+import io.greenbus.client.proto.Envelope.SubscriptionEventType
+import io.greenbus.client.service.ProcessingService.Descriptors
+import io.greenbus.client.service.proto.Model.ModelUUID
+import io.greenbus.client.service.proto.Processing.{ MeasOverride, OverrideNotification }
+import io.greenbus.client.service.proto.ProcessingRequests._
+import io.greenbus.services.framework.{ ServiceContext, Success, _ }
+import io.greenbus.services.model.EventAlarmModel.SysEventTemplate
+import io.greenbus.services.model.{ FrontEndModel, EventAlarmModel, EventSeeding, ProcessingModel }
+import io.greenbus.services.model.UUIDHelpers._
+
+import scala.collection.JavaConversions._
+
+object ProcessingServices {
+
+ val overrideResource = "meas_override"
+}
+
+class ProcessingServices(services: ServiceRegistry, processingModel: ProcessingModel, eventModel: EventAlarmModel, frontEndModel: FrontEndModel, overSubMgr: SubscriptionChannelBinder) {
+ import io.greenbus.services.core.ProcessingServices._
+
+ services.fullService(Descriptors.GetOverrides, getOverrides)
+ services.fullService(Descriptors.SubscribeToOverrides, subscribeToOverrides)
+ services.fullService(Descriptors.PutOverrides, putOverrides)
+ services.fullService(Descriptors.DeleteOverrides, deleteOverrides)
+
+ def getOverrides(request: GetOverridesRequest, headers: Map[String, String], context: ServiceContext): Response[GetOverridesResponse] = {
+
+ val filter = context.auth.authorize(overrideResource, "read")
+
+ if (!request.hasRequest) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val keySet = request.getRequest
+
+ if (keySet.getUuidsCount == 0 && keySet.getNamesCount == 0) {
+ throw new BadRequestException("Must include at least one id or name")
+ }
+
+ val uuids = keySet.getUuidsList.toSeq.map(protoUUIDToUuid)
+ val names = keySet.getNamesList.toSeq
+
+ val results = processingModel.overrideKeyQuery(uuids, names, filter)
+
+ val response = GetOverridesResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def subscribeToOverrides(request: SubscribeOverridesRequest, headers: Map[String, String], context: ServiceContext): Response[SubscribeOverridesResponse] = {
+
+ val filter = context.auth.authorize(overrideResource, "read")
+
+ if (!request.hasQuery) {
+ throw new BadRequestException("Must include request content")
+ }
+
+ val query = request.getQuery
+
+ val queue = headers.get("__subscription").getOrElse {
+ throw new BadRequestException("Must include subscription queue in headers")
+ }
+
+ val hasKeys = query.getPointUuidsCount != 0 || query.getPointNamesCount != 0
+ val hasParams = hasKeys || query.getEndpointUuidsCount != 0
+
+ val immediateResults = if (!hasParams) {
+
+ if (filter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket read permissions on entities when subscribing to all entities")
+ }
+
+ overSubMgr.bindAll(queue)
+ Seq()
+
+ } else if (query.getEndpointUuidsCount != 0 && !hasKeys) {
+
+ if (query.getPointUuidsCount != 0 || query.getPointNamesCount != 0) {
+ throw new BadRequestException("Must not include key parameters when subscribing by endpoints")
+ }
+ if (filter.nonEmpty) {
+ throw new ForbiddenException("Must have blanket read permissions on entities when subscribing by endpoints")
+ }
+
+ val endpoints = query.getEndpointUuidsList.toSeq
+
+ overSubMgr.bindEach(queue, Seq(Nil, endpoints.map(_.getValue)))
+
+ Nil
+
+ } else if (hasKeys && query.getEndpointUuidsCount == 0) {
+
+ val names = query.getPointNamesList.toSeq
+ val uuids = query.getPointUuidsList.toSeq.map(protoUUIDToUuid)
+
+ val results = processingModel.overrideKeyQuery(uuids, names, filter)
+
+ val filteredUuids = results.map(_.getPointUuid.getValue)
+
+ if (filteredUuids.nonEmpty) {
+ overSubMgr.bindEach(queue, Seq(filteredUuids, Nil))
+ }
+ results
+
+ } else {
+
+ throw new BadRequestException("Must not include key parameters when subscribing by endpoints")
+ }
+
+ val response = SubscribeOverridesResponse.newBuilder().addAllResults(immediateResults).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def putOverrides(request: PutOverridesRequest, headers: Map[String, String], context: ServiceContext): Response[PutOverridesResponse] = {
+
+ val filter = context.auth.authorize(overrideResource, "update")
+
+ if (request.getOverridesCount == 0) {
+ throw new BadRequestException("Must include at least one measurement override")
+ }
+
+ request.getOverridesList.toList.foreach { measOver =>
+ if (!measOver.hasPointUuid) {
+ throw new BadRequestException("Measurement overrides must include point uuid")
+ }
+ }
+
+ val measOverrides = request.getOverridesList.toList.map(o => (protoUUIDToUuid(o.getPointUuid), o))
+
+ val results = processingModel.putOverrides(context.notifier, measOverrides, filter)
+
+ publishEventsForOverrides(context, results, EventSeeding.System.setNotInService.eventType, EventSeeding.System.setOverride.eventType)
+
+ val response = PutOverridesResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ def deleteOverrides(request: DeleteOverridesRequest, headers: Map[String, String], context: ServiceContext): Response[DeleteOverridesResponse] = {
+
+ val filter = context.auth.authorize(overrideResource, "delete")
+
+ if (request.getPointUuidsCount == 0) {
+ throw new BadRequestException("Must include at least one item to delete")
+ }
+
+ val uuids = request.getPointUuidsList.map(protoUUIDToUuid)
+
+ val results = processingModel.deleteOverrides(context.notifier, uuids, filter)
+
+ publishEventsForOverrides(context, results, EventSeeding.System.removeNotInService.eventType, EventSeeding.System.removeOverride.eventType)
+
+ val response = DeleteOverridesResponse.newBuilder().addAllResults(results).build
+ Success(Envelope.Status.OK, response)
+ }
+
+ private def publishEventsForOverrides(context: ServiceContext, results: Seq[MeasOverride], nisType: String, replaceType: String): Unit = {
+ val points = frontEndModel.pointKeyQuery(results.map(_.getPointUuid).map(protoUUIDToUuid), Seq())
+ val uuidToNameMap = points.map(p => (p.getUuid, p.getName)).toMap
+
+ val eventTemplates = results.map { over =>
+ val pointName = uuidToNameMap.getOrElse(over.getPointUuid, "unknown")
+ val attrs = EventAlarmModel.mapToAttributesList(Seq(("point", pointName)))
+ val eventType = if (over.hasMeasurement) replaceType else nisType
+ SysEventTemplate(context.auth.agentName, eventType, Some("services"), None, None, None, attrs)
+ }
+
+ eventModel.postEvents(context.notifier, eventTemplates)
+ }
+}
+
+case class OverrideWithEndpoint(payload: MeasOverride, endpoint: Option[ModelUUID])
+
+object OverrideSubscriptionDescriptor extends SubscriptionDescriptor[OverrideWithEndpoint] {
+
+ def keyParts(obj: OverrideWithEndpoint): Seq[String] = {
+ Seq(obj.payload.getPointUuid.getValue, obj.endpoint.map(_.getValue).getOrElse(""))
+ }
+
+ def notification(eventType: SubscriptionEventType, obj: OverrideWithEndpoint): Array[Byte] = {
+ OverrideNotification.newBuilder
+ .setValue(obj.payload)
+ .setEventType(eventType)
+ .build()
+ .toByteArray
+ }
+}
+
diff --git a/services/src/main/scala/io/greenbus/services/core/event/MessageFormatter.scala b/services/src/main/scala/io/greenbus/services/core/event/MessageFormatter.scala
new file mode 100755
index 0000000..e48fe3a
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/core/event/MessageFormatter.scala
@@ -0,0 +1,159 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.core.event
+
+import io.greenbus.client.service.proto.Events.Attribute
+
+/**
+ *
+ * Render the resource string using an AttributeList.
+ *
+ * This class is like MessageFormat.java, but uses custom resource strings that
+ * have named attributes to fill in. The standard Java classes uses ordered
+ * attributes.
+ *
+ * Goals
+ * The resource strings should use the same syntax as the standard Java resource libraries,
+ * with the exception that properties are named.
+ *
+ * Use Cases
+ * Render a message given a resource string and an AttributeList proto.
+ *
+ *
+ * val (severity, designation, alarmState, resource) = eventConfig.getProperties(req.getEventType)
+ * theString = MessageFormatter.format( resource, req.getArgs)
+ *
+ *
+ * @author flint
+ */
+
+object MessageFormatter {
+
+ def attributeListToMap(alist: Seq[Attribute]): Map[String, String] = {
+ alist.map(attributeToPair).toMap
+ }
+
+ def attributeToPair(a: Attribute): (String, String) = {
+ val key = if (a.hasName) {
+ a.getName
+ } else {
+ throw new IllegalArgumentException("Attribute had no name")
+ }
+
+ val v = if (a.hasValueBool) {
+ a.getValueBool.toString
+ } else if (a.hasValueDouble) {
+ a.getValueDouble.toString
+ } else if (a.hasValueSint64) {
+ a.getValueSint64.toString
+ } else if (a.hasValueString) {
+ a.getValueString
+ } else {
+ throw new IllegalArgumentException("Attribute had no value")
+ }
+
+ (key, v)
+ }
+
+ /**
+ * Render the resource string using the AttributeList.
+ */
+ def format(resource: String, alist: Seq[Attribute]): String = {
+ val attrMap = attributeListToMap(alist)
+ parseResource(resource).map(_.apply(attrMap)).mkString("")
+ }
+
+ /**
+ * Return a list of ResourceSegment
+ */
+ protected def parseResource(resource: String): List[ResourceSegment] = {
+ var segments: List[ResourceSegment] = List()
+ var index = 0;
+ var leftBrace = indexOfWithEscape(resource, '{', index)
+ while (leftBrace >= 0) {
+ val rightBrace = indexOfWithEscape(resource, '}', leftBrace + 1)
+ if (rightBrace < 0)
+ throw new IllegalArgumentException("Braces do not match in resource string")
+
+ // Is there a string before the brace?
+ if (leftBrace > index)
+ segments ::= ResourceSegmentString(resource.substring(index, leftBrace))
+
+ // For now we have a simple name inside the brace
+ segments ::= ResourceSegmentNamedValue(resource.substring(leftBrace + 1, rightBrace).trim, resource.substring(leftBrace, rightBrace + 1))
+
+ index = rightBrace + 1
+ leftBrace = indexOfWithEscape(resource, '{', index)
+ }
+
+ // Either there were never any braces or there's a string after the last brace
+ if (index < resource.length)
+ segments ::= ResourceSegmentString(resource.substring(index))
+
+ segments.reverse
+ }
+
+ /**
+ * Return the index that the delimiter occurs or -1.
+ * Escape sequence for resource strings is '{'name'}' or '{name}'
+ *
+ */
+ def indexOfWithEscape(s: String, delimiter: Int, index: Int) = {
+ s.indexOf(delimiter, index)
+ // The escape sequence for resource strings uses single quotes and it's complicated!
+ /*var foundAt = s.indexOf(delimiter, index)
+ var i = index;
+
+ while (foundAt >= 0 /* &&bla bla bla*/) {
+ i = foundAt + 1
+ foundAt = s.indexOf(delimiter, i)
+ }
+
+ foundAt
+ */
+ }
+}
+
+/**
+ * A resource string is chopped up into segments. A "segment" is either
+ * a named variable or a string of characters between named variables.
+ */
+trait ResourceSegment {
+ /**
+ * Apply the values from the AttributeList to the named
+ * properties in the resource segment.
+ */
+ def apply(alist: Map[String, String]): String
+}
+
+/**
+ * Just a fixed string segment of a resource string.
+ */
+case class ResourceSegmentString(s: String) extends ResourceSegment {
+ def apply(alist: Map[String, String]) = s
+}
+
+/**
+ * A named value part of a resource string.
+ */
+case class ResourceSegmentNamedValue(name: String, original: String) extends ResourceSegment {
+
+ def apply(alist: Map[String, String]) = alist.get(name).getOrElse(original)
+}
+
diff --git a/services/src/main/scala/io/greenbus/services/data/Auth.scala b/services/src/main/scala/io/greenbus/services/data/Auth.scala
new file mode 100644
index 0000000..de88e29
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/data/Auth.scala
@@ -0,0 +1,103 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.data
+
+import java.util.UUID
+import org.squeryl.KeyedEntity
+
+/**
+ * Helpers for handling the implementation of salted password and encoded passwords
+ * http://www.jasypt.org/howtoencryptuserpasswords.html
+ */
+object SaltedPasswordHelper {
+ import org.apache.commons.codec.binary.Base64
+ import java.security.{ SecureRandom, MessageDigest }
+
+ def enc64(original: String): String = {
+ new String(Base64.encodeBase64(original.getBytes("UTF-8")), "US-ASCII")
+ }
+
+ def dec64(original: String): String = {
+ new String(Base64.decodeBase64(original.getBytes("US-ASCII")), "UTF-8")
+ }
+
+ /**
+ * @returns tuple of (digest, salt)
+ */
+ def makeDigestAndSalt(password: String): (String, String) = {
+ val b = new Array[Byte](20)
+ new SecureRandom().nextBytes(b)
+ val salt = dec64(enc64(new String(b, "UTF-8")))
+ (calcDigest(salt, password), salt)
+ }
+
+ def calcDigest(salt: String, pass: String) = {
+ val combinedSaltPassword = (salt + pass).getBytes("UTF-8")
+ val digestBytes = MessageDigest.getInstance("SHA-256").digest(combinedSaltPassword)
+ // TODO: figure out how to roundtrip bytes through UTF-8
+ dec64(enc64(new String(digestBytes, "UTF-8")))
+ }
+}
+
+object AgentRow {
+ import SaltedPasswordHelper._
+
+ def apply(id: UUID, name: String, password: String): AgentRow = {
+ val (digest, salt) = SaltedPasswordHelper.makeDigestAndSalt(password)
+ AgentRow(id, name, enc64(digest), enc64(salt))
+ }
+}
+case class AgentRow(id: UUID,
+ name: String,
+ digest: String,
+ salt: String) extends KeyedEntity[UUID] {
+
+ def checkPassword(password: String): Boolean = {
+ import SaltedPasswordHelper._
+ dec64(digest) == calcDigest(dec64(salt), password)
+ }
+
+ def withUpdatedPassword(password: String): AgentRow = {
+ import SaltedPasswordHelper._
+ val (digest, saltText) = makeDigestAndSalt(password)
+ this.copy(digest = enc64(digest), salt = enc64(saltText))
+ }
+
+ override def toString: String = {
+ s"AgentRow($id, $name, [digest], [salt])"
+ }
+}
+
+case class PermissionSetRow(id: Long,
+ name: String,
+ protoBytes: Array[Byte]) extends KeyedEntity[Long] {
+}
+
+case class AuthTokenRow(id: Long,
+ token: String,
+ agentId: UUID,
+ loginLocation: String,
+ clientVersion: String,
+ revoked: Boolean,
+ issueTime: Long,
+ expirationTime: Long) extends KeyedEntity[Long] {
+}
+
+case class AgentPermissionSetJoinRow(permissionSetId: Long, agentId: UUID)
+case class AuthTokenPermissionSetJoinRow(permissionSetId: Long, authTokenId: Long)
\ No newline at end of file
diff --git a/services/src/main/scala/io/greenbus/services/data/Commands.scala b/services/src/main/scala/io/greenbus/services/data/Commands.scala
new file mode 100644
index 0000000..cccd813
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/data/Commands.scala
@@ -0,0 +1,26 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.data
+
+import java.util.UUID
+import org.squeryl.KeyedEntity
+
+case class CommandLockRow(id: Long, access: Int, expireTime: Option[Long], agent: UUID) extends KeyedEntity[Long]
+
+case class LockJoinRow(commandId: Long, lockId: Long)
diff --git a/services/src/main/scala/io/greenbus/services/data/Events.scala b/services/src/main/scala/io/greenbus/services/data/Events.scala
new file mode 100644
index 0000000..bff0460
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/data/Events.scala
@@ -0,0 +1,73 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.data
+
+import java.util.UUID
+import io.greenbus.client.service.proto.Events.EventConfig.Designation
+import io.greenbus.client.service.proto.Events.Alarm.State
+import org.squeryl.KeyedEntity
+
+case class EventRow(id: Long,
+ eventType: String,
+ alarm: Boolean,
+ time: Long,
+ deviceTime: Option[Long],
+ severity: Int,
+ subsystem: String,
+ userId: String,
+ entityId: Option[UUID],
+ modelGroup: Option[String],
+ args: Array[Byte],
+ rendered: String) extends KeyedEntity[Long]
+
+object EventConfigRow {
+ val ALARM = Designation.ALARM.getNumber
+ val EVENT = Designation.EVENT.getNumber
+ val LOG = Designation.LOG.getNumber
+}
+
+case class EventConfigRow(
+ id: Long,
+ eventType: String,
+ severity: Int,
+ designation: Int, // Alarm, Event, or Log
+ alarmState: Int, // Initial alarm start state: UNACK_AUDIBLE, UNACK_SILENT, or ACKNOWLEDGED
+ resource: String,
+ builtIn: Boolean) extends KeyedEntity[Long]
+
+/**
+ * The Model for the Alarm. It's part DB map and part Model.
+ */
+object AlarmRow {
+
+ // Get the enum values from the proto.
+ //
+ val UNACK_AUDIBLE = State.UNACK_AUDIBLE.getNumber
+ val UNACK_SILENT = State.UNACK_SILENT.getNumber
+ val ACKNOWLEDGED = State.ACKNOWLEDGED.getNumber
+ val REMOVED = State.REMOVED.getNumber
+}
+
+/**
+ * The Model for the Alarm. It's part DB map and part Model.
+ */
+case class AlarmRow(
+ id: Long,
+ state: Int,
+ eventId: Long) extends KeyedEntity[Long]
diff --git a/services/src/main/scala/io/greenbus/services/data/FrontEnd.scala b/services/src/main/scala/io/greenbus/services/data/FrontEnd.scala
new file mode 100644
index 0000000..1263842
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/data/FrontEnd.scala
@@ -0,0 +1,32 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.data
+
+import java.util.UUID
+import org.squeryl.KeyedEntity
+
+case class PointRow(id: Long, entityId: UUID, pointCategory: Int, unit: String) extends EntityBased
+
+case class CommandRow(id: Long, entityId: UUID, displayName: String, commandCategory: Int, lastSelectId: Option[Long]) extends EntityBased
+
+case class EndpointRow(id: Long, entityId: UUID, protocol: String, disabled: Boolean) extends EntityBased
+
+case class FrontEndConnectionRow(id: Long, endpointId: UUID, inputAddress: String, commandAddress: Option[String]) extends KeyedEntity[Long]
+
+case class FrontEndCommStatusRow(id: Long, endpointId: UUID, status: Int, updateTime: Long) extends KeyedEntity[Long]
diff --git a/services/src/main/scala/io/greenbus/services/data/ProcessingLockSchema.scala b/services/src/main/scala/io/greenbus/services/data/ProcessingLockSchema.scala
new file mode 100644
index 0000000..ccf5166
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/data/ProcessingLockSchema.scala
@@ -0,0 +1,35 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.data
+
+import java.util.UUID
+
+import org.squeryl.{ KeyedEntity, Schema }
+
+object ProcessingLockSchema extends Schema {
+
+ val processingLocks = table[ProcessingLock]
+
+ def reset() {
+ drop
+ create
+ }
+}
+
+case class ProcessingLock(id: UUID, nodeId: String, lastCheckIn: Long) extends KeyedEntity[UUID]
diff --git a/services/src/main/scala/io/greenbus/services/data/ServicesSchema.scala b/services/src/main/scala/io/greenbus/services/data/ServicesSchema.scala
new file mode 100644
index 0000000..df4bcca
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/data/ServicesSchema.scala
@@ -0,0 +1,111 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.data
+
+import org.squeryl.{ KeyedEntity, Schema }
+import org.squeryl.PrimitiveTypeMode._
+import java.util.UUID
+
+trait EntityBased extends KeyedEntity[Long] {
+ def id: Long
+ def entityId: UUID
+}
+
+case class EntityRow(id: UUID, name: String) extends KeyedEntity[UUID]
+
+case class EntityTypeRow(entityId: UUID, entType: String)
+
+case class EntityEdgeRow(id: Long,
+ parentId: UUID,
+ childId: UUID,
+ relationship: String,
+ distance: Int) extends KeyedEntity[Long]
+
+case class EntityKeyStoreRow(id: Long, uuid: UUID, key: String, data: Array[Byte]) extends KeyedEntity[Long]
+
+object ServicesSchema extends Schema {
+
+ // Auth
+ val agents = table[AgentRow]
+ val permissionSets = table[PermissionSetRow]
+ val authTokens = table[AuthTokenRow]
+
+ on(authTokens)(s => declare(s.token is (indexed), s.expirationTime is (indexed)))
+ on(agents)(s => declare(
+ s.name is (unique, indexed)))
+ on(permissionSets)(s => declare(
+ s.name is (unique, indexed)))
+
+ val tokenSetJoins = table[AuthTokenPermissionSetJoinRow]
+ val agentSetJoins = table[AgentPermissionSetJoinRow]
+
+ on(tokenSetJoins)(s => declare(s.authTokenId is (indexed)))
+ on(agentSetJoins)(s => declare(s.agentId is (indexed)))
+
+ // Model
+ val entities = table[EntityRow]
+ val edges = table[EntityEdgeRow]
+ val entityTypes = table[EntityTypeRow]
+
+ val entityKeyValues = table[EntityKeyStoreRow]
+
+ on(entities)(s => declare(
+ s.name is (unique, indexed)))
+ on(edges)(s => declare(
+ columns(s.parentId, s.childId, s.relationship) are (unique),
+ columns(s.childId, s.relationship) are (indexed),
+ columns(s.parentId, s.relationship) are (indexed)))
+ on(entityTypes)(s => declare(
+ s.entType is (indexed),
+ s.entityId is (indexed)))
+
+ on(entityKeyValues)(s => declare(
+ columns(s.uuid, s.key) are (unique, indexed)))
+
+ val points = table[PointRow]
+ val commands = table[CommandRow]
+ val endpoints = table[EndpointRow]
+
+ on(points)(s => declare(
+ s.entityId is (unique, indexed)))
+ on(commands)(s => declare(
+ s.entityId is (unique, indexed)))
+ on(endpoints)(s => declare(
+ s.entityId is (unique, indexed)))
+
+ val locks = table[CommandLockRow]
+ val lockJoins = table[LockJoinRow]
+
+ val frontEndConnections = table[FrontEndConnectionRow]
+ val frontEndCommStatuses = table[FrontEndCommStatusRow]
+
+ val events = table[EventRow]
+ val eventConfigs = table[EventConfigRow]
+ val alarms = table[AlarmRow]
+
+ on(events)(s => declare(
+ s.time is (indexed)))
+ on(alarms)(s => declare(
+ s.eventId is (indexed)))
+
+ def reset() {
+ drop
+ create
+ }
+}
diff --git a/services/src/main/scala/io/greenbus/services/framework/DecodingServiceHandler.scala b/services/src/main/scala/io/greenbus/services/framework/DecodingServiceHandler.scala
new file mode 100644
index 0000000..b3aa5c5
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/framework/DecodingServiceHandler.scala
@@ -0,0 +1,131 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.framework
+
+import java.sql.SQLException
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.msg.RequestDescriptor
+import io.greenbus.msg.service.ServiceHandler
+import io.greenbus.client.exception.ServiceException
+import io.greenbus.client.proto.Envelope
+import io.greenbus.jmx.Metrics
+import io.greenbus.services.model.{ ModelInputException, ModelPermissionException }
+
+import scala.collection.JavaConversions._
+
+class DecodingServiceHandler[A, B](decoder: RequestDescriptor[A, B], handler: TypedServiceHandler[A, B]) extends ExtractedServiceHandler {
+ def handle(request: Array[Byte], headers: Map[String, String], responseHandler: Response[Array[Byte]] => Unit): Unit = {
+
+ def onResponse(result: Response[B]) {
+ result match {
+ case Failure(status, message) => responseHandler(Failure(status, message))
+ case Success(status, obj) =>
+ val bytes = decoder.encodeResponse(obj)
+ val mapped = Success(status, bytes)
+ responseHandler(mapped)
+ }
+ }
+
+ val requestObj = decoder.decodeRequest(request)
+ handler.handle(requestObj, headers, onResponse)
+ }
+}
+
+class TypedServiceInstrumenter[A, B](requestId: String, handler: TypedServiceHandler[A, B], metrics: Metrics) extends TypedServiceHandler[A, B] with Logging {
+
+ val counter = metrics.counter("Count")
+ val handleTime = metrics.average("HandleTime")
+
+ def handle(request: A, headers: Map[String, String], responseHandler: (Response[B]) => Unit): Unit = {
+ counter(1)
+ val startTime = System.currentTimeMillis()
+
+ def logHandled() {
+ val elapsed = System.currentTimeMillis() - startTime
+ logger.debug(s"Handled $requestId in $elapsed")
+ handleTime(elapsed.toInt)
+ }
+
+ def onResponse(response: Response[B]): Unit = {
+ logHandled()
+ responseHandler(response)
+ }
+
+ try {
+ handler.handle(request, headers, onResponse)
+ } catch {
+ case ex: Throwable =>
+ logHandled()
+ throw ex
+ }
+ }
+}
+
+class ServiceHandlerMetricsInstrumenter(requestId: String, metrics: Metrics, handler: ServiceHandler) extends ServiceHandler with Logging {
+
+ val counter = metrics.counter("Count")
+ val handleTime = metrics.average("HandleTime")
+
+ def handleMessage(request: Array[Byte], responseHandler: Array[Byte] => Unit): Unit = {
+ counter(1)
+ val startTime = System.currentTimeMillis()
+
+ def logHandled() {
+ val elapsed = System.currentTimeMillis() - startTime
+ logger.debug(s"Handled $requestId in $elapsed")
+ handleTime(elapsed.toInt)
+ }
+
+ def onResponse(response: Array[Byte]): Unit = {
+ logHandled()
+ responseHandler(response)
+ }
+
+ handler.handleMessage(request, onResponse)
+ }
+}
+
+class ModelErrorTransformingHandler[A, B](handler: TypedServiceHandler[A, B]) extends TypedServiceHandler[A, B] with Logging {
+ def handle(request: A, headers: Map[String, String], responseHandler: (Response[B]) => Unit): Unit = {
+ try {
+ handler.handle(request, headers, responseHandler)
+
+ } catch {
+ case ex: ModelPermissionException =>
+ responseHandler(Failure(Envelope.Status.FORBIDDEN, ex.getMessage))
+ case ex: ModelInputException =>
+ responseHandler(Failure(Envelope.Status.BAD_REQUEST, ex.getMessage))
+ case rse: ServiceException =>
+ responseHandler(Failure(rse.getStatus, rse.getMessage))
+ case sqlEx: SQLException =>
+ if (sqlEx.iterator().toList.exists(ex => ex.getClass == classOf[java.net.ConnectException])) {
+ logger.error("Database connection error: " + sqlEx.getMessage)
+ responseHandler(Failure(Envelope.Status.BUS_UNAVAILABLE, "Data unavailable."))
+ } else {
+ val next = Option(sqlEx.getNextException)
+ next.foreach(ex => logger.error("Sql next exception: " + ex))
+ logger.error("Internal service error: " + sqlEx)
+ responseHandler(Failure(Envelope.Status.BUS_UNAVAILABLE, "Data unavailable."))
+ }
+ case ex: IllegalArgumentException =>
+ responseHandler(Failure(Envelope.Status.BAD_REQUEST, ex.getMessage))
+ }
+ }
+}
diff --git a/services/src/main/scala/io/greenbus/services/framework/EnvelopeParsingHandler.scala b/services/src/main/scala/io/greenbus/services/framework/EnvelopeParsingHandler.scala
new file mode 100644
index 0000000..3892311
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/framework/EnvelopeParsingHandler.scala
@@ -0,0 +1,83 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.framework
+
+import com.google.protobuf.{ ByteString, InvalidProtocolBufferException }
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.msg.service.ServiceHandler
+import io.greenbus.client.proto.Envelope
+import io.greenbus.client.proto.Envelope.{ ServiceRequest, ServiceResponse }
+
+import scala.collection.JavaConversions._
+
+object EnvelopeParsingHandler {
+ def buildErrorEnvelope(status: Envelope.Status, message: String): ServiceResponse = {
+ ServiceResponse.newBuilder()
+ .setStatus(status)
+ .setErrorMessage(message)
+ .build()
+ }
+}
+
+class EnvelopeParsingHandler(handler: ExtractedServiceHandler) extends ServiceHandler with Logging {
+ import io.greenbus.services.framework.EnvelopeParsingHandler._
+
+ def handleMessage(msg: Array[Byte], responseHandler: (Array[Byte]) => Unit) {
+
+ def onResponse(result: Response[Array[Byte]]) {
+ result match {
+ case Failure(status, message) =>
+ val responseBytes = buildErrorEnvelope(status, message).toByteArray
+ responseHandler(responseBytes)
+
+ case Success(status, bytes) =>
+ val responseBytes = ServiceResponse.newBuilder()
+ .setStatus(status)
+ .setPayload(ByteString.copyFrom(bytes))
+ .build()
+ .toByteArray
+
+ responseHandler(responseBytes)
+ }
+ }
+
+ def internalError(ex: Throwable) {
+ logger.error("Internal service error: " + ex)
+ val response = buildErrorEnvelope(Envelope.Status.INTERNAL_ERROR, "Internal service error.")
+ responseHandler(response.toByteArray)
+ }
+
+ try {
+ val requestEnvelope = ServiceRequest.parseFrom(msg)
+ val headers: Map[String, String] = requestEnvelope.getHeadersList.map(hdr => (hdr.getKey, hdr.getValue)).toMap
+ if (requestEnvelope.hasPayload) {
+ val payload = requestEnvelope.getPayload.toByteArray
+ handler.handle(payload, headers, onResponse)
+ }
+ } catch {
+ case protoEx: InvalidProtocolBufferException =>
+ logger.warn("Error parsing client request: " + protoEx)
+ val response = buildErrorEnvelope(Envelope.Status.BAD_REQUEST, "Couldn't parse service request.")
+ responseHandler(response.toByteArray)
+ case ex: Throwable =>
+ internalError(ex)
+ }
+ }
+
+}
diff --git a/services/src/main/scala/io/greenbus/services/framework/ExtractedServiceHandler.scala b/services/src/main/scala/io/greenbus/services/framework/ExtractedServiceHandler.scala
new file mode 100644
index 0000000..b239509
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/framework/ExtractedServiceHandler.scala
@@ -0,0 +1,23 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.framework
+
+trait ExtractedServiceHandler {
+ def handle(request: Array[Byte], headers: Map[String, String], responseHandler: Response[Array[Byte]] => Unit)
+}
diff --git a/services/src/main/scala/io/greenbus/services/framework/MarshallingHandler.scala b/services/src/main/scala/io/greenbus/services/framework/MarshallingHandler.scala
new file mode 100644
index 0000000..7715c1d
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/framework/MarshallingHandler.scala
@@ -0,0 +1,29 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.framework
+
+import io.greenbus.msg.service.ServiceHandler
+
+class MarshallingHandler(marshaller: ServiceMarshaller, handler: ServiceHandler) extends ServiceHandler {
+ def handleMessage(msg: Array[Byte], responseHandler: (Array[Byte]) => Unit) {
+ marshaller.run {
+ handler.handleMessage(msg, responseHandler)
+ }
+ }
+}
diff --git a/services/src/main/scala/io/greenbus/services/framework/RequestResponseDecoder.scala b/services/src/main/scala/io/greenbus/services/framework/RequestResponseDecoder.scala
new file mode 100644
index 0000000..1971aba
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/framework/RequestResponseDecoder.scala
@@ -0,0 +1,24 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.framework
+
+trait RequestResponseDecoder[A, B] {
+ def decodeRequest(bytes: Array[Byte]): A
+ def encodeResponse(response: B): Array[Byte]
+}
diff --git a/services/src/main/scala/io/greenbus/services/framework/ServiceMarshaller.scala b/services/src/main/scala/io/greenbus/services/framework/ServiceMarshaller.scala
new file mode 100644
index 0000000..e4b2854
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/framework/ServiceMarshaller.scala
@@ -0,0 +1,23 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.framework
+
+trait ServiceMarshaller {
+ def run[U](runner: => U)
+}
diff --git a/services/src/main/scala/io/greenbus/services/framework/ServiceRegistry.scala b/services/src/main/scala/io/greenbus/services/framework/ServiceRegistry.scala
new file mode 100644
index 0000000..f6e5378
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/framework/ServiceRegistry.scala
@@ -0,0 +1,177 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.framework
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.jmx.{ Metrics, MetricsManager, MetricsSource, Tag }
+import io.greenbus.msg.RequestDescriptor
+import io.greenbus.msg.amqp.AmqpServiceOperations
+import io.greenbus.msg.service.ServiceHandler
+import io.greenbus.services.authz.{ AuthContext, AuthLookup }
+import io.greenbus.sql.{ DbConnection, TransactionMetrics, TransactionMetricsListener }
+import org.squeryl.logging.StatisticsListener
+
+object ServiceHandlerRegistry {
+
+ def fullHandlerChain[A, B](desc: RequestDescriptor[A, B], handler: TypedServiceHandler[A, B], metrics: Metrics): ServiceHandler = {
+ new ServiceHandlerMetricsInstrumenter(desc.requestId, metrics,
+ new EnvelopeParsingHandler(
+ new DecodingServiceHandler(desc,
+ new ModelErrorTransformingHandler(handler))))
+ }
+}
+
+trait ServiceHandlerRegistry {
+
+ protected val metricsSource: MetricsSource
+
+ private var registered = List.empty[(String, ServiceHandler)]
+
+ def getRegistered: Seq[(String, ServiceHandler)] = registered
+
+ protected def register(requestId: String, handler: ServiceHandler) {
+ registered ::= (requestId, handler)
+ }
+
+}
+
+trait ServiceRegistry {
+ def simpleAsync[A, B](desc: RequestDescriptor[A, B], handler: (A, Map[String, String], (Response[B]) => Unit) => Unit)
+ def simpleSync[A, B](desc: RequestDescriptor[A, B], handler: (A, Map[String, String]) => Response[B])
+
+ def fullService[A, B](desc: RequestDescriptor[A, B], handler: (A, Map[String, String], ServiceContext) => Response[B])
+}
+
+trait SimpleServiceRegistry extends ServiceHandlerRegistry {
+
+ def simpleAsync[A, B](desc: RequestDescriptor[A, B], handler: (A, Map[String, String], (Response[B]) => Unit) => Unit) {
+
+ val metrics = metricsSource.metrics(desc.requestId, Tag("SubHead", "Handlers"))
+
+ val methodHandler = new TypedServiceHandler[A, B] {
+ def handle(request: A, headers: Map[String, String], responseHandler: (Response[B]) => Unit) {
+ handler(request, headers, responseHandler)
+ }
+ }
+
+ val fullHandler = ServiceHandlerRegistry.fullHandlerChain(desc, methodHandler, metrics)
+ register(desc.requestId, fullHandler)
+ }
+
+ def simpleSync[A, B](desc: RequestDescriptor[A, B], handler: (A, Map[String, String]) => Response[B]) {
+
+ val metrics = metricsSource.metrics(desc.requestId, Tag("SubHead", "Handlers"))
+
+ val methodHandler = new SyncTypedServiceHandler[A, B] {
+ def handle(request: A, headers: Map[String, String]): Response[B] = handler(request, headers)
+ }
+
+ val fullHandler = ServiceHandlerRegistry.fullHandlerChain(desc, methodHandler, metrics)
+ register(desc.requestId, fullHandler)
+ }
+}
+
+case class ServiceContext(auth: AuthContext, notifier: ModelNotifier)
+
+class FullServiceRegistry(sql: DbConnection, ops: AmqpServiceOperations, auth: AuthLookup, mapper: ModelEventMapper, protected val metricsSource: MetricsSource) extends ServiceRegistry with SimpleServiceRegistry {
+
+ def fullService[A, B](desc: RequestDescriptor[A, B], handler: (A, Map[String, String], ServiceContext) => Response[B]) {
+
+ val metrics = metricsSource.metrics(desc.requestId, Tag("SubHead", "Handlers"))
+ val sqlMetrics = new TransactionMetrics(metrics)
+
+ val wrappingHandler = new SyncTypedServiceHandler[A, B] {
+ def handle(request: A, headers: Map[String, String]): Response[B] = {
+ val listener = new TransactionMetricsListener(request.getClass.getSimpleName)
+
+ val result = ServiceTransactionSource.transaction(sql, ops, auth, mapper, headers, Some(listener)) { context =>
+ handler(request, headers, context)
+ }
+
+ listener.report(sqlMetrics)
+
+ result
+ }
+ }
+
+ val fullHandler = ServiceHandlerRegistry.fullHandlerChain(desc, wrappingHandler, metrics)
+ register(desc.requestId, fullHandler)
+ }
+}
+
+trait ServiceTransactionSource {
+ def transaction[A](headers: Map[String, String])(handler: ServiceContext => A): A
+}
+
+object ServiceTransactionSource extends Logging {
+
+ private object GlobalServiceMetrics {
+ val metricsMgr = MetricsManager("io.greenbus.services")
+ val metrics = metricsMgr.metrics("Auth")
+
+ val authTime = metrics.timer("AuthTime")
+
+ val notifierMetrics = metricsMgr.metrics("Notifier")
+ val notificationAverage = notifierMetrics.average("Average")
+ val notificationCount = notifierMetrics.counter("Counter")
+ val notificationTime = notifierMetrics.average("FlushTime")
+ val notificationTimePerNotification = notifierMetrics.average("FlushTimePerMessage")
+
+ metricsMgr.register()
+ }
+
+ import GlobalServiceMetrics._
+
+ def notificationObserver(width: Int, time: Int) = {
+ notificationAverage(width)
+ notificationCount(width)
+ notificationTime(time)
+ notificationTimePerNotification(if (width != 0) (time.toDouble / width.toDouble).toInt else 0)
+ }
+
+ def apply(sql: DbConnection, ops: AmqpServiceOperations, auth: AuthLookup, mapper: ModelEventMapper): ServiceTransactionSource = {
+ new DefaultTransactionSource(sql, ops, auth, mapper)
+ }
+
+ def transaction[A](sql: DbConnection, ops: AmqpServiceOperations, auth: AuthLookup, mapper: ModelEventMapper, headers: Map[String, String], listener: Option[StatisticsListener])(handler: ServiceContext => A): A = {
+
+ var transStartEnd = 0L
+ // We flush the notifications outside of the transaction so that when observers receive a notification
+ // the database is consistent.
+ val notificationBuffer = new BufferingModelNotifier(mapper, ops, notificationObserver)
+ val result = sql.transaction(listener) {
+ val authContext = authTime { auth.validateAuth(headers) }
+ val context = ServiceContext(authContext, notificationBuffer)
+ val result = handler(context)
+ transStartEnd = System.currentTimeMillis()
+ result
+ }
+ logger.trace("Transaction end time: " + (System.currentTimeMillis() - transStartEnd))
+ notificationBuffer.flush()
+ result
+ }
+
+ private class DefaultTransactionSource(sql: DbConnection, ops: AmqpServiceOperations, auth: AuthLookup, mapper: ModelEventMapper) extends ServiceTransactionSource {
+ def transaction[A](headers: Map[String, String])(handler: (ServiceContext) => A): A = {
+ ServiceTransactionSource.transaction(sql, ops, auth, mapper, headers, None)(handler)
+ }
+ }
+
+}
+
diff --git a/services/src/main/scala/io/greenbus/services/framework/SubscriptionChannelManager.scala b/services/src/main/scala/io/greenbus/services/framework/SubscriptionChannelManager.scala
new file mode 100644
index 0000000..56c4346
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/framework/SubscriptionChannelManager.scala
@@ -0,0 +1,194 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.framework
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.client.proto.Envelope.SubscriptionEventType
+import io.greenbus.msg.amqp.{ AmqpAddressedMessage, AmqpMessage, AmqpServiceOperations }
+import io.greenbus.services.NotificationConversions
+
+import scala.annotation.tailrec
+import scala.collection.mutable
+
+object SubscriptionChannelManager {
+ def toRoutingKey(parts: Seq[Option[String]]): String = {
+ val mapped: Seq[String] = parts.map {
+ case None => "*"
+ case Some(part) => part.replace(".", "-")
+ }
+
+ mapped.mkString(".")
+ }
+
+ def routingKeyPermutations(parts: Seq[Seq[String]]): Seq[String] = {
+ val count = parts.size
+
+ @tailrec
+ def makePerms(results: List[String], prefix: Int, remaining: List[Seq[String]]): Seq[String] = {
+ remaining match {
+ case Nil => results
+ case head :: tail =>
+ val column = head.map(str => Nil.padTo(prefix, None) ::: (Some(str) :: Nil.padTo(count - prefix - 1, None)))
+ val built = (column map toRoutingKey).toList
+ makePerms(built ++ results, prefix + 1, tail)
+ }
+ }
+
+ makePerms(Nil, 0, parts.toList)
+ }
+
+ def makeKey(parts: Seq[String]): String = {
+ parts.map(_.replace('.', '-')).mkString(".")
+ }
+}
+
+trait SubscriptionDescriptor[Proto] {
+
+ def keyParts(payload: Proto): Seq[String]
+
+ def notification(eventType: SubscriptionEventType, payload: Proto): Array[Byte]
+}
+
+trait SubscriptionChannelBinder {
+ def bindAll(queue: String)
+ def bindEach(queue: String, params: Seq[Seq[String]])
+ def bindTogether(queue: String, params: Seq[Seq[Option[String]]])
+}
+
+class SubscriptionChannelManager[Proto](descriptor: SubscriptionDescriptor[Proto], protected val ops: AmqpServiceOperations, protected val exchange: String)
+ extends SubscriptionChannelBinder
+ with ChannelBinderImpl
+ with ModelEventTransformer[Proto]
+ with Logging {
+
+ import NotificationConversions._
+ import SubscriptionChannelManager._
+
+ ops.declareExchange(exchange)
+
+ def toMessage(typ: ModelEvent, event: Proto): AmqpAddressedMessage = {
+ val key = makeKey(descriptor.keyParts(event))
+ val notification = descriptor.notification(protoType(typ), event)
+ AmqpAddressedMessage(exchange, key, AmqpMessage(notification, None, None))
+ }
+}
+
+class SubscriptionChannelBinderOnly(protected val ops: AmqpServiceOperations, protected val exchange: String) extends SubscriptionChannelBinder with ChannelBinderImpl {
+
+ ops.declareExchange(exchange)
+}
+
+trait ChannelBinderImpl extends Logging {
+ protected val ops: AmqpServiceOperations
+ protected val exchange: String
+
+ import SubscriptionChannelManager._
+
+ def bindAll(queue: String) {
+ logger.debug(s"Binding queue $queue to exchange $exchange with key #")
+ ops.bindQueue(queue, exchange, "#")
+ }
+
+ def bindEach(queue: String, params: Seq[Seq[String]]): Unit = {
+ val keys = routingKeyPermutations(params)
+
+ if (keys.isEmpty) {
+ bindAll(queue)
+ } else {
+ logger.debug(s"Binding queue $queue to exchange $exchange with keys: " + keys)
+ keys.foreach(key => ops.bindQueue(queue, exchange, key))
+ }
+ }
+
+ def bindTogether(queue: String, params: Seq[Seq[Option[String]]]): Unit = {
+ val keys = params.map(toRoutingKey)
+
+ if (keys.isEmpty) {
+ bindAll(queue)
+ } else {
+ logger.debug(s"Binding queue $queue to exchange $exchange with keys: " + keys)
+ keys.foreach(key => ops.bindQueue(queue, exchange, key))
+ }
+
+ }
+}
+
+trait ModelEventTransformer[A] {
+ def toMessage(typ: ModelEvent, event: A): AmqpAddressedMessage
+}
+
+sealed trait ModelEvent
+case object Created extends ModelEvent
+case object Updated extends ModelEvent
+case object Deleted extends ModelEvent
+
+trait ModelNotifier {
+ def notify[A](typ: ModelEvent, event: A)
+}
+
+object NullModelNotifier extends ModelNotifier {
+ def notify[A](typ: ModelEvent, event: A) {}
+}
+
+class BufferingModelNotifier(mapper: ModelEventMapper, ops: AmqpServiceOperations, observer: (Int, Int) => Unit) extends ModelNotifier {
+ private val buffer = scala.collection.mutable.ArrayBuffer.empty[AmqpAddressedMessage]
+
+ def notify[A](typ: ModelEvent, event: A) {
+ mapper.toMessage(typ, event).foreach(buffer +=)
+ }
+
+ def flush() {
+ val size = buffer.size
+ val flushStart = System.currentTimeMillis()
+
+ ops.publishBatch(buffer.toSeq)
+
+ val flushTime = (System.currentTimeMillis() - flushStart).toInt
+ observer(size, flushTime)
+ }
+
+}
+
+class SimpleModelNotifier(mapper: ModelEventMapper, ops: AmqpServiceOperations, observer: (Int, Int) => Unit) extends ModelNotifier {
+ def notify[A](typ: ModelEvent, event: A) {
+ mapper.toMessage(typ, event).foreach { msg =>
+ ops.publishEvent(msg.exchange, msg.message.msg, msg.key)
+ }
+ }
+}
+
+class ModelEventMapper extends Logging {
+
+ private val map = mutable.Map.empty[Class[_], ModelEventTransformer[_]]
+ private val missed = mutable.Set.empty[Class[_]]
+
+ def register[A](klass: Class[A], publisher: ModelEventTransformer[A]) {
+ map.update(klass, publisher)
+ }
+
+ def toMessage[A](typ: ModelEvent, event: A): Option[AmqpAddressedMessage] = {
+ val klass = event.getClass
+ val transformerOpt = map.get(klass)
+ if (transformerOpt.isEmpty && !missed.contains(klass)) {
+ logger.warn("No event publisher for class: " + klass)
+ missed += klass
+ }
+ transformerOpt.map(trans => trans.asInstanceOf[ModelEventTransformer[A]].toMessage(typ, event))
+ }
+}
diff --git a/services/src/main/scala/io/greenbus/services/framework/TypedServiceHandler.scala b/services/src/main/scala/io/greenbus/services/framework/TypedServiceHandler.scala
new file mode 100644
index 0000000..c783896
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/framework/TypedServiceHandler.scala
@@ -0,0 +1,38 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.framework
+
+import io.greenbus.client.proto.Envelope
+
+sealed trait Response[+A]
+case class Failure(status: Envelope.Status, message: String) extends Response[Nothing]
+case class Success[A](status: Envelope.Status, response: A) extends Response[A]
+
+trait TypedServiceHandler[A, B] {
+ def handle(request: A, headers: Map[String, String], responseHandler: Response[B] => Unit)
+}
+
+trait SyncTypedServiceHandler[A, B] extends TypedServiceHandler[A, B] {
+
+ def handle(request: A, headers: Map[String, String], responseHandler: (Response[B]) => Unit) {
+ responseHandler(handle(request, headers))
+ }
+
+ def handle(request: A, headers: Map[String, String]): Response[B]
+}
diff --git a/services/src/main/scala/io/greenbus/services/model/AuthModel.scala b/services/src/main/scala/io/greenbus/services/model/AuthModel.scala
new file mode 100644
index 0000000..4752a34
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/model/AuthModel.scala
@@ -0,0 +1,693 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.model
+
+import java.util.UUID
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.client.service.proto.Auth.{ Agent, Permission, PermissionSet }
+import io.greenbus.client.service.proto.Model.ModelUUID
+import io.greenbus.services.SqlAuthenticationModule
+import io.greenbus.services.data.ServicesSchema._
+import io.greenbus.services.data._
+import io.greenbus.services.framework._
+import io.greenbus.services.model.UUIDHelpers._
+import org.squeryl.PrimitiveTypeMode._
+import org.squeryl.dsl.ast.{ LogicalBoolean, ExpressionNode }
+
+import scala.collection.JavaConversions._
+
+object AuthModel {
+ val permissionSetType = "PermissionSet"
+ val agentType = "Agent"
+
+ case class AgentInfo(passwordOpt: Option[String], permissionSets: Seq[String])
+ case class PermissionSetInfo(permissions: Seq[Permission])
+
+ case class CoreUntypedTemplate[ID, A](idOpt: Option[ID], name: String, info: A)
+ case class IdentifiedUntypedTemplate[ID, A](id: ID, name: String, info: A)
+ case class NamedUntypedTemplate[A](name: String, info: A)
+
+ def splitTemplates[ID, A](templates: Seq[CoreUntypedTemplate[ID, A]]): (Seq[IdentifiedUntypedTemplate[ID, A]], Seq[NamedUntypedTemplate[A]]) = {
+ val (hasIds, noIds) = templates.partition(_.idOpt.nonEmpty)
+
+ val withIds = hasIds.map(t => IdentifiedUntypedTemplate(t.idOpt.get, t.name, t.info))
+ val withNames = noIds.map(t => NamedUntypedTemplate(t.name, t.info))
+
+ (withIds, withNames)
+ }
+}
+
+trait AuthModel {
+ import io.greenbus.services.model.AuthModel._
+
+ def agentIdForName(name: String): Option[UUID]
+ def createTokenAndStore(agentId: UUID, expiration: Long, clientVersion: String, location: String): String
+
+ def simpleLogin(name: String, password: String, expiration: Long, clientVersion: String, location: String): Either[String, (String, ModelUUID)]
+ def simpleLogout(token: String)
+ def authValidate(token: String): Boolean
+
+ def agentKeyQuery(uuids: Seq[UUID], names: Seq[String]): Seq[Agent]
+ def agentQuery(self: Option[UUID], setNames: Seq[String], lastUuid: Option[UUID], lastName: Option[String], pageSize: Int, pageByName: Boolean = true): Seq[Agent]
+ def putAgents(notifier: ModelNotifier, templates: Seq[CoreUntypedTemplate[UUID, AgentInfo]], allowCreate: Boolean = true): Seq[Agent]
+ def modifyAgentPasswords(updates: Seq[(UUID, String)]): Seq[Agent]
+ def deleteAgents(notifier: ModelNotifier, ids: Seq[UUID]): Seq[Agent]
+
+ def permissionSetKeyQuery(ids: Seq[Long], names: Seq[String]): Seq[PermissionSet]
+ def permissionSetQuery(last: Option[Long], pageSize: Int): Seq[PermissionSet]
+ def permissionSetSelfKeyQuery(self: UUID, ids: Seq[Long], names: Seq[String]): Seq[PermissionSet]
+ def permissionSetSelfQuery(self: UUID, last: Option[Long], pageSize: Int): Seq[PermissionSet]
+ def putPermissionSets(notifier: ModelNotifier, templates: Seq[CoreUntypedTemplate[Long, PermissionSetInfo]], allowCreate: Boolean = true): Seq[PermissionSet]
+ def deletePermissionSets(notifier: ModelNotifier, ids: Seq[Long]): Seq[PermissionSet]
+}
+
+class SquerylAuthModule extends SqlAuthenticationModule {
+ def authenticate(name: String, password: String /*, expiration: Long, clientVersion: String, location: String*/ ): (Boolean, Option[UUID]) = {
+ val agentOpt = from(agents)(agent =>
+ where(agent.name === name)
+ select (agent)).toSeq.headOption
+
+ agentOpt match {
+ case None => (false, None)
+ case Some(agent) =>
+ agent.checkPassword(password) match {
+ case false => (false, Some(agent.id))
+ case true => (true, Some(agent.id))
+ }
+ }
+
+ }
+}
+
+object SquerylAuthModel extends AuthModel with Logging {
+ import io.greenbus.services.model.AuthModel._
+
+ def agentIdForName(name: String): Option[UUID] = {
+ from(agents)(agent =>
+ where(agent.name === name)
+ select (agent)).toSeq.headOption.map(_.id)
+ }
+
+ def createTokenAndStore(agentId: UUID, expiration: Long, clientVersion: String, location: String): String = {
+ // Random UUID is cryptographically sound and unguessable
+ // http://docs.oracle.com/javase/1.5.0/docs/api/java/util/UUID.html#randomUUID()
+ val authToken = UUID.randomUUID().toString
+ val issueTime = System.currentTimeMillis()
+
+ val tokenRow = authTokens.insert(AuthTokenRow(0, authToken, agentId, location, clientVersion, false, issueTime, expiration))
+
+ val agentPermissions = from(agentSetJoins)(j => where(j.agentId === agentId) select (j.permissionSetId)).toSeq
+
+ val joinInserts = agentPermissions.map(AuthTokenPermissionSetJoinRow(_, tokenRow.id))
+ tokenSetJoins.insert(joinInserts)
+
+ authToken
+ }
+
+ def simpleLogin(name: String, password: String, expiration: Long, clientVersion: String, location: String): Either[String, (String, ModelUUID)] = {
+
+ val agentOpt = from(agents)(agent =>
+ where(agent.name === name)
+ select (agent)).toSeq.headOption
+
+ agentOpt match {
+ case None => Left("Invalid username or password")
+ case Some(agent) => {
+ agent.checkPassword(password) match {
+ case false => Left("Invalid username or password")
+ case true => {
+ // Random UUID is cryptographically sound and unguessable
+ // http://docs.oracle.com/javase/1.5.0/docs/api/java/util/UUID.html#randomUUID()
+ val authToken = UUID.randomUUID().toString
+ val issueTime = System.currentTimeMillis()
+
+ val tokenRow = authTokens.insert(AuthTokenRow(0, authToken, agent.id, location, clientVersion, false, issueTime, expiration))
+
+ val agentPermissions = from(agentSetJoins)(j => where(j.agentId === agent.id) select (j.permissionSetId)).toSeq
+
+ val joinInserts = agentPermissions.map(AuthTokenPermissionSetJoinRow(_, tokenRow.id))
+ tokenSetJoins.insert(joinInserts)
+ Right((authToken, uuidToProtoUUID(agent.id)))
+ }
+ }
+ }
+ }
+ }
+
+ def simpleLogout(token: String) {
+ val current = authTokens.where(t => t.token === token).headOption
+
+ current match {
+ case None =>
+ case Some(tokenRow) =>
+ if (!tokenRow.revoked) {
+ authTokens.update(tokenRow.copy(revoked = true))
+ }
+ }
+ }
+
+ private def lookupToken(token: String): Option[(AuthTokenRow, UUID, String, Seq[Array[Byte]])] = {
+ val now = System.currentTimeMillis
+
+ val tokenAndSets = join(authTokens, permissionSets.leftOuter)((tok, set) =>
+ where(tok.token === token and
+ (tok.expirationTime gt now) and
+ tok.revoked === false)
+ select (tok, set.map(_.protoBytes))
+ on (set.map(_.id) in
+ from(tokenSetJoins)(j =>
+ where(j.authTokenId === tok.id)
+ select (j.permissionSetId)))).toSeq
+
+ val authTokenRow = tokenAndSets.headOption.map(_._1)
+
+ authTokenRow.flatMap { tokenRow =>
+ val permBytes = tokenAndSets.flatMap(_._2)
+ val resultOpt =
+ from(agents)(a =>
+ where(a.id === tokenRow.agentId)
+ select (a.id, a.name)).headOption
+
+ if (resultOpt.isEmpty) {
+ logger.warn("Auth token retrieved without associated agent")
+ }
+
+ resultOpt.map {
+ case (agentId, agentName) =>
+ (tokenRow, agentId, agentName, permBytes)
+ }
+ }
+ }
+
+ def authLookup(token: String): Option[(UUID, String, Seq[Array[Byte]])] = {
+ lookupToken(token).map {
+ case (_, uuid, name, perms) => (uuid, name, perms)
+ }
+ }
+
+ def authValidate(token: String): Boolean = {
+ lookupToken(token).nonEmpty
+ }
+
+ def agentKeyQuery(uuids: Seq[UUID], names: Seq[String]): Seq[Agent] = {
+
+ val agentInfo: Seq[(UUID, String)] =
+ from(agents)(agent =>
+ where((agent.id in uuids) or (agent.name in names))
+ select ((agent.id, agent.name))).toSeq
+
+ val agentIds = agentInfo.map(_._1)
+
+ val agentIdToSetNames: Map[UUID, Seq[String]] =
+ from(agentSetJoins, permissionSets)((j, set) =>
+ where((j.agentId in agentIds) and
+ j.permissionSetId === set.id)
+ select (j.agentId, set.name))
+ .groupBy(_._1)
+ .toMap
+ .mapValues(_.map(_._2).toSeq)
+
+ agentInfo.map {
+ case (uuid, name) =>
+ val sets = agentIdToSetNames.getOrElse(uuid, Seq())
+ Agent.newBuilder()
+ .setUuid(uuid)
+ .setName(name)
+ .addAllPermissionSets(sets)
+ .build
+ }
+
+ /*
+ Runtime error ?!?!
+ val results: Seq[((UUID, String), Option[String])] =
+ from(entityQuery, agents, entities.leftOuter)((ent, agent, setEnt) =>
+ where(ent.id === agent.entityId and
+ (setEnt.map(_.id) in from(agentSetJoins, permissionSets)((j, set) =>
+ where(j.agentId === agent.id and j.permissionSetId === set.id)
+ select(set.entityId))))
+ select((ent.id, ent.name), setEnt.map(_.name))
+ orderBy(ent.id)).toSeq
+
+ val grouped: Seq[((UUID, String), Seq[String])] = groupSortedAndPreserveOrder(results) map { tup => (tup._1, tup._2.flatten) }
+
+ grouped.map {
+ case ((uuid, name), sets) =>
+ Agent.newBuilder()
+ .setUuid(uuid)
+ .setName(name)
+ .addAllPermissionSets(sets)
+ .build
+ } */
+ }
+
+ def agentQuery(self: Option[UUID], setNames: Seq[String], lastUuid: Option[UUID], lastName: Option[String], pageSize: Int, pageByName: Boolean = true): Seq[Agent] = {
+
+ def ordering(ag: AgentRow): ExpressionNode = {
+ if (pageByName) {
+ ag.name
+ } else {
+ ag.id
+ }
+ }
+
+ def guardFunc(lastUuid: Option[UUID], lastName: Option[String], pageByName: Boolean = true): AgentRow => LogicalBoolean = {
+ if (pageByName) {
+
+ val nameOpt = lastName match {
+ case Some(name) => Some(name)
+ case None =>
+ lastUuid match {
+ case Some(uuid) => entities.where(t => t.id === uuid).headOption.map(_.name)
+ case None => None
+ }
+ }
+
+ def pageGuard(ag: AgentRow) = ag.name gt nameOpt.?
+ pageGuard
+
+ } else {
+
+ val uuidOpt = lastUuid match {
+ case Some(uuid) => Some(uuid)
+ case None =>
+ lastName match {
+ case Some(name) => entities.where(t => t.name === name).headOption.map(_.id)
+ case None => None
+ }
+ }
+
+ def pageGuard(ag: AgentRow) = ag.id > uuidOpt.?
+ pageGuard
+ }
+ }
+
+ val guard = guardFunc(lastUuid, lastName, pageByName)
+
+ val agentResults: Seq[(UUID, String)] = if (setNames.nonEmpty) {
+ from(agents, agentSetJoins, permissionSets)((agent, j, set) =>
+ where(
+ guard(agent) and
+ agent.id === self.? and
+ j.agentId === agent.id and
+ j.permissionSetId === set.id and
+ (set.name in setNames).inhibitWhen(setNames.isEmpty))
+ select (agent.id, agent.name)
+ orderBy (ordering(agent)))
+ .page(0, pageSize)
+ .toVector
+ .distinct
+ } else {
+ from(agents)(agent =>
+ where(guard(agent) and
+ agent.id === self.?)
+ select (agent.id, agent.name)
+ orderBy (ordering(agent)))
+ .page(0, pageSize)
+ .toVector
+ .distinct
+ }
+
+ val agentIds = agentResults.map(_._1).toSet
+
+ val agentUuidToSetNames: Map[UUID, Seq[String]] =
+ from(agents, agentSetJoins, permissionSets)((agent, j, set) =>
+ where(
+ (agent.id in agentIds) and
+ j.agentId === agent.id and
+ j.permissionSetId === set.id)
+ select (agent.id, set.name))
+ .groupBy(_._1)
+ .toMap
+ .mapValues(_.map(_._2).toSeq)
+
+ agentResults.map {
+ case (uuid, name) =>
+ val sets = agentUuidToSetNames.getOrElse(uuid, Nil)
+ Agent.newBuilder()
+ .setUuid(uuid)
+ .setName(name)
+ .addAllPermissionSets(sets)
+ .build
+ }
+
+ }
+
+ private def agentsByKey(uuids: Seq[UUID], names: Seq[String]): Seq[(AgentRow, Seq[String])] = {
+ val agentInfo: Seq[AgentRow] =
+ from(agents)(agent =>
+ where((agent.id in uuids) or (agent.name in names))
+ select (agent)).toVector
+
+ val agentIds = agentInfo.map(_.id)
+
+ val agentIdToSetNames: Map[UUID, Seq[String]] =
+ from(agentSetJoins, permissionSets)((j, set) =>
+ where((j.agentId in agentIds) and
+ j.permissionSetId === set.id)
+ select (j.agentId, set.name))
+ .groupBy(_._1)
+ .toMap
+ .mapValues(_.map(_._2).toVector)
+
+ agentInfo.map { agent =>
+ val sets = agentIdToSetNames.getOrElse(agent.id, Seq())
+ (agent, sets)
+ }
+ }
+
+ def putAgents(notifier: ModelNotifier, templates: Seq[CoreUntypedTemplate[UUID, AgentInfo]], allowCreate: Boolean = true): Seq[Agent] = {
+
+ // For any given agent, three things can change: the entity name/types, the agent table entry (password) or the permission set joins
+ // We do the first two with common code and handle the latter manually
+
+ val allSetNames = templates.flatMap(_.info.permissionSets).toSet
+
+ val existingSets = if (allSetNames.nonEmpty) {
+ from(permissionSets)(perm =>
+ where(perm.name in allSetNames)
+ select (perm.name, perm.id)).toVector
+ } else {
+ Seq()
+ }
+
+ val setMap: Map[String, Long] = existingSets.toMap
+
+ val bogusSets = allSetNames.filterNot(setMap.contains)
+ if (bogusSets.nonEmpty) {
+ throw new ModelInputException(s"Permission sets requested do not exist: " + bogusSets.mkString(", "))
+ }
+
+ val (withIds, withNames) = AuthModel.splitTemplates(templates)
+
+ val templateIds = withIds.map(_.id)
+ val templateNames = withNames.map(_.name)
+
+ val existing = agentsByKey(templateIds, templateNames)
+
+ val existingIdSet = existing.map(_._1.id).toSet
+ val existingNameSet = existing.map(_._1.name).toSet
+
+ val nonexistentByIds = withIds.filterNot(t => existingIdSet.contains(t.id))
+ val nonexistentByNames = withNames.filterNot(t => existingNameSet.contains(t.name))
+
+ if ((nonexistentByIds.nonEmpty || nonexistentByNames.nonEmpty) && !allowCreate) {
+ throw new ModelPermissionException("Must have blanket create permissions to create entities")
+ }
+
+ val withIdsById: Map[UUID, IdentifiedUntypedTemplate[UUID, AgentInfo]] = withIds.map(temp => (temp.id, temp)).toMap
+ val withNamesByName: Map[String, NamedUntypedTemplate[AgentInfo]] = withNames.map(temp => (temp.name, temp)).toMap
+
+ val agentUpdates: Seq[(UUID, Option[AgentRow], Set[String], Set[String])] = existing.flatMap {
+ case (row, permSets) =>
+
+ val optTemp: Option[(String, AgentInfo)] = withIdsById.get(row.id).map(temp => (temp.name, temp.info))
+ .orElse(withNamesByName.get(row.name).map(temp => (temp.name, temp.info)))
+
+ optTemp.flatMap {
+ case (updateName, updateInfo) =>
+
+ val passwordUpdatedOpt = updateInfo.passwordOpt.map(pass => row.withUpdatedPassword(pass))
+
+ val passwordChanged = passwordUpdatedOpt.exists(updateRow => updateRow.digest != row.digest || updateRow.salt != row.salt)
+
+ val existPermsSet = permSets.toSet
+ val updatePermsSet = updateInfo.permissionSets.toSet
+
+ val permAdds = updatePermsSet diff existPermsSet
+ val permRems = existPermsSet diff updatePermsSet
+
+ if (updateName != row.name || passwordChanged) {
+ val startFrom = passwordUpdatedOpt.getOrElse(row)
+ Some((row.id, Some(startFrom.copy(name = updateName)), permAdds, permRems))
+ } else {
+ if (permAdds.isEmpty && permRems.isEmpty) {
+ None
+ } else {
+ Some((row.id, None, permAdds, permRems))
+ }
+ }
+ }
+ }
+
+ val toCreateByIds = nonexistentByIds.map { temp =>
+ val password = temp.info.passwordOpt.getOrElse { throw new ModelInputException(s"Password must be provided for non-existent agent ${temp.name}") }
+ val permAdds = temp.info.permissionSets.map(perm => (temp.id, perm))
+ (AgentRow(temp.id, temp.name, password), permAdds)
+ }
+
+ val toCreateByNames = nonexistentByNames.map { temp =>
+ val uuid = UUID.randomUUID()
+ val password = temp.info.passwordOpt.getOrElse { throw new ModelInputException(s"Password must be provided for non-existent agent ${temp.name}") }
+ val permAdds = temp.info.permissionSets.map(perm => (uuid, perm))
+ (AgentRow(uuid, temp.name, password), permAdds)
+ }
+
+ val toCreateTupAll = toCreateByIds ++ toCreateByNames
+
+ val createRowInserts = toCreateTupAll.map(_._1)
+ val createPermAdds = toCreateTupAll.flatMap(_._2)
+
+ val rowUpdates = agentUpdates.flatMap(_._2)
+ val updatePermAdds = agentUpdates.flatMap(tup => tup._3.map(perm => (tup._1, perm)))
+ val updatePermRems = agentUpdates.flatMap(tup => tup._4.map(perm => (tup._1, perm)))
+
+ val allPermAdds = createPermAdds ++ updatePermAdds
+
+ val permAddRows = allPermAdds.map {
+ case (uuid, permName) =>
+ val permId = setMap.getOrElse(permName, throw new ModelAssertionException(s"Could not find ID for permission $permName"))
+ AgentPermissionSetJoinRow(permId, uuid)
+ }
+
+ agents.insert(createRowInserts)
+
+ agents.update(rowUpdates)
+
+ agentSetJoins.insert(permAddRows)
+
+ val permsToBeDeleted = updatePermRems.map(_._2)
+
+ val delSetMap: Map[String, Long] =
+ from(permissionSets)(set =>
+ where(set.name in permsToBeDeleted)
+ select ((set.name, set.id))).toVector.toMap
+
+ val groupedPermRems: Map[UUID, Seq[String]] = updatePermRems.groupBy(_._1).mapValues(_.map(_._2))
+
+ groupedPermRems.foreach {
+ case (uuid, names) =>
+ val permIds = names.flatMap(delSetMap.get)
+ agentSetJoins.deleteWhere(j => j.agentId === uuid and (j.permissionSetId in permIds))
+ }
+
+ val results = agentKeyQuery(templateIds, templateNames)
+
+ val createIds = toCreateTupAll.map(_._1.id).toSet
+ val updateIds = agentUpdates.map(_._1).toSet
+
+ val created = results.filter(r => createIds.contains(protoUUIDToUuid(r.getUuid)))
+ val updated = results.filter(r => updateIds.contains(protoUUIDToUuid(r.getUuid)))
+
+ created.foreach(notifier.notify(Created, _))
+ updated.foreach(notifier.notify(Updated, _))
+
+ results
+ }
+
+ def modifyAgentPasswords(updates: Seq[(UUID, String)]): Seq[Agent] = {
+ val uuids = updates.map(_._1).distinct
+
+ val all = agents.where(a => a.id in uuids).toVector
+
+ val agentByUuid: Map[UUID, AgentRow] = all.map(a => (a.id, a)).toMap
+
+ val updatedRows = updates.map {
+ case (uuid, pass) =>
+ val row = agentByUuid.getOrElse(uuid, throw new ModelInputException("Agent did not exist"))
+ row.withUpdatedPassword(pass)
+ }
+
+ agents.update(updatedRows)
+
+ agentKeyQuery(uuids, Seq())
+ }
+
+ def deleteAgents(notifier: ModelNotifier, ids: Seq[UUID]): Seq[Agent] = {
+
+ val results = agentKeyQuery(ids, Nil)
+
+ authTokens.deleteWhere(tok => tok.agentId in ids)
+ agentSetJoins.deleteWhere(j => j.agentId in ids)
+ agents.deleteWhere(agent => agent.id in ids)
+
+ results.foreach { notifier.notify(Deleted, _) }
+
+ results
+ }
+
+ def permissionSetSelfKeyQuery(self: UUID, ids: Seq[Long], names: Seq[String]): Seq[PermissionSet] = {
+
+ val results: Seq[(Long, String, Array[Byte])] =
+ from(permissionSets, agentSetJoins, agents)((set, j, agent) =>
+ where(
+ agent.id === self and
+ j.agentId === agent.id and j.permissionSetId === set.id and
+ ((set.id in ids) or (set.name in names)))
+ select (set.id, set.name, set.protoBytes)).toVector
+
+ results.map {
+ case (id, name, bytes) =>
+ PermissionSet.newBuilder(PermissionSet.parseFrom(bytes))
+ .setId(id)
+ .setName(name)
+ .build()
+ }
+ }
+
+ def permissionSetKeyQuery(ids: Seq[Long], names: Seq[String]): Seq[PermissionSet] = {
+
+ val results: Seq[(Long, String, Array[Byte])] =
+ from(permissionSets)(set =>
+ where((set.id in ids) or (set.name in names))
+ select (set.id, set.name, set.protoBytes)).toVector
+
+ results.map {
+ case (id, name, bytes) =>
+ PermissionSet.newBuilder(PermissionSet.parseFrom(bytes))
+ .setId(id)
+ .setName(name)
+ .build()
+ }
+ }
+
+ def permissionSetSelfQuery(self: UUID, last: Option[Long], pageSize: Int): Seq[PermissionSet] = {
+
+ val results = from(permissionSets, agentSetJoins, agents)((set, j, agent) =>
+ where(set.id > last.? and
+ agent.id === self and
+ j.agentId === agent.id and
+ j.permissionSetId === set.id)
+ select (set)
+ orderBy (set.id))
+ .page(0, pageSize)
+ .toVector
+
+ results.map {
+ case (row) =>
+ PermissionSet.newBuilder(PermissionSet.parseFrom(row.protoBytes))
+ .setId(row.id)
+ .setName(row.name)
+ .build()
+ }
+ }
+
+ def permissionSetQuery(last: Option[Long], pageSize: Int): Seq[PermissionSet] = {
+
+ val results = from(permissionSets)(set =>
+ where(set.id > last.?)
+ select (set)
+ orderBy (set.id))
+ .page(0, pageSize)
+ .toVector
+
+ results.map {
+ case (row) =>
+ PermissionSet.newBuilder(PermissionSet.parseFrom(row.protoBytes))
+ .setId(row.id)
+ .setName(row.name)
+ .build()
+ }
+ }
+
+ def putPermissionSets(notifier: ModelNotifier, templates: Seq[CoreUntypedTemplate[Long, PermissionSetInfo]], allowCreate: Boolean = true): Seq[PermissionSet] = {
+
+ val (withIds, withNames) = AuthModel.splitTemplates(templates)
+
+ val templateIds = withIds.map(_.id)
+ val templateNames = withNames.map(_.name)
+
+ val existing = permissionSets.where(set => (set.id in templateIds) or (set.name in templateNames)).toVector
+
+ val existingIdSet = existing.map(_.id).toSet
+ val existingNameSet = existing.map(_.name).toSet
+
+ val nonexistentByIds = withIds.filterNot(t => existingIdSet.contains(t.id))
+ val nonexistentByNames = withNames.filterNot(t => existingNameSet.contains(t.name))
+
+ if (nonexistentByIds.nonEmpty) {
+ throw new ModelInputException("Cannot create new permission sets with an ID")
+ }
+
+ if (nonexistentByNames.nonEmpty && !allowCreate) {
+ throw new ModelPermissionException("Must have blanket create permissions to create entities")
+ }
+
+ val withIdsById: Map[Long, IdentifiedUntypedTemplate[Long, PermissionSetInfo]] = withIds.map(temp => (temp.id, temp)).toMap
+ val withNamesByName: Map[String, NamedUntypedTemplate[PermissionSetInfo]] = withNames.map(temp => (temp.name, temp)).toMap
+
+ val updates = existing.flatMap { row =>
+ val optTemp: Option[(String, PermissionSetInfo)] = withIdsById.get(row.id).map(temp => (temp.name, temp.info))
+ .orElse(withNamesByName.get(row.name).map(temp => (temp.name, temp.info)))
+
+ optTemp.flatMap {
+ case (updateName, info) =>
+ val updateBytes = PermissionSet.newBuilder().addAllPermissions(info.permissions).build().toByteArray
+
+ if (updateName != row.name || !java.util.Arrays.equals(row.protoBytes, updateBytes)) {
+ Some(row.copy(name = updateName, protoBytes = updateBytes))
+ } else {
+ None
+ }
+ }
+ }
+
+ val createRows = nonexistentByNames.map { temp =>
+ val bytes = PermissionSet.newBuilder().addAllPermissions(temp.info.permissions).build().toByteArray
+ PermissionSetRow(0, temp.name, bytes)
+ }
+
+ permissionSets.update(updates)
+ permissionSets.insert(createRows)
+
+ val createNames = createRows.map(_.name).toSet
+ val updateIds = updates.map(_.id).toSet
+
+ val results = permissionSetKeyQuery(templateIds, templateNames)
+
+ results.filter(set => createNames.contains(set.getName)).foreach(notifier.notify(Created, _))
+ results.filter(set => updateIds.contains(protoIdToLong(set.getId))).foreach(notifier.notify(Updated, _))
+
+ results
+ }
+
+ def deletePermissionSets(notifier: ModelNotifier, ids: Seq[Long]): Seq[PermissionSet] = {
+
+ val results = permissionSetKeyQuery(ids, Seq())
+
+ permissionSets.deleteWhere(ps => ps.id in ids)
+ agentSetJoins.deleteWhere(j => j.permissionSetId in ids)
+ tokenSetJoins.deleteWhere(j => j.permissionSetId in ids)
+
+ results.foreach(notifier.notify(Deleted, _))
+
+ results
+ }
+
+}
diff --git a/services/src/main/scala/io/greenbus/services/model/AuthSeeding.scala b/services/src/main/scala/io/greenbus/services/model/AuthSeeding.scala
new file mode 100644
index 0000000..5bd4e7c
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/model/AuthSeeding.scala
@@ -0,0 +1,95 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.model
+
+import java.util.UUID
+
+import io.greenbus.client.service.proto.Auth.{ Agent, EntitySelector, Permission, PermissionSet }
+import io.greenbus.services.framework.NullModelNotifier
+import io.greenbus.services.model.AuthModel.{ AgentInfo, CoreUntypedTemplate, PermissionSetInfo }
+
+trait AuthSeeder {
+
+ def addPermissionSets(descs: Seq[(String, Seq[Permission])]): Seq[PermissionSet]
+
+ def addUsers(descs: Seq[(String, String, Seq[String])]): Seq[Agent]
+}
+
+object ModelAuthSeeder extends AuthSeeder {
+
+ def addPermissionSets(descs: Seq[(String, Seq[Permission])]): Seq[PermissionSet] = {
+ val templates = descs.map {
+ case (name, perms) =>
+ CoreUntypedTemplate(Option.empty[Long], name, PermissionSetInfo(perms))
+ }
+
+ SquerylAuthModel.putPermissionSets(NullModelNotifier, templates)
+ }
+
+ def addUsers(descs: Seq[(String, String, Seq[String])]): Seq[Agent] = {
+ val templates = descs.map {
+ case (name, pass, perms) => CoreUntypedTemplate(Option.empty[UUID], name, AgentInfo(Some(pass), perms))
+ }
+
+ SquerylAuthModel.putAgents(NullModelNotifier, templates)
+ }
+}
+
+object AuthSeedData {
+
+ private def makeSelector(style: String, arguments: List[String] = Nil) = {
+ val b = EntitySelector.newBuilder.setStyle(style)
+ arguments.foreach(b.addArguments)
+ b.build
+ }
+
+ private def makePermission(allow: Boolean, verbs: List[String] = List("*"), resources: List[String] = List("*"), selectors: List[EntitySelector] = Nil) = {
+ val b = Permission.newBuilder.setAllow(allow)
+ verbs.foreach(b.addActions)
+ resources.foreach(b.addResources)
+ if (selectors.isEmpty) b.addSelectors(makeSelector("*"))
+ else selectors.foreach(b.addSelectors)
+ b.build
+ }
+
+ def seed(seeder: AuthSeeder, systemPassword: String) {
+ val all = makePermission(true)
+ val readOnly = makePermission(true, List("read"))
+
+ val selfSelector = makeSelector("self")
+
+ val readAndDeleteOwnTokens = makePermission(true, List("read", "delete"), List("auth_token"), List(selfSelector))
+ val denyAuthTokens = makePermission(false, List("read", "delete"), List("auth_token"))
+
+ var permSetDescs = List.empty[(String, Seq[Permission])]
+
+ permSetDescs ::= ("read_only", List(readAndDeleteOwnTokens, denyAuthTokens, readOnly))
+ permSetDescs ::= ("all", List(all))
+ seeder.addPermissionSets(permSetDescs)
+
+ val standardRoles = List("user_role", "system_viewer")
+
+ val agentDescs = List(
+ ("system", systemPassword, List("all")),
+ ("guest", systemPassword, List("read_only")))
+
+ seeder.addUsers(agentDescs)
+
+ }
+}
diff --git a/services/src/main/scala/io/greenbus/services/model/CommandModel.scala b/services/src/main/scala/io/greenbus/services/model/CommandModel.scala
new file mode 100644
index 0000000..8e07d80
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/model/CommandModel.scala
@@ -0,0 +1,327 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.model
+
+import org.squeryl.PrimitiveTypeMode._
+import io.greenbus.services.data.ServicesSchema._
+import io.greenbus.services.data._
+import UUIDHelpers._
+import java.util.UUID
+import scala.collection.JavaConversions._
+import io.greenbus.services.framework.{ Deleted, Created, ModelNotifier }
+import io.greenbus.services.authz.EntityFilter
+import io.greenbus.client.service.proto.Commands.CommandLock
+import org.squeryl.Query
+
+class CommandLockException(msg: String) extends ModelInputException(msg)
+
+trait CommandModel {
+
+ def lockKeyQuery(lockIds: Seq[Long], agentFilter: Option[EntityFilter] = None): Seq[CommandLock]
+ def lockQuery(cmdIds: Seq[UUID], agents: Seq[UUID], accessMode: Option[CommandLock.AccessMode], lastId: Option[Long], pageSize: Int, agentFilter: Option[EntityFilter] = None): Seq[CommandLock]
+
+ def agentHasSelect(cmd: UUID, agent: UUID, filter: Option[EntityFilter] = None): Boolean
+ def agentCanIssueCommand(cmd: UUID, agent: UUID, filter: Option[EntityFilter] = None): Boolean
+
+ def selectCommands(notifier: ModelNotifier, cmdIds: Seq[UUID], agent: UUID, expireTime: Long, filter: Option[EntityFilter] = None): CommandLock
+ def blockCommands(notifier: ModelNotifier, cmdIds: Seq[UUID], agent: UUID, filter: Option[EntityFilter] = None): CommandLock
+
+ def deleteLocks(notifier: ModelNotifier, lockIds: Seq[Long], agentFilter: Option[UUID] = None): Seq[CommandLock]
+}
+
+object SquerylCommandModel extends CommandModel {
+ import ModelHelpers._
+
+ private def findApplicableLocks(cmd: UUID, agent: UUID, filter: Option[EntityFilter] = None) = {
+ filter.foreach { filt =>
+ if (filt.isOutsideSet(cmd)) {
+ throw new ModelPermissionException("Tried to lookup restricted commands")
+ }
+ }
+
+ val now = System.currentTimeMillis
+
+ from(commands, locks, lockJoins)((c, l, j) =>
+ where((c.entityId === cmd) and
+ l.agent === agent and
+ j.commandId === c.id and
+ j.lockId === l.id and
+ (l.expireTime.isNull or (l.expireTime gt now)))
+ select (l)).toVector
+ }
+
+ def agentCanIssueCommand(cmd: UUID, agent: UUID, filter: Option[EntityFilter] = None): Boolean = {
+
+ filter.foreach { filt =>
+ if (filt.isOutsideSet(cmd)) {
+ throw new ModelPermissionException("Tried to lookup restricted commands")
+ }
+ }
+
+ val now = System.currentTimeMillis
+
+ val locksForCommands = from(commands, locks, lockJoins)((c, l, j) =>
+ where((c.entityId === cmd) and
+ j.commandId === c.id and
+ j.lockId === l.id and
+ (l.expireTime.isNull or (l.expireTime gt now)))
+ select (l)).toVector
+
+ (locksForCommands.isEmpty ||
+ locksForCommands.exists(lock => lock.access == CommandLock.AccessMode.ALLOWED.getNumber && lock.agent == agent)) &&
+ !locksForCommands.exists(lock => lock.access == CommandLock.AccessMode.BLOCKED.getNumber)
+ }
+
+ def agentHasSelect(cmd: UUID, agent: UUID, filter: Option[EntityFilter] = None): Boolean = {
+
+ val applicableLocks = findApplicableLocks(cmd, agent, filter)
+
+ applicableLocks.exists(lock => lock.access == CommandLock.AccessMode.ALLOWED.getNumber && lock.agent == agent) &&
+ !applicableLocks.exists(lock => lock.access == CommandLock.AccessMode.BLOCKED.getNumber)
+ }
+
+ def lockKeyQuery(lockIds: Seq[Long], agentFilter: Option[EntityFilter] = None): Seq[CommandLock] = {
+
+ val results: Seq[(CommandLockRow, Option[UUID])] =
+ join(locks, commands.leftOuter)((lock, cmd) =>
+ where(EntityFilter.optional(agentFilter, lock.agent).inhibitWhen(agentFilter.isEmpty) and
+ (lock.id in lockIds))
+ select (lock, cmd.map(_.entityId))
+ orderBy (lock.id)
+ on (cmd.map(_.id) in from(lockJoins)(j => where(j.lockId in lockIds) select (j.commandId)))).toSeq
+
+ val grouped = groupSortedAndPreserveOrder(results)
+
+ grouped.map {
+ case (row, optCmdList) =>
+ val b = CommandLock.newBuilder()
+ .setId(row.id)
+ .setAccess(CommandLock.AccessMode.valueOf(row.access))
+ .addAllCommandUuids(optCmdList.flatten.map(uuidToProtoUUID))
+ .setAgentUuid(row.agent)
+
+ row.expireTime.foreach(b.setExpireTime)
+
+ b.build()
+ }
+
+ }
+
+ def lockQuery(cmdIds: Seq[UUID], agents: Seq[UUID], accessMode: Option[CommandLock.AccessMode], lastId: Option[Long], pageSize: Int, agentFilter: Option[EntityFilter] = None): Seq[CommandLock] = {
+
+ val idQuery: Query[Long] = if (cmdIds.isEmpty) {
+
+ from(locks)((lock) =>
+ where(EntityFilter.optional(agentFilter, lock.agent).inhibitWhen(agentFilter.isEmpty) and
+ (lock.agent in agents).inhibitWhen(agents.isEmpty) and
+ (Some(lock.access) === accessMode.map(_.getNumber)).inhibitWhen(accessMode.isEmpty) and
+ (lock.id gt lastId.?))
+ select (lock.id)).page(0, pageSize)
+
+ } else {
+
+ from(locks, lockJoins, commands)((lock, lj, cmd) =>
+ where(EntityFilter.optional(agentFilter, lock.agent).inhibitWhen(agentFilter.isEmpty) and
+ (lock.id === lj.lockId and cmd.id === lj.commandId) and
+ (lock.agent in agents).inhibitWhen(agents.isEmpty) and
+ (Some(lock.access) === accessMode.map(_.getNumber)).inhibitWhen(accessMode.isEmpty) and
+ (cmd.entityId in cmdIds) and
+ (lock.id gt lastId.?))
+ select (lock.id)).page(0, pageSize)
+ }
+
+ val lockIdList = idQuery.toList
+
+ val query: Query[(CommandLockRow, UUID)] =
+ join(locks, commands)((lock, cmd) =>
+ where(lock.id in lockIdList)
+ select (lock, cmd.entityId)
+ orderBy (lock.id)
+ on (cmd.id in from(lockJoins)(j => where(j.lockId === lock.id) select (j.commandId))))
+
+ val results: Seq[(CommandLockRow, UUID)] = query.toSeq
+
+ val locksWithCmds: Seq[(CommandLockRow, Seq[UUID])] = groupSortedAndPreserveOrder(results.toList)
+
+ /*val locksWithCmds: Seq[(CommandLockRow, Seq[UUID])] = if (cmdIds.isEmpty) {
+
+ val idQuery: Query[Long] =
+ from(locks)((lock) =>
+ where(EntityFilter.optional(agentFilter, lock.agent).inhibitWhen(agentFilter.isEmpty) and
+ (lock.agent in agents).inhibitWhen(agents.isEmpty) and
+ (Some(lock.access) === accessMode.map(_.getNumber)).inhibitWhen(accessMode.isEmpty) and
+ (lock.id gt lastId.?))
+ select (lock.id)).page(0, pageSize)
+
+ println(idQuery)
+
+ val lockIdList = idQuery.toList
+
+ val query: Query[(CommandLockRow, UUID)] =
+ join(locks, commands)((lock, cmd) =>
+ where(lock.id in lockIdList /*lks*/ /*from(lockQuery)(l => select(l.id)).page(0, pageSize)*/)
+ select (lock, cmd.entityId)
+ orderBy (lock.id)
+ on (cmd.id in from(lockJoins)(j => where(j.lockId === lock.id) select (j.commandId))))
+
+ println(query)
+
+ val results: Seq[(CommandLockRow, UUID)] = query.toSeq
+
+ groupSortedAndPreserveOrder(results.toList)
+
+ } else {
+
+ val lockQuery: Query[CommandLockRow] =
+ from(locks, lockJoins, commands)((lock, lj, cmd) =>
+ where(EntityFilter.optional(agentFilter, lock.agent).inhibitWhen(agentFilter.isEmpty) and
+ (lock.id === lj.lockId and cmd.id === lj.commandId) and
+ (lock.agent in agents).inhibitWhen(agents.isEmpty) and
+ (Some(lock.access) === accessMode.map(_.getNumber)).inhibitWhen(accessMode.isEmpty) and
+ (cmd.entityId in cmdIds) and
+ (lock.id gt lastId.?))
+ select (lock)).page(0, pageSize)
+
+ val results: Seq[(CommandLockRow, UUID)] =
+ join(locks, commands)((lock, cmd) =>
+ where(lock.id in from(lockQuery)(l => select(l.id)))
+ select (lock, cmd.entityId)
+ orderBy (lock.id)
+ on (cmd.id in from(lockJoins)(j => where(j.lockId === lock.id) select (j.commandId)))).toSeq
+
+ groupSortedAndPreserveOrder(results.toList)
+ }*/
+
+ locksWithCmds.map {
+ case (row, cmdList) =>
+ val b = CommandLock.newBuilder()
+ .setId(row.id)
+ .setAccess(CommandLock.AccessMode.valueOf(row.access))
+ .addAllCommandUuids(cmdList.map(uuidToProtoUUID))
+ .setAgentUuid(row.agent)
+
+ row.expireTime.foreach(b.setExpireTime)
+
+ b.build()
+ }
+ }
+
+ def selectCommands(notifier: ModelNotifier, cmdIds: Seq[UUID], agent: UUID, expireTime: Long, filter: Option[EntityFilter] = None): CommandLock = {
+ filter.foreach { filt =>
+ if (filt.anyOutsideSet(cmdIds)) {
+ throw new ModelPermissionException("Tried to lock restricted commands")
+ }
+ }
+
+ val lockedCmds = commands.where(c => c.entityId in cmdIds).forUpdate.toSeq
+ val now = System.currentTimeMillis
+
+ val lockedIds = lockedCmds.map(_.id)
+
+ val existingLocks =
+ from(locks, lockJoins)((l, j) =>
+ where((j.commandId in lockedIds) and
+ j.lockId === l.id and
+ (l.expireTime.isNull or (l.expireTime gt now)))
+ select (l)).toSeq
+
+ if (existingLocks.nonEmpty) {
+ throw new CommandLockException("Commands already locked")
+ }
+
+ val lock = locks.insert(CommandLockRow(0, CommandLock.AccessMode.ALLOWED.getNumber, Some(expireTime), agent))
+
+ val joins = lockedIds.map(cmdId => LockJoinRow(cmdId, lock.id))
+
+ lockJoins.insert(joins)
+
+ update(commands)(c =>
+ where(c.id in lockedIds)
+ set (c.lastSelectId := Some(lock.id)))
+
+ val created = CommandLock.newBuilder()
+ .setId(lock.id)
+ .setAccess(CommandLock.AccessMode.ALLOWED)
+ .setExpireTime(expireTime)
+ .addAllCommandUuids(lockedCmds.map(_.entityId).map(uuidToProtoUUID))
+ .setAgentUuid(agent)
+ .build()
+
+ notifier.notify(Created, created)
+
+ created
+ }
+
+ def blockCommands(notifier: ModelNotifier, cmdIds: Seq[UUID], agent: UUID, filter: Option[EntityFilter] = None): CommandLock = {
+ filter.foreach { filt =>
+ if (filt.anyOutsideSet(cmdIds)) {
+ throw new ModelPermissionException("Tried to lock restricted commands")
+ }
+ }
+
+ val lockedCmds = commands.where(c => c.entityId in cmdIds).forUpdate.toSeq
+ val lockedIds = lockedCmds.map(_.id)
+
+ val lock = locks.insert(CommandLockRow(0, CommandLock.AccessMode.BLOCKED.getNumber, None, agent))
+
+ val joins = lockedIds.map(cmdId => LockJoinRow(cmdId, lock.id))
+
+ lockJoins.insert(joins)
+
+ update(commands)(c =>
+ where(c.id in lockedIds)
+ set (c.lastSelectId := Some(lock.id)))
+
+ val created = CommandLock.newBuilder()
+ .setId(lock.id)
+ .setAccess(CommandLock.AccessMode.BLOCKED)
+ .addAllCommandUuids(lockedCmds.map(_.entityId).map(uuidToProtoUUID))
+ .setAgentUuid(agent)
+ .build()
+
+ notifier.notify(Created, created)
+
+ created
+ }
+
+ def deleteLocks(notifier: ModelNotifier, lockIds: Seq[Long], agentFilter: Option[UUID] = None): Seq[CommandLock] = {
+
+ agentFilter.foreach { agentUuid =>
+ val nonOwnedIdCount =
+ from(locks)(lock =>
+ where(not(lock.agent === agentUuid) and
+ (lock.id in lockIds))
+ select (lock)).page(0, 1).nonEmpty
+
+ if (nonOwnedIdCount) {
+ throw new ModelPermissionException("Tried to delete restricted command locks")
+ }
+ }
+
+ val results = lockKeyQuery(lockIds)
+
+ locks.deleteWhere(lock => lock.id in lockIds)
+ lockJoins.deleteWhere(j => j.lockId in lockIds)
+
+ results.foreach(notifier.notify(Deleted, _))
+
+ results
+ }
+}
+
diff --git a/services/src/main/scala/io/greenbus/services/model/EntityBasedModels.scala b/services/src/main/scala/io/greenbus/services/model/EntityBasedModels.scala
new file mode 100644
index 0000000..f92f1f7
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/model/EntityBasedModels.scala
@@ -0,0 +1,264 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.model
+
+import io.greenbus.services.data.{ EntityRow, EntityBased }
+import org.squeryl.Table
+import org.squeryl.dsl.ast.{ ExpressionNode, LogicalBoolean }
+import java.util.UUID
+import io.greenbus.services.authz.EntityFilter
+import org.squeryl.PrimitiveTypeMode._
+import io.greenbus.services.data.ServicesSchema._
+import io.greenbus.services.framework.{ Deleted, Updated, Created, ModelNotifier }
+import io.greenbus.services.model.UUIDHelpers._
+import io.greenbus.services.model.EntityModel.TypeParams
+import io.greenbus.client.service.proto.Model.ModelUUID
+import io.greenbus.services.model.FrontEndModel.{ NamedEntityTemplate, IdentifiedEntityTemplate }
+
+object EntityBasedModels {
+
+ def entityBasedQuery[DbType <: EntityBased](
+ table: Table[DbType],
+ typeParams: TypeParams,
+ rowClause: DbType => LogicalBoolean,
+ lastUuid: Option[UUID],
+ lastName: Option[String],
+ pageSize: Int,
+ pageByName: Boolean = true,
+ filter: Option[EntityFilter] = None): Seq[(UUID, String, Seq[String], DbType)] = {
+
+ val guard = SquerylEntityModel.guardFunc(lastUuid, lastName, pageByName)
+
+ def ordering(ent: EntityRow): ExpressionNode = {
+ if (pageByName) {
+ ent.name
+ } else {
+ ent.id
+ }
+ }
+
+ val query =
+ from(entities, table)((ent, row) =>
+ where(EntityFilter.optional(filter, ent).inhibitWhen(filter.isEmpty) and
+ guard(ent) and
+ ent.id === row.entityId and
+ SquerylEntityModel.entTypesClause(ent, typeParams) and
+ rowClause(row))
+ select (ent.id, ent.name, row)
+ orderBy (ordering(ent)))
+ .page(0, pageSize)
+
+ val results: Seq[(UUID, String, DbType)] = query.toVector
+
+ val entIds = results.map(_._1)
+
+ val typeMap: Map[UUID, Seq[String]] = SquerylEntityModel.entitiesWithTypes(entities.where(t => t.id in entIds)).map { case (row, types) => (row.id, types) }.toMap
+
+ results.map {
+ case (uuid, name, row) =>
+ val types = typeMap.get(uuid).getOrElse(Nil)
+ (uuid, name, types, row)
+ }
+ }
+
+ def entityBasedKeyQuery[A <: EntityBased](uuids: Seq[UUID], names: Seq[String], table: Table[A], filter: Option[EntityFilter] = None): Seq[(UUID, String, Seq[String], A)] = {
+
+ val entQuery = from(entities)(ent =>
+ where(EntityFilter.optional(filter, ent).inhibitWhen(filter.isEmpty) and
+ ((ent.id in uuids).inhibitWhen(uuids.isEmpty) or
+ (ent.name in names).inhibitWhen(names.isEmpty)))
+ select (ent))
+
+ val typeMap: Map[UUID, Seq[String]] = SquerylEntityModel.entitiesWithTypes(entQuery).map { case (row, types) => (row.id, types) }.toMap
+
+ val entAndRow: Seq[(UUID, String, A)] =
+ from(entities, table)((ent, mod) =>
+ where(EntityFilter.optional(filter, ent).inhibitWhen(filter.isEmpty) and
+ ((ent.id in uuids).inhibitWhen(uuids.isEmpty) or
+ (ent.name in names).inhibitWhen(names.isEmpty)) and
+ (mod.entityId === ent.id))
+ select (ent.id, ent.name, mod)).toSeq
+
+ entAndRow.map {
+ case (uuid, name, row) =>
+ val types = typeMap.get(uuid).getOrElse(Nil)
+ (uuid, name, types, row)
+ }
+
+ }
+
+ def existingEntityBased[DbType <: EntityBased, TemplateInfo](ids: Seq[UUID], names: Seq[String], table: Table[DbType], coreType: String): Seq[(UUID, String, DbType)] = {
+
+ val currentStateOpt: Seq[(UUID, String, Option[DbType])] =
+ join(entities, table.leftOuter)((ent, mod) =>
+ where((ent.id in ids) or (ent.name in names))
+ select (ent.id, ent.name, mod)
+ on (Some(ent.id) === mod.map(_.entityId))).toList
+
+ // It's illegal to have an entity that's not attached to a point/command/etc, and add it on top of it
+ val currentState: Seq[(UUID, String, DbType)] = currentStateOpt.map {
+ case (id, name, rowOpt) => rowOpt match {
+ case None => throw new ModelInputException(s"Entity $name already exists but is not a $coreType")
+ case Some(row) => (id, name, row)
+ }
+ }
+
+ currentState
+ }
+
+ def entityBasedCreatesAndUpdates[DbType <: EntityBased, TemplateInfo](
+ withIds: Seq[IdentifiedEntityTemplate[TemplateInfo]],
+ withNames: Seq[NamedEntityTemplate[TemplateInfo]],
+ currentMap: Map[UUID, DbType],
+ nameToUuidMap: Map[String, UUID],
+ create: (UUID, TemplateInfo) => DbType,
+ update: (UUID, DbType, TemplateInfo) => Option[DbType]): (Seq[DbType], Seq[DbType]) = {
+
+ var creates = List.empty[DbType]
+ var updates = List.empty[DbType]
+
+ withIds.map { temp =>
+ currentMap.get(temp.uuid) match {
+ case None =>
+ creates ::= create(temp.uuid, temp.info)
+ case Some(row) =>
+ update(temp.uuid, row, temp.info).foreach { up => updates ::= up }
+ }
+ }
+
+ withNames.map { temp =>
+ nameToUuidMap.get(temp.name) match {
+ case None => throw new ModelAssertionException(s"Failed to find or create entity for '${temp.name}'")
+ case Some(uuid) =>
+ currentMap.get(uuid) match {
+ case None =>
+ creates ::= create(uuid, temp.info)
+ case Some(row) =>
+ update(uuid, row, temp.info).foreach { up => updates ::= up }
+ }
+ }
+ }
+
+ (creates, updates)
+ }
+
+ def performEntityBasedCreateAndUpdates[DbType <: EntityBased, TemplateInfo](
+ notifier: ModelNotifier,
+ withIds: Seq[IdentifiedEntityTemplate[TemplateInfo]],
+ withNames: Seq[NamedEntityTemplate[TemplateInfo]],
+ coreType: String,
+ create: (UUID, TemplateInfo) => DbType,
+ update: (UUID, DbType, TemplateInfo) => Option[DbType],
+ table: Table[DbType],
+ allowCreate: Boolean = true,
+ updateFilter: Option[EntityFilter] = None): (Set[UUID], Set[UUID], Set[UUID], Map[String, UUID]) = {
+
+ val ids = withIds.map(_.uuid)
+ val names = withNames.map(_.name)
+
+ updateFilter.foreach { filter =>
+ if (filter.anyOutsideSet(entities.where(t => (t.id in ids) and (t.name in names)))) {
+ throw new ModelPermissionException("Tried to put restricted entities")
+ }
+ }
+
+ val currentState: Seq[(UUID, String, DbType)] = existingEntityBased(ids, names, table, coreType)
+
+ val currentMap: Map[UUID, DbType] = currentState.map(cs => (cs._1, cs._3)).toMap
+
+ val withIdDescs = withIds.map(tup => (tup.uuid, tup.name, tup.types))
+ val withNameDescs = withNames.map(tup => (tup.name, tup.types))
+
+ val (entCreateIds, entUpdateIds, foundOrCreatedEntities) = SquerylEntityModel.findOrCreateEntities(notifier, withIdDescs, withNameDescs, allowCreate, updateFilter)
+
+ val nameToUuidMap: Map[String, UUID] = {
+ val currentStateNameToUuidMap: Map[String, UUID] = currentState.map(cs => (cs._2, cs._1)).toMap
+ val updatedNameToUuidMap: Map[String, UUID] = foundOrCreatedEntities.map(e => (e.getName, protoUUIDToUuid(e.getUuid))).toMap
+
+ currentStateNameToUuidMap ++ updatedNameToUuidMap
+ }
+
+ val (creates, updates) = entityBasedCreatesAndUpdates(withIds, withNames, currentMap, nameToUuidMap, create, update)
+
+ if (creates.nonEmpty) {
+ table.insert(creates)
+ }
+ if (updates.nonEmpty) {
+ table.update(updates)
+ }
+
+ val allCreateIds = (creates.map(_.entityId) ++ entCreateIds).toSet
+ val allUpdateIds = (updates.map(_.entityId) ++ entUpdateIds).toSet
+
+ val allUuids = (currentState.map(_._1) ++ entCreateIds).toSet
+
+ (allCreateIds, allUpdateIds, allUuids, nameToUuidMap)
+ }
+
+ def putEntityBased[DbType <: EntityBased, TemplateInfo, ProtoType](
+ notifier: ModelNotifier,
+ withIds: Seq[IdentifiedEntityTemplate[TemplateInfo]],
+ withNames: Seq[NamedEntityTemplate[TemplateInfo]],
+ coreType: String,
+ create: (UUID, TemplateInfo) => DbType,
+ update: (UUID, DbType, TemplateInfo) => Option[DbType],
+ query: (Seq[UUID], Seq[String]) => Seq[ProtoType],
+ uuidFromProto: ProtoType => ModelUUID,
+ table: Table[DbType],
+ allowCreate: Boolean = true,
+ updateFilter: Option[EntityFilter] = None): Seq[ProtoType] = {
+
+ val ids = withIds.map(_.uuid)
+ val names = withNames.map(_.name)
+
+ val (allCreateIds, allUpdateIds, _, _) = performEntityBasedCreateAndUpdates(notifier, withIds, withNames, coreType, create, update, table, allowCreate, updateFilter)
+
+ val allResults = query(ids, names)
+ lazy val resultMap: Map[UUID, ProtoType] = allResults.map(proto => (protoUUIDToUuid(uuidFromProto(proto)), proto)).toMap
+
+ allCreateIds.flatMap(resultMap.get).foreach(notifier.notify(Created, _))
+ allUpdateIds.flatMap(resultMap.get).foreach(notifier.notify(Updated, _))
+
+ allResults
+ }
+
+ def deleteEntityBased[A <: EntityBased, C](
+ notifier: ModelNotifier,
+ ids: Seq[UUID],
+ query: Seq[UUID] => Seq[C],
+ table: Table[A],
+ filter: Option[EntityFilter] = None): Seq[C] = {
+
+ filter.foreach { filt =>
+ if (filt.anyOutsideSet(ids)) {
+ throw new ModelPermissionException("Tried to delete restricted agents")
+ }
+ }
+
+ val results = query(ids)
+
+ SquerylEntityModel.deleteEntityBased(notifier, ids)
+
+ table.deleteWhere(row => row.entityId in ids)
+
+ results.foreach(notifier.notify(Deleted, _))
+
+ results
+ }
+}
diff --git a/services/src/main/scala/io/greenbus/services/model/EntityModel.scala b/services/src/main/scala/io/greenbus/services/model/EntityModel.scala
new file mode 100644
index 0000000..e972bb7
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/model/EntityModel.scala
@@ -0,0 +1,1148 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.model
+
+import java.util.UUID
+
+import org.squeryl.PrimitiveTypeMode._
+import org.squeryl.Query
+import org.squeryl.dsl.ast.{ ExpressionNode, LogicalBoolean }
+import io.greenbus.client.service.proto.Model._
+import io.greenbus.client.service.proto.ModelRequests.EntityKeyPair
+import io.greenbus.services.authz.EntityFilter
+import io.greenbus.services.core.EntityKeyValueWithEndpoint
+import io.greenbus.services.data.ServicesSchema._
+import io.greenbus.services.data.{ EntityEdgeRow, EntityKeyStoreRow, EntityRow, EntityTypeRow }
+import io.greenbus.services.framework.{ Created, Deleted, ModelNotifier, Updated }
+
+import scala.annotation.tailrec
+import scala.collection.JavaConversions._
+import scala.collection.immutable.VectorBuilder
+
+object EntityModel {
+
+ class DiamondException(msg: String) extends ModelInputException(msg)
+
+ case class TypeParams(includeTypes: Seq[String], matchTypes: Seq[String], filterNotTypes: Seq[String])
+
+ val reservedTypes = Set("Point", "Command", "Endpoint", "ConfigFile")
+}
+
+trait EntityModel {
+ import io.greenbus.services.model.EntityModel._
+
+ def fullQuery(typeParams: TypeParams, lastUuid: Option[UUID], lastName: Option[String], pageSize: Int, pageByName: Boolean = true, filter: Option[EntityFilter] = None): Seq[Entity]
+ def keyQuery(uuids: Seq[UUID], names: Seq[String], filter: Option[EntityFilter] = None): Seq[Entity]
+
+ def keyQueryFilter(uuids: Seq[UUID], names: Seq[String], filter: Option[EntityFilter] = None): Seq[UUID]
+
+ def idsRelationFlatQuery(ids: Seq[UUID], relation: String, descendantOf: Boolean, endTypes: Seq[String], depthLimit: Option[Int], lastUuid: Option[UUID], lastName: Option[String], pageSize: Int, pageByName: Boolean = true, filter: Option[EntityFilter] = None): Seq[Entity]
+ def namesRelationFlatQuery(names: Seq[String], relation: String, descendantOf: Boolean, endTypes: Seq[String], depthLimit: Option[Int], lastUuid: Option[UUID], lastName: Option[String], pageSize: Int, pageByName: Boolean = true, filter: Option[EntityFilter] = None): Seq[Entity]
+ def typesRelationFlatQuery(types: Seq[String], relation: String, descendantOf: Boolean, endTypes: Seq[String], depthLimit: Option[Int], lastUuid: Option[UUID], lastName: Option[String], pageSize: Int, pageByName: Boolean = true, filter: Option[EntityFilter] = None): Seq[Entity]
+
+ def putEntities(notifier: ModelNotifier, withIds: Seq[(UUID, String, Set[String])], withNames: Seq[(String, Set[String])], allowCreate: Boolean = true, updateFilter: Option[EntityFilter] = None): Seq[Entity]
+ def deleteEntities(notifier: ModelNotifier, ids: Seq[UUID], filter: Option[EntityFilter] = None): Seq[Entity]
+
+ def edgeQuery(parents: Seq[UUID], relations: Seq[String], children: Seq[UUID], depthLimit: Option[Int], lastId: Option[Long], pageSize: Int, filter: Option[EntityFilter] = None): Seq[EntityEdge]
+
+ //def putEdgesOnParent(parent: UUID, children: Seq[UUID], relation: String): Seq[EntityEdge]
+ def putEdges(notifier: ModelNotifier, edgeList: Seq[(UUID, UUID)], relation: String): Seq[EntityEdge]
+ def deleteEdgesFromParent(notifier: ModelNotifier, parent: UUID, children: Seq[UUID], relation: String): Seq[EntityEdge]
+ def deleteEdges(notifier: ModelNotifier, edgeList: Seq[(UUID, UUID)], relation: String): Seq[EntityEdge]
+
+ //def getKeyValues(ids: Seq[UUID], key: String, filter: Option[EntityFilter] = None): Seq[(UUID, Array[Byte])]
+ //def putKeyValues(values: Seq[(UUID, Array[Byte])], key: String, filter: Option[EntityFilter] = None): (Seq[(UUID, Array[Byte])], Seq[UUID], Seq[UUID])
+ //def deleteKeyValues(ids: Seq[UUID], key: String, filter: Option[EntityFilter] = None): Seq[(UUID, Array[Byte])]
+
+ def getKeyValues2(set: Seq[(UUID, String)], filter: Option[EntityFilter] = None): Seq[EntityKeyValue]
+ def getKeyValuesForUuids(set: Seq[UUID], filter: Option[EntityFilter] = None): Seq[EntityKeyValue]
+ def getKeys(ids: Seq[UUID], filter: Option[EntityFilter] = None): Seq[EntityKeyPair]
+ def putKeyValues2(notifier: ModelNotifier, set: Seq[(UUID, String, StoredValue)], allowCreate: Boolean = true, updateFilter: Option[EntityFilter] = None): Seq[EntityKeyValue]
+ def deleteKeyValues2(notifier: ModelNotifier, set: Seq[(UUID, String)], filter: Option[EntityFilter] = None): Seq[EntityKeyValue]
+}
+
+object SquerylEntityModel extends EntityModel {
+ import io.greenbus.services.model.EntityModel._
+ import io.greenbus.services.model.ModelHelpers._
+ import io.greenbus.services.model.UUIDHelpers._
+
+ private def entitiesFromJoin(results: Seq[(EntityRow, Option[String])]): Seq[Entity] = {
+ groupSortedAndPreserveOrder(results).map {
+ case (row, types) =>
+ val flatTypes = types.flatten
+ Entity.newBuilder
+ .setUuid(row.id)
+ .setName(row.name)
+ .addAllTypes(flatTypes)
+ .build()
+ }
+ }
+
+ private def simpleEntity(id: UUID, name: String, types: Seq[String]): Entity = {
+ Entity.newBuilder
+ .setUuid(id)
+ .setName(name)
+ .addAllTypes(types)
+ .build()
+ }
+
+ private def filterEntities(ent: EntityRow, filter: Option[EntityFilter]): LogicalBoolean = {
+ filter match {
+ case None => true === true
+ case Some(f) => f(ent.id)
+ }
+ }
+
+ def allQuery(last: Option[UUID], pageSize: Int): Seq[Entity] = {
+
+ val sql = last match {
+ case None =>
+ join(entities, entityTypes.leftOuter)((ent, typ) =>
+ select(ent, typ.map(_.entType))
+ orderBy (ent.id)
+ on (Some(ent.id) === typ.map(_.entityId))).page(0, pageSize)
+ case Some(lastUuid) =>
+ join(entities, entityTypes.leftOuter)((ent, typ) =>
+ where(ent.id > lastUuid)
+ select (ent, typ.map(_.entType))
+ orderBy (ent.id)
+ on (Some(ent.id) === typ.map(_.entityId))).page(0, pageSize)
+ }
+
+ entitiesFromJoin(sql.toSeq)
+ }
+
+ def typeMatchClause(ent: EntityRow, matchTypes: Seq[String]): LogicalBoolean = {
+ if (matchTypes.nonEmpty) {
+ def typeExists(typ: String): LogicalBoolean = exists(from(entityTypes)(t =>
+ where(t.entityId === ent.id and t.entType === typ)
+ select (t.entityId)))
+
+ matchTypes.map(typeExists).reduceLeft(logicalAnd)
+ //matchTypes.map(typeExists).reduceLeft((a, b) => new BinaryOperatorNodeLogicalBoolean(a, b, "and"))
+ } else {
+ true === true
+ }
+ }
+
+ def entTypesClause(ent: EntityRow, params: TypeParams): LogicalBoolean = {
+ (ent.id in from(entityTypes)(t =>
+ where(t.entType in params.includeTypes)
+ select (t.entityId))).inhibitWhen(params.includeTypes.isEmpty) and
+ (ent.id notIn from(entityTypes)(t =>
+ where(t.entType in params.filterNotTypes)
+ select (t.entityId))).inhibitWhen(params.filterNotTypes.isEmpty) and
+ SquerylEntityModel.typeMatchClause(ent, params.matchTypes).inhibitWhen(params.matchTypes.isEmpty)
+ }
+
+ def guardFunc(lastUuid: Option[UUID], lastName: Option[String], pageByName: Boolean = true): EntityRow => LogicalBoolean = {
+ if (pageByName) {
+
+ val nameOpt = lastName match {
+ case Some(name) => Some(name)
+ case None =>
+ lastUuid match {
+ case Some(uuid) => entities.where(t => t.id === uuid).headOption.map(_.name)
+ case None => None
+ }
+ }
+
+ def pageGuard(ent: EntityRow) = ent.name gt nameOpt.?
+ pageGuard
+
+ } else {
+
+ val uuidOpt = lastUuid match {
+ case Some(uuid) => Some(uuid)
+ case None =>
+ lastName match {
+ case Some(name) => entities.where(t => t.name === name).headOption.map(_.id)
+ case None => None
+ }
+ }
+
+ def pageGuard(ent: EntityRow) = ent.id > uuidOpt.?
+ pageGuard
+ }
+ }
+
+ def fullQuery(typeParams: TypeParams, lastUuid: Option[UUID], lastName: Option[String], pageSize: Int, pageByName: Boolean = true, filter: Option[EntityFilter] = None): Seq[Entity] = {
+
+ val guard = SquerylEntityModel.guardFunc(lastUuid, lastName, pageByName)
+
+ def ordering(ent: EntityRow): ExpressionNode = {
+ if (pageByName) {
+ ent.name
+ } else {
+ ent.id
+ }
+ }
+
+ val entSet = from(entities)(ent =>
+ where(filterEntities(ent, filter) and
+ guard(ent) and
+ (ent.id in from(entityTypes)(t =>
+ where(t.entType in typeParams.includeTypes)
+ select (t.entityId))).inhibitWhen(typeParams.includeTypes.isEmpty) and
+ (ent.id notIn from(entityTypes)(t =>
+ where(t.entType in typeParams.filterNotTypes)
+ select (t.entityId))).inhibitWhen(typeParams.filterNotTypes.isEmpty) and
+ typeMatchClause(ent, typeParams.matchTypes).inhibitWhen(typeParams.matchTypes.isEmpty))
+ select (ent.id)
+ orderBy (ordering(ent))).page(0, pageSize)
+
+ val entList = entSet.toList
+
+ val sql = join(entities, entityTypes.leftOuter)((ent, typ) =>
+ where(ent.id in entList)
+ select (ent, typ.map(_.entType))
+ orderBy (ordering(ent))
+ on (Some(ent.id) === typ.map(_.entityId)))
+
+ entitiesFromJoin(sql.toVector)
+ }
+
+ def keyQueryFilter(uuids: Seq[UUID], names: Seq[String], filter: Option[EntityFilter] = None): Seq[UUID] = {
+ from(entities)(ent =>
+ where(filterEntities(ent, filter).inhibitWhen(filter.isEmpty) and
+ ((ent.id in uuids).inhibitWhen(uuids.isEmpty) or
+ (ent.name in names).inhibitWhen(names.isEmpty)))
+ select (ent.id)).toList
+ }
+
+ def keyQuery(uuids: Seq[UUID], names: Seq[String], filter: Option[EntityFilter] = None): Seq[Entity] = {
+ if (uuids.isEmpty && names.isEmpty) {
+ throw new ModelInputException("Must include uuids or names for querying")
+ }
+
+ val sql = join(entities, entityTypes.leftOuter)((ent, typ) =>
+ where(filterEntities(ent, filter).inhibitWhen(filter.isEmpty) and
+ ((ent.id in uuids).inhibitWhen(uuids.isEmpty) or
+ (ent.name in names).inhibitWhen(names.isEmpty)))
+ select (ent, typ.map(_.entType))
+ orderBy (ent.id)
+ on (Some(ent.id) === typ.map(_.entityId)))
+
+ entitiesFromJoin(sql.toSeq)
+ }
+
+ def keyQueryAbstract(uuids: Seq[UUID], names: Seq[String], filter: Option[EntityFilter] = None): Query[EntityRow] = {
+ if (uuids.isEmpty && names.isEmpty) {
+ throw new ModelInputException("Must include uuids or names for querying")
+ }
+
+ from(entities)(ent =>
+ where(filterEntities(ent, filter).inhibitWhen(filter.isEmpty) and
+ ((ent.id in uuids).inhibitWhen(uuids.isEmpty) or
+ (ent.name in names).inhibitWhen(names.isEmpty)))
+ select (ent))
+ }
+
+ def idQuery(uuids: Seq[UUID], filter: Option[EntityFilter] = None): Seq[Entity] = {
+ keyQuery(uuids, Nil, filter)
+ }
+
+ def nameQuery(names: Seq[String], filter: Option[EntityFilter] = None): Seq[Entity] = {
+ keyQuery(Nil, names, filter)
+ }
+
+ def idsRelationFlatQuery(ids: Seq[UUID], relation: String, descendantOf: Boolean, endTypes: Seq[String], depthLimit: Option[Int], lastUuid: Option[UUID], lastName: Option[String], pageSize: Int, pageByName: Boolean = true, filter: Option[EntityFilter] = None): Seq[Entity] = {
+ val start = from(entities)(e => where(filterEntities(e, filter) and (e.id in ids)) select (e.id))
+
+ relationFlatQuery(start, relation, descendantOf, endTypes, depthLimit, lastUuid, lastName, pageSize, pageByName, filter)
+ }
+
+ def namesRelationFlatQuery(names: Seq[String], relation: String, descendantOf: Boolean, endTypes: Seq[String], depthLimit: Option[Int], lastUuid: Option[UUID], lastName: Option[String], pageSize: Int, pageByName: Boolean = true, filter: Option[EntityFilter] = None): Seq[Entity] = {
+ val start = from(entities)(e => where(filterEntities(e, filter) and (e.name in names)) select (e.id))
+
+ relationFlatQuery(start, relation, descendantOf, endTypes, depthLimit, lastUuid, lastName, pageSize, pageByName, filter)
+ }
+
+ def typesRelationFlatQuery(types: Seq[String], relation: String, descendantOf: Boolean, endTypes: Seq[String], depthLimit: Option[Int], lastUuid: Option[UUID], lastName: Option[String], pageSize: Int, pageByName: Boolean = true, filter: Option[EntityFilter] = None): Seq[Entity] = {
+ val start = from(entities, entityTypes)((ent, typ) =>
+ where(filterEntities(ent, filter) and ent.id === typ.entityId and (typ.entType in types))
+ select (ent.id))
+
+ relationFlatQuery(start, relation, descendantOf, endTypes, depthLimit, lastUuid, lastName, pageSize, pageByName, filter)
+ }
+
+ private def relationFlatQuery(startQuery: Query[UUID], relation: String, descendantOf: Boolean, endTypes: Seq[String], depthLimit: Option[Int], lastUuid: Option[UUID], lastName: Option[String], pageSize: Int, pageByName: Boolean = true, filter: Option[EntityFilter] = None): Seq[Entity] = {
+
+ val guard = SquerylEntityModel.guardFunc(lastUuid, lastName, pageByName)
+
+ def ordering(ent: EntityRow): ExpressionNode = {
+ if (pageByName) {
+ ent.name
+ } else {
+ ent.id
+ }
+ }
+
+ // Note: inhibitWhen appears not to work on exists()
+ val entSet = endTypes.isEmpty match {
+ case true =>
+ from(entities, edges)((ent, edge) =>
+ where(filterEntities(ent, filter) and
+ edge.relationship === relation and
+ (edge.distance lte depthLimit.?) and
+ ((edge.parentId in startQuery) and
+ (edge.childId === ent.id)).inhibitWhen(!descendantOf) and
+ ((edge.childId in startQuery) and
+ (edge.parentId === ent.id)).inhibitWhen(descendantOf))
+ select (ent.id))
+ case false =>
+ from(entities, edges)((ent, edge) =>
+ where(filterEntities(ent, filter) and
+ edge.relationship === relation and
+ (edge.distance lte depthLimit.?) and
+ ((edge.parentId in startQuery) and
+ (edge.childId === ent.id)).inhibitWhen(!descendantOf) and
+ ((edge.childId in startQuery) and
+ (edge.parentId === ent.id)).inhibitWhen(descendantOf) and
+ exists(
+ from(entityTypes)(typ =>
+ where(typ.entityId === ent.id and
+ (typ.entType in endTypes))
+ select (typ.entityId))))
+ select (ent.id))
+ }
+
+ val results = join(entities, entityTypes.leftOuter)((ent, typ) =>
+ where(guard(ent) and (ent.id in entSet))
+ select (ent, typ.map(_.entType))
+ orderBy (ordering(ent))
+ on (Some(ent.id) === typ.map(_.entityId))).page(0, pageSize)
+
+ entitiesFromJoin(results.toSeq)
+ }
+
+ private def entityIdFilter(id: UUID, filter: Option[EntityFilter]): LogicalBoolean = {
+ filter match {
+ case None => true === true
+ case Some(filt) => filt(id)
+ }
+ }
+
+ def edgeQuery(parents: Seq[UUID], relations: Seq[String], children: Seq[UUID], depthLimit: Option[Int], lastId: Option[Long], pageSize: Int, filter: Option[EntityFilter] = None): Seq[EntityEdge] = {
+
+ val edgeResults =
+ from(edges)(edge =>
+ where((edge.id > lastId.?) and
+ (entityIdFilter(edge.parentId, filter) and
+ entityIdFilter(edge.childId, filter)).inhibitWhen(filter.isEmpty) and
+ (edge.parentId in parents).inhibitWhen(parents.isEmpty) and
+ (edge.childId in children).inhibitWhen(children.isEmpty) and
+ (edge.relationship in relations).inhibitWhen(relations.isEmpty) and
+ (edge.distance lte depthLimit.?))
+ select (edge)
+ orderBy (edge.id)).page(0, pageSize).toVector
+
+ edgeResults map edgeRowToProto
+ }
+
+ def putEntity(notifier: ModelNotifier, name: String, types: Seq[String], preserveTypes: Boolean, mustHaveAllTypes: Boolean, mustOnlyHaveTypes: Boolean, allowCreate: Boolean = true, filter: Option[EntityFilter] = None): EntityRow = {
+
+ val entOption = entities.where(e => e.name === name).toSeq.headOption
+
+ entOption match {
+ case Some(existing) => {
+
+ filter.foreach { filt =>
+ if (filt.isOutsideSet(existing.id)) {
+ throw new ModelPermissionException("Don't have permissions for entity")
+ }
+ }
+
+ val existingTypes =
+ from(entityTypes)(t =>
+ where(t.entityId === existing.id)
+ select (t.entType)).toSeq
+
+ val existingTypesSet = existingTypes.toSet
+ val wantedTypesSet = types.toSet
+
+ val onlyInExisting = existingTypesSet &~ wantedTypesSet
+ if (mustOnlyHaveTypes && onlyInExisting.nonEmpty) {
+ throw new ModelInputException("Existing entity had extra types, refusing to overwrite")
+ }
+
+ val onlyInWanted = wantedTypesSet &~ existingTypesSet
+ if (mustHaveAllTypes && onlyInWanted.nonEmpty) {
+ throw new ModelInputException("Existing entity did not include types, refusing to overwrite")
+ }
+
+ if (!preserveTypes && onlyInExisting.nonEmpty) {
+ entityTypes.deleteWhere(t => t.entityId === existing.id and (t.entType in onlyInExisting))
+ }
+
+ if (onlyInWanted.nonEmpty) {
+ val typeInserts = onlyInWanted.map(EntityTypeRow(existing.id, _))
+ entityTypes.insert(typeInserts)
+ }
+
+ val allTypes = if (!preserveTypes) wantedTypesSet else wantedTypesSet ++ onlyInExisting
+
+ if ((!preserveTypes && onlyInExisting.nonEmpty) || onlyInWanted.nonEmpty) {
+ notifier.notify(Updated, simpleEntity(existing.id, name, allTypes.toSeq))
+ }
+
+ existing
+ }
+ case None => {
+ if (!allowCreate) {
+ throw new ModelPermissionException("Entity create not allowed")
+ }
+
+ val ent = entities.insert(EntityRow(UUID.randomUUID(), name))
+ val typeInserts = types.map(EntityTypeRow(ent.id, _))
+ entityTypes.insert(typeInserts)
+
+ notifier.notify(Created, simpleEntity(ent.id, name, types))
+
+ ent
+ }
+ }
+ }
+
+ def entitiesWithTypes(ents: Query[EntityRow]): Seq[(EntityRow, Seq[String])] = {
+
+ val joins = join(ents, entityTypes.leftOuter)((ent, typ) =>
+ select(ent, typ.map(_.entType))
+ orderBy (ent.id)
+ on (Some(ent.id) === typ.map(_.entityId)))
+
+ groupSortedAndPreserveOrder(joins.toSeq)
+ .map { case (row, optTypes) => (row, optTypes.flatten) }
+ }
+
+ private def typeDiff(existingTypes: Set[String], wantedTypes: Set[String]): (Set[String], Set[String]) = {
+
+ lazy val existingTypesSet = existingTypes.toSet
+ lazy val wantedTypesSet = wantedTypes.toSet
+ lazy val onlyInWanted = wantedTypesSet &~ existingTypesSet
+
+ val toBeRemoved = existingTypesSet &~ wantedTypesSet
+ val toBeAdded = onlyInWanted
+
+ (toBeAdded, toBeRemoved)
+ }
+
+ private case class EntityPutResult(created: Seq[EntityRow],
+ updated: Seq[UUID],
+ updatedRows: Seq[EntityRow],
+ typeInserts: Seq[EntityTypeRow],
+ typeDeletes: Seq[EntityTypeRow]) {
+
+ def merge(rhs: EntityPutResult): EntityPutResult = {
+ EntityPutResult(
+ created ++ rhs.created,
+ updated ++ rhs.updated,
+ updatedRows ++ rhs.updatedRows,
+ typeInserts ++ rhs.typeInserts,
+ typeDeletes ++ rhs.typeDeletes)
+ }
+ }
+
+ private def computePutEntities(
+ withIds: Seq[(UUID, String, Set[String])],
+ withNames: Seq[(String, Set[String])],
+ enforceReserved: Boolean,
+ allowCreate: Boolean = true,
+ updateFilter: Option[EntityFilter] = None): EntityPutResult = {
+
+ val specifiedIds = withIds.map(_._1)
+ val specifiedNames = withNames.map(_._1)
+
+ val existingQuery = entities.where(ent => (ent.id in specifiedIds) or (ent.name in specifiedNames))
+
+ val existing: Seq[(EntityRow, Seq[String])] = entitiesWithTypes(existingQuery)
+
+ if (enforceReserved) {
+ val foundReservedExisting = existing.flatMap(_._2).filter(reservedTypes.contains)
+ if (foundReservedExisting.nonEmpty) {
+ throw new ModelInputException("Cannot modify entities with reserved types: " + foundReservedExisting.mkString("(", ", ", ")") + ", use specific service")
+ }
+
+ val foundReservedUpdates = (withIds.flatMap(_._3) ++ withNames.flatMap(_._2)).filter(reservedTypes.contains)
+ if (foundReservedUpdates.nonEmpty) {
+ throw new ModelInputException("Cannot add reserved types: " + foundReservedUpdates.mkString("(", ", ", ")") + ", use specific service")
+ }
+ }
+
+ val existingIdSet = existing.map(_._1.id).toSet
+ val existingNameSet = existing.map(_._1.name).toSet
+
+ val notExistingWithIds = withIds.filterNot(entry => existingIdSet.contains(entry._1))
+ val notExistingWithNames = withNames.filterNot(entry => existingNameSet.contains(entry._1))
+
+ if ((notExistingWithIds.nonEmpty || notExistingWithNames.nonEmpty) && !allowCreate) {
+ throw new ModelPermissionException("Must have blanket create permissions to create entities")
+ }
+
+ val withIdsById: Map[UUID, (UUID, String, Set[String])] = withIds.map(tup => (tup._1, tup)).toMap
+ val withNamesByName: Map[String, (String, Set[String])] = withNames.map(tup => (tup._1, tup)).toMap
+
+ val updates: Seq[(UUID, Option[EntityRow], Set[String], Set[String])] = existing.flatMap {
+ case (currentRow, currentTypes) =>
+ val id = currentRow.id
+ val currentName = currentRow.name
+
+ val update: Option[(String, Set[String])] = withIdsById.get(id).map(tup => (tup._2, tup._3)).orElse(withNamesByName.get(currentName))
+
+ update.flatMap {
+ case (updateName, updateTypes) =>
+
+ val rowUpdate = if (currentName != updateName) Some(currentRow.copy(name = updateName)) else None
+
+ val (adds, deletes) = typeDiff(currentTypes.toSet, updateTypes)
+
+ if (rowUpdate.nonEmpty || adds.nonEmpty || deletes.nonEmpty) {
+ Some((id, rowUpdate, adds, deletes))
+ } else {
+ None
+ }
+ }
+ }
+
+ val idRowAndTypeInserts: Seq[(EntityRow, Seq[EntityTypeRow])] = notExistingWithIds.map {
+ case (id, name, typeSet) => (EntityRow(id, name), typeSet.toSeq.map(EntityTypeRow(id, _)))
+ }
+
+ val nameRowAndTypeInserts: Seq[(EntityRow, Seq[EntityTypeRow])] = notExistingWithNames.map {
+ case (name, typeSet) =>
+ val uuid = UUID.randomUUID()
+ val typeInserts = typeSet.toSeq.map(EntityTypeRow(uuid, _))
+ (EntityRow(uuid, name), typeInserts)
+ }
+
+ val rowInserts = idRowAndTypeInserts.map(_._1) ++ nameRowAndTypeInserts.map(_._1)
+
+ val updatedIds = updates.map(_._1)
+
+ val rowUpdates = updates.flatMap(_._2)
+
+ val typeInserts = updates.flatMap(tup => tup._3.toSeq.map(EntityTypeRow(tup._1, _))) ++ idRowAndTypeInserts.flatMap(_._2) ++ nameRowAndTypeInserts.flatMap(_._2)
+
+ val typeDeletes = updates.flatMap(tup => tup._4.map(EntityTypeRow(tup._1, _)))
+
+ EntityPutResult(rowInserts, updatedIds, rowUpdates, typeInserts, typeDeletes)
+ }
+
+ private def performPutEntities(notifier: ModelNotifier, result: EntityPutResult): Seq[Entity] = {
+ if (result.typeDeletes.nonEmpty) {
+ result.typeDeletes.foreach { delRow =>
+ entityTypes.deleteWhere(t => t.entityId === delRow.entityId and t.entType === delRow.entType)
+ }
+ }
+
+ if (result.created.nonEmpty) {
+ entities.insert(result.created)
+ }
+ if (result.updatedRows.nonEmpty) {
+ entities.update(result.updatedRows)
+ }
+ if (result.typeInserts.nonEmpty) {
+ entityTypes.insert(result.typeInserts)
+ }
+
+ val createdIds = result.created.map(_.id)
+ val updatedIds = result.updated
+
+ val allIds = createdIds ++ updatedIds
+
+ val results = if (allIds.nonEmpty) {
+ keyQuery(createdIds ++ updatedIds, Nil)
+ } else {
+ Nil
+ }
+
+ val resultMap: Map[UUID, Entity] = results.map(r => (protoUUIDToUuid(r.getUuid), r)).toMap
+
+ createdIds.flatMap(resultMap.get).foreach(notifier.notify(Created, _))
+ updatedIds.flatMap(resultMap.get).foreach(notifier.notify(Updated, _))
+
+ results
+ }
+
+ def findOrCreateEntities(notifier: ModelNotifier,
+ withIds: Seq[(UUID, String, Set[String])],
+ withNames: Seq[(String, Set[String])],
+ allowCreate: Boolean = true,
+ updateFilter: Option[EntityFilter] = None): (Seq[UUID], Seq[UUID], Seq[Entity]) = {
+
+ val computeResult = computePutEntities(withIds, withNames, false, allowCreate, updateFilter)
+
+ val entities = performPutEntities(notifier, computeResult)
+
+ (computeResult.created.map(_.id), computeResult.updated, entities)
+ }
+
+ def putEntities(notifier: ModelNotifier,
+ withIds: Seq[(UUID, String, Set[String])],
+ withNames: Seq[(String, Set[String])],
+ allowCreate: Boolean = true,
+ updateFilter: Option[EntityFilter] = None): Seq[Entity] = {
+
+ val specifiedIds = withIds.map(_._1)
+ val specifiedNames = withNames.map(_._1)
+
+ // Reject them even if they're NOT changed, otherwise it's an information leak
+ // about the current name/types (rather than just existence, which is unavoidable)
+ updateFilter.foreach { filter =>
+
+ val existingQuery = from(entities)(ent =>
+ where((ent.id in specifiedIds).inhibitWhen(specifiedIds.isEmpty) or
+ (ent.name in specifiedNames).inhibitWhen(specifiedNames.isEmpty))
+ select (ent))
+
+ if (filter.anyOutsideSet(existingQuery)) {
+ throw new ModelPermissionException("Tried to update restricted entities")
+ }
+ }
+
+ if (withIds.isEmpty && withNames.isEmpty) {
+ throw new ModelInputException("Must specify at least one entity to query for")
+ }
+
+ val result = computePutEntities(withIds, withNames, true, allowCreate, updateFilter)
+
+ performPutEntities(notifier, result)
+
+ keyQuery(specifiedIds, specifiedNames)
+ }
+
+ def deleteEntities(notifier: ModelNotifier, ids: Seq[UUID], filter: Option[EntityFilter] = None): Seq[Entity] = {
+ deleteByIds(notifier, ids, true, filter)
+ }
+
+ def deleteEntityBased(notifier: ModelNotifier, ids: Seq[UUID], filter: Option[EntityFilter] = None): Seq[Entity] = {
+ deleteByIds(notifier, ids, false, filter)
+ }
+
+ private def deleteByIds(notifier: ModelNotifier, ids: Seq[UUID], enforceReserved: Boolean, filter: Option[EntityFilter] = None): Seq[Entity] = {
+ filter.foreach { filt =>
+ if (filt.anyOutsideSet(ids)) {
+ throw new ModelPermissionException("Tried to delete restricted entities")
+ }
+ }
+
+ val results = idQuery(ids)
+
+ if (enforceReserved) {
+ val foundReserved = results.flatMap(_.getTypesList.toSeq).filter(reservedTypes.contains)
+ if (foundReserved.nonEmpty) {
+ throw new ModelInputException("Cannot delete entities with reserved types: " + foundReserved.mkString("(", ", ", ")") + ", use specific service")
+ }
+ }
+
+ entities.deleteWhere(t => t.id in ids)
+ entityTypes.deleteWhere(t => t.entityId in ids)
+
+ ids.foreach(deleteAllEdgesFromNode(notifier, _))
+
+ deleteAllKeyValues2(notifier, ids)
+
+ results.foreach(notifier.notify(Deleted, _))
+
+ results
+ }
+
+ private def checkForDiamonds(parent: UUID, children: Seq[UUID], relation: String) {
+ val childrenOfChildren = from(edges)(e =>
+ where((e.parentId in children) and e.relationship === relation)
+ select (e.childId))
+
+ val ourParents = from(edges)(e =>
+ where(e.childId === parent and e.relationship === relation)
+ select (e.parentId))
+
+ val existingPathToChildren = from(edges)(e =>
+ where((e.parentId === parent or (e.parentId in ourParents)) and
+ ((e.childId in children) or (e.childId in childrenOfChildren)) and
+ e.relationship === relation)
+ select ((e.parentId, e.childId, e.relationship))).page(0, 3).toSeq
+
+ if (existingPathToChildren.nonEmpty) {
+ val summary = existingPathToChildren.map {
+ case (parent, child, relation) => s"$parent -($relation)-> $child"
+ }.mkString("", ", ", " ...")
+ throw new DiamondException("Edge insertion would create diamond: " + summary)
+ }
+ }
+
+ private def edgeRowToProto(edgeRow: EntityEdgeRow): EntityEdge = {
+ EntityEdge.newBuilder
+ .setId(ModelID.newBuilder.setValue(edgeRow.id.toString))
+ .setParent(edgeRow.parentId)
+ .setChild(edgeRow.childId)
+ .setRelationship(edgeRow.relationship)
+ .setDistance(edgeRow.distance)
+ .build
+ }
+
+ def deleteAllEdgesFromNode(notifier: ModelNotifier, node: UUID): Seq[EntityEdge] = {
+
+ val relationships =
+ from(edges)(e =>
+ where(e.parentId === node or e.childId === node)
+ select (e.relationship)).distinct.toList
+
+ val relationEdgeSets: Seq[Query[EntityEdgeRow]] = relationships.map { relation =>
+ // Node and its parents
+ val upperSet =
+ from(edges)(e =>
+ where(e.childId === node and
+ e.relationship === relation)
+ select (e.parentId))
+
+ // Node and its children
+ val lowerSet =
+ from(edges)(e =>
+ where(e.parentId === node and
+ e.relationship === relation)
+ select (e.childId))
+
+ // All paths that go through node (simple paths between all nodes)
+ from(edges)(e =>
+ where(((e.parentId in upperSet) or e.parentId === node) and
+ ((e.childId in lowerSet) or (e.childId === node)) and
+ e.relationship === relation)
+ select (e))
+ }
+
+ val results = relationEdgeSets.flatMap(_.toList map edgeRowToProto)
+
+ relationEdgeSets.foreach(edges.delete)
+
+ results.foreach(notifier.notify(Deleted, _))
+
+ results
+ }
+
+ def deleteEdgesFromParent(notifier: ModelNotifier, parent: UUID, children: Seq[UUID], relation: String): Seq[EntityEdge] = {
+
+ // Parent and parents of parent
+ val upper = from(edges)(e =>
+ where((e.childId === parent or
+ (e.parentId === parent and (e.childId in children))) and
+ e.relationship === relation)
+ select (e.parentId))
+
+ // Children and children of children
+ val lower = from(edges)(e =>
+ where((e.parentId in children or
+ (e.parentId === parent and (e.childId in children))) and
+ e.relationship === relation)
+ select (e.childId))
+
+ // Because we have simple paths between all nodes, deleted edges completely separate
+ // the component the parent is attached to from the child components
+ val allEdges = from(edges)(e =>
+ where(e.relationship === relation and
+ (e.childId in lower) and
+ (e.parentId in upper))
+ select (e))
+
+ val results = allEdges.toSeq map edgeRowToProto
+
+ edges.delete(allEdges)
+
+ results.foreach(notifier.notify(Deleted, _))
+
+ results
+ }
+
+ def deleteEdges(notifier: ModelNotifier, edgeList: Seq[(UUID, UUID)], relation: String): Seq[EntityEdge] = {
+
+ val parentGrouped: Map[UUID, Seq[UUID]] = edgeList.groupBy(_._1).mapValues(_.map(_._2))
+
+ parentGrouped.toSeq.flatMap {
+ case (parent, children) =>
+ deleteEdgesFromParent(notifier, parent, children, relation)
+ }
+ }
+
+ def existingEdges(edgeList: Seq[(UUID, UUID)], relation: String): Seq[EntityEdgeRow] = {
+
+ val parentGrouped: Map[UUID, Seq[UUID]] = edgeList.groupBy(_._1).mapValues(_.map(_._2))
+
+ parentGrouped.flatMap { case (parent, children) => existingEdges(parent, children, relation).toSeq }.toSeq
+ }
+
+ def existingEdges(parent: UUID, children: Seq[UUID], relation: String): Seq[EntityEdgeRow] = {
+ from(edges)(e =>
+ where(e.parentId === parent and
+ (e.childId in children) and
+ e.relationship === relation and
+ e.distance === 1)
+ select (e)).toSeq
+ }
+
+ def calculateEdgeAdditions(parent: UUID, child: UUID, parentsOfParent: Seq[(UUID, Int)], childrenOfChild: Seq[(UUID, Int)]): Seq[(UUID, UUID, Int)] = {
+
+ val upper: Seq[(UUID, Int)] = parentsOfParent :+ (parent, 0)
+ val lower: Seq[(UUID, Int)] = childrenOfChild :+ (child, 0)
+
+ upper.flatMap {
+ case (parentId, height) =>
+ lower.map {
+ case (childId, depth) =>
+ (parentId, childId, height + depth + 1)
+ }
+ }
+ }
+
+ def groupEdgeList(list: Seq[(UUID, UUID, Int)]): (Map[UUID, Seq[(UUID, Int)]], Map[UUID, Seq[(UUID, Int)]]) = {
+ val childToParents = list.groupBy(_._2).mapValues(_.map(tup => (tup._1, tup._3)))
+ val parentToChildren = list.groupBy(_._1).mapValues(_.map(tup => (tup._2, tup._3)))
+
+ (childToParents, parentToChildren)
+ }
+
+ private def mergeMaps[A, B](a: Map[A, Seq[B]], b: Map[A, Seq[B]]): Map[A, Seq[B]] = {
+ (a.keySet ++ b.keySet).map { key =>
+ (key, a.get(key).getOrElse(Nil) ++ b.get(key).getOrElse(Nil))
+ }.toMap
+ }
+
+ @tailrec
+ private def findEdgeAdditions(
+ results: List[(UUID, UUID, Int)],
+ toAdd: List[(UUID, UUID)],
+ childToParents: Map[UUID, Seq[(UUID, Int)]],
+ parentsToChildren: Map[UUID, Seq[(UUID, Int)]]): List[(UUID, UUID, Int)] = {
+
+ toAdd match {
+ case Nil => results
+ case (parent, child) :: tail =>
+
+ val parentSet = childToParents.get(parent).getOrElse(Nil)
+ val childSet = parentsToChildren.get(child).getOrElse(Nil)
+
+ val added = calculateEdgeAdditions(parent, child, parentSet, childSet)
+
+ val (childMapAdditions, parentMapAdditions) = groupEdgeList(added)
+
+ findEdgeAdditions(results ++ added, tail, mergeMaps(childToParents, childMapAdditions), mergeMaps(parentsToChildren, parentMapAdditions))
+ }
+ }
+
+ def putEdges(notifier: ModelNotifier, edgeList: Seq[(UUID, UUID)], relation: String): Seq[EntityEdge] = {
+
+ if (edgeList.exists(tup => tup._1 == tup._2)) {
+ throw new ModelInputException("Edges must have different parent and child UUIDs")
+ }
+
+ val edgeListDistinct = edgeList.distinct
+
+ val existing = existingEdges(edgeListDistinct, relation)
+ val existingSet: Set[(UUID, UUID)] = existing.map(r => (r.parentId, r.childId)).toSet
+ val nonExistent = edgeListDistinct.filterNot(existingSet.contains)
+
+ val (parents, children) = nonExistent.unzip
+
+ val parentsAndTheirParents: Seq[(UUID, UUID, Int)] =
+ from(edges)(e =>
+ where((e.childId in parents) and
+ e.relationship === relation)
+ select (e.childId, e.parentId, e.distance)).toVector
+
+ val parentsOfParents: Map[UUID, Seq[(UUID, Int)]] = parentsAndTheirParents.groupBy(_._1).mapValues(_.map(tup => (tup._2, tup._3)))
+
+ val childrenAndTheirChildren: Seq[(UUID, UUID, Int)] =
+ from(edges)(e =>
+ where((e.parentId in children) and
+ e.relationship === relation)
+ select (e.parentId, e.childId, e.distance)).toVector
+
+ val childrenOfChildren: Map[UUID, Seq[(UUID, Int)]] = childrenAndTheirChildren.groupBy(_._1).mapValues(_.map(tup => (tup._2, tup._3)))
+
+ val additions = findEdgeAdditions(Nil, nonExistent.toList, parentsOfParents, childrenOfChildren)
+
+ val rowAdditions = additions.map {
+ case (parent, child, dist) => EntityEdgeRow(0, parent, child, relation, dist)
+ }
+
+ val insertedRows = try {
+ //edges.insert(rowAdditions)
+ rowAdditions.map(edges.insert)
+ } catch {
+ case ex: RuntimeException =>
+ // TODO: HACK!! this might have been a disconnect, but we're going to assume it was a 'unique_violation' in postgres (23505), otherwise user gets unhelpful internal service error
+ throw new DiamondException("Edge insertion would create diamond")
+ }
+
+ val insertedProtos = insertedRows map edgeRowToProto
+ val existingProtos = existing map edgeRowToProto
+
+ insertedProtos.foreach(notifier.notify(Created, _))
+
+ insertedProtos ++ existingProtos
+ }
+
+ def getKeyValues(ids: Seq[UUID], key: String, filter: Option[EntityFilter] = None): Seq[(UUID, Array[Byte])] = {
+
+ from(entities, entityKeyValues)((ent, kv) =>
+ where(EntityFilter.optional(filter, ent).inhibitWhen(filter.isEmpty) and
+ (ent.id in ids) and
+ ent.id === kv.uuid and
+ kv.key === key)
+ select (ent.id, kv.data)).toSeq
+ }
+
+ def getAllKeyValues(ids: Seq[UUID], filter: Option[EntityFilter] = None): Seq[(UUID, String, Array[Byte])] = {
+
+ from(entities, entityKeyValues)((ent, kv) =>
+ where(EntityFilter.optional(filter, ent).inhibitWhen(filter.isEmpty) and
+ (ent.id in ids) and
+ ent.id === kv.uuid)
+ select (ent.id, kv.key, kv.data)).toSeq
+ }
+
+ def putKeyValues(values: Seq[(UUID, Array[Byte])], key: String, filter: Option[EntityFilter] = None): (Seq[(UUID, Array[Byte])], Seq[UUID], Seq[UUID]) = {
+
+ val ids = values.map(_._1)
+ filter.foreach { filt =>
+ if (filt.anyOutsideSet(ids)) {
+ throw new ModelPermissionException("Tried to update restricted entities")
+ }
+ }
+
+ val current: Map[UUID, EntityKeyStoreRow] =
+ from(entities, entityKeyValues)((ent, kv) =>
+ where((ent.id in ids) and
+ kv.key === key and
+ ent.id === kv.uuid)
+ select (ent.id, kv)).toList.toMap
+
+ val isCreatedAndRow: Seq[(Boolean, (UUID, EntityKeyStoreRow))] = values.flatMap {
+ case (id, bytes) =>
+ current.get(id) match {
+ case None =>
+ Some(true, (id, EntityKeyStoreRow(0, id, key, bytes)))
+ case Some(ks) =>
+ if (!java.util.Arrays.equals(bytes, ks.data)) {
+ Some(false, (id, ks.copy(data = bytes)))
+ } else {
+ None
+ }
+ }
+ }
+
+ val creates: Seq[(UUID, EntityKeyStoreRow)] = isCreatedAndRow.filter(_._1).map(_._2)
+ val updates: Seq[(UUID, EntityKeyStoreRow)] = isCreatedAndRow.filter(!_._1).map(_._2)
+
+ val createRows = creates.map(_._2)
+ val updateRows = updates.map(_._2)
+
+ entityKeyValues.insert(createRows)
+ entityKeyValues.update(updateRows)
+
+ val results = getKeyValues(ids, key)
+
+ (results, creates.map(_._1), updates.map(_._1))
+ }
+
+ def deleteKeyValues(ids: Seq[UUID], key: String, filter: Option[EntityFilter] = None): Seq[(UUID, Array[Byte])] = {
+ filter.foreach { filt =>
+ if (filt.anyOutsideSet(ids)) {
+ throw new ModelPermissionException("Tried to delete restricted entity key values")
+ }
+ }
+
+ val results = getKeyValues(ids, key)
+
+ entityKeyValues.deleteWhere(t => (t.uuid in ids) and t.key === key)
+
+ results
+ }
+
+ def deleteAllKeyValues(ids: Seq[UUID], filter: Option[EntityFilter] = None): Seq[(UUID, String, Array[Byte])] = {
+ filter.foreach { filt =>
+ if (filt.anyOutsideSet(ids)) {
+ throw new ModelPermissionException("Tried to delete restricted entity key values")
+ }
+ }
+
+ val results = getAllKeyValues(ids)
+
+ entityKeyValues.deleteWhere(t => t.uuid in ids)
+
+ results
+ }
+
+ def getCurrentKeyValues(set: Seq[(UUID, String)], filter: Option[EntityFilter] = None): Seq[EntityKeyStoreRow] = {
+ from(entityKeyValues)(kv =>
+ where(
+ EntityFilter.optional(filter, kv.uuid).inhibitWhen(filter.isEmpty) and
+ set.map(tup => tup._1 === kv.uuid and tup._2 === kv.key).reduce(ModelHelpers.logicalOr))
+ select (kv)).toVector
+ }
+
+ def getCurrentKeyValuesByUuids(ids: Seq[UUID], filter: Option[EntityFilter] = None): Seq[EntityKeyStoreRow] = {
+ from(entityKeyValues)(kv =>
+ where(
+ EntityFilter.optional(filter, kv.uuid).inhibitWhen(filter.isEmpty) and
+ (kv.uuid in ids))
+ select (kv)).toVector
+ }
+
+ def kvToProto(row: EntityKeyStoreRow): EntityKeyValue = {
+ EntityKeyValue.newBuilder()
+ .setUuid(uuidToProtoUUID(row.uuid))
+ .setKey(row.key)
+ .setValue(StoredValue.parseFrom(row.data))
+ .build()
+ }
+
+ def getKeys(ids: Seq[UUID], filter: Option[EntityFilter] = None): Seq[EntityKeyPair] = {
+ val uuidAndKeys = from(entityKeyValues)(kv =>
+ where(
+ EntityFilter.optional(filter, kv.uuid).inhibitWhen(filter.isEmpty) and
+ (kv.uuid in ids))
+ select (kv.uuid, kv.key))
+ .toVector
+ .filterNot(_._2 == ProcessingModel.overrideKey)
+
+ uuidAndKeys.map {
+ case (uuid, key) =>
+ EntityKeyPair.newBuilder()
+ .setUuid(uuid)
+ .setKey(key)
+ .build
+ }
+ }
+
+ def getKeyValues2(set: Seq[(UUID, String)], filter: Option[EntityFilter] = None): Seq[EntityKeyValue] = {
+ getCurrentKeyValues(set, filter)
+ .filterNot(_.key == ProcessingModel.overrideKey)
+ .map(kvToProto)
+ }
+
+ def getKeyValuesForUuids(set: Seq[UUID], filter: Option[EntityFilter] = None): Seq[EntityKeyValue] = {
+ getCurrentKeyValuesByUuids(set, filter)
+ .filterNot(_.key == ProcessingModel.overrideKey)
+ .map(kvToProto)
+ }
+
+ def putKeyValues2(notifier: ModelNotifier, set: Seq[(UUID, String, StoredValue)], allowCreate: Boolean = true, updateFilter: Option[EntityFilter] = None): Seq[EntityKeyValue] = {
+ val ids = set.map(_._1)
+ updateFilter.foreach { filt =>
+ if (filt.anyOutsideSet(ids)) {
+ throw new ModelPermissionException("Tried to update restricted entities")
+ }
+ }
+
+ if (set.map(_._2).contains(ProcessingModel.overrideKey)) {
+ throw new ModelInputException(s"Cannot put reserved key ${ProcessingModel.overrideKey}")
+ }
+
+ val uuidsAndKeys = set.map { case (id, key, _) => (id, key) }
+
+ val current = getCurrentKeyValues(uuidsAndKeys)
+
+ val currentMap = current.map(r => ((r.uuid, r.key), r)).toMap
+
+ val (creates, updates, same) = {
+ val crd = new VectorBuilder[EntityKeyStoreRow]
+ val upd = new VectorBuilder[EntityKeyStoreRow]
+ val unch = new VectorBuilder[EntityKeyStoreRow]
+
+ set.foreach {
+ case (uuid, key, protoValue) =>
+ currentMap.get((uuid, key)) match {
+ case None => crd += EntityKeyStoreRow(0, uuid, key, protoValue.toByteArray)
+ case Some(existing) =>
+ val bytes = protoValue.toByteArray
+ if (!java.util.Arrays.equals(bytes, existing.data)) {
+ upd += existing.copy(data = bytes)
+ } else {
+ unch += existing
+ }
+ }
+ }
+
+ (crd.result(), upd.result(), unch.result())
+ }
+
+ val endpointMap = keyEndpointMap(ids)
+
+ val createProtosAndEndpoint = creates.map(row => (kvToProto(row), endpointMap.get(row.uuid).map(uuidToProtoUUID)))
+ val updateProtosAndEndpoint = updates.map(row => (kvToProto(row), endpointMap.get(row.uuid).map(uuidToProtoUUID)))
+
+ entityKeyValues.insert(creates)
+ createProtosAndEndpoint.map(t => EntityKeyValueWithEndpoint(t._1, t._2)).foreach(notifier.notify(Created, _))
+
+ entityKeyValues.update(updates)
+ updateProtosAndEndpoint.map(t => EntityKeyValueWithEndpoint(t._1, t._2)).foreach(notifier.notify(Updated, _))
+
+ createProtosAndEndpoint.map(_._1) ++ updateProtosAndEndpoint.map(_._1) ++ same.map(kvToProto)
+ }
+
+ def deleteKeyValues2(notifier: ModelNotifier, set: Seq[(UUID, String)], filter: Option[EntityFilter] = None): Seq[EntityKeyValue] = {
+
+ val ids = set.map(_._1)
+ filter.foreach { filt =>
+ if (filt.anyOutsideSet(ids)) {
+ throw new ModelPermissionException("Tried to delete restricted entity key value pairs")
+ }
+ }
+ if (set.map(_._2).contains(ProcessingModel.overrideKey)) {
+ throw new ModelInputException(s"Cannot delete reserved key ${ProcessingModel.overrideKey}")
+ }
+
+ val originalRows = getCurrentKeyValues(set, filter)
+ val endpointMap = keyEndpointMap(ids)
+
+ val origProtosAndEndpoint = originalRows.map(row => (kvToProto(row), endpointMap.get(row.uuid).map(uuidToProtoUUID)))
+
+ entityKeyValues.deleteWhere(kv => set.map(tup => tup._1 === kv.uuid and tup._2 === kv.key).reduce(ModelHelpers.logicalOr))
+
+ origProtosAndEndpoint.map(t => EntityKeyValueWithEndpoint(t._1, t._2)).foreach(notifier.notify(Deleted, _))
+
+ origProtosAndEndpoint.map(_._1)
+ }
+
+ def deleteAllKeyValues2(notifier: ModelNotifier, ids: Seq[UUID], filter: Option[EntityFilter] = None): Seq[EntityKeyValue] = {
+ filter.foreach { filt =>
+ if (filt.anyOutsideSet(ids)) {
+ throw new ModelPermissionException("Tried to delete restricted entity key values")
+ }
+ }
+
+ val results = getKeyValuesForUuids(ids).filterNot(_.getKey == ProcessingModel.overrideKey)
+ val endpointMap = keyEndpointMap(ids).map { case (u1, u2) => (uuidToProtoUUID(u1), uuidToProtoUUID(u2)) }
+
+ entityKeyValues.deleteWhere(t => t.uuid in ids)
+
+ val resultsWithEndpoint = results.map(r => EntityKeyValueWithEndpoint(r, endpointMap.get(r.getUuid)))
+
+ resultsWithEndpoint.foreach(notifier.notify(Deleted, _))
+
+ results
+ }
+
+ def keyEndpointMap(ids: Seq[UUID]): Map[UUID, UUID] = {
+ from(edges)(e =>
+ where(e.relationship === "source" and
+ (e.childId in ids))
+ select (e.childId, e.parentId)).toVector.toMap
+ }
+
+}
diff --git a/services/src/main/scala/io/greenbus/services/model/EventAlarmModel.scala b/services/src/main/scala/io/greenbus/services/model/EventAlarmModel.scala
new file mode 100644
index 0000000..42293a9
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/model/EventAlarmModel.scala
@@ -0,0 +1,529 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.model
+
+import org.squeryl.PrimitiveTypeMode._
+import io.greenbus.services.data.ServicesSchema._
+import io.greenbus.services.data._
+import io.greenbus.services.framework._
+import io.greenbus.client.service.proto.Events._
+import io.greenbus.client.service.proto.Model.{ ModelID, ModelUUID }
+import UUIDHelpers._
+import io.greenbus.services.core.event.MessageFormatter
+import java.util.UUID
+import org.squeryl.Query
+import io.greenbus.services.authz.EntityFilter
+
+object EventAlarmModel {
+
+ val defaultEventQueryPageSize = 200
+
+ val unconfiguredEventSeverity = 9
+ val unconfiguredEventMessage = "Event type not configured"
+
+ def mapToAttributesList(map: Seq[(String, String)]): Seq[Attribute] = {
+ map.map {
+ case (k, v) =>
+ Attribute.newBuilder()
+ .setName(k)
+ .setValueString(v)
+ .build()
+ }
+ }
+
+ case class SysEventTemplate(user: String, eventType: String, subsystem: Option[String], deviceTime: Option[Long], entityUuid: Option[ModelUUID], modelGroup: Option[String], args: Seq[Attribute])
+
+ case class EventConfigTemp(eventType: String, severity: Int, designation: EventConfig.Designation, alarmState: Alarm.State, resource: String)
+
+ case class EventQueryParams(
+ eventTypes: Seq[String] = Nil,
+ timeFrom: Option[Long] = None,
+ timeTo: Option[Long] = None,
+ severities: Seq[Int] = Nil,
+ severityOrHigher: Option[Int] = None,
+ subsystems: Seq[String] = Nil,
+ agents: Seq[String] = Nil,
+ entityUuids: Seq[UUID] = Nil,
+ entityNames: Seq[String] = Nil,
+ modelGroups: Seq[String] = Nil,
+ isAlarm: Option[Boolean] = None,
+ latest: Boolean = true,
+ last: Option[Long] = None,
+ pageSize: Int = defaultEventQueryPageSize)
+}
+
+trait EventAlarmModel {
+ import EventAlarmModel._
+
+ def eventConfigQuery(severities: Seq[Int], severityOrHigher: Option[Int], designations: Seq[EventConfig.Designation], alarmStates: Seq[Alarm.State], lastEventType: Option[String], pageSize: Int): Seq[EventConfig]
+ def getEventConfigs(eventTypes: Seq[String]): Seq[EventConfig]
+ def putEventConfigs(notifier: ModelNotifier, configs: Seq[EventConfigTemp]): Seq[EventConfig]
+ def deleteEventConfigs(notifier: ModelNotifier, eventTypes: Seq[String]): Seq[EventConfig]
+
+ def getEvents(eventIds: Seq[Long], filter: Option[EntityFilter] = None): Seq[Event]
+ def postEvents(notifier: ModelNotifier, templates: Seq[SysEventTemplate], filter: Option[EntityFilter] = None): Seq[Event]
+ def eventQuery(params: EventQueryParams, filter: Option[EntityFilter] = None): Seq[Event]
+
+ def alarmQuery(alarmStates: Seq[Alarm.State], params: EventQueryParams, filter: Option[EntityFilter] = None): Seq[Alarm]
+
+ def putAlarmStates(notifier: ModelNotifier, updates: Seq[(Long, Alarm.State)], filter: Option[EntityFilter] = None): Seq[Alarm]
+}
+
+object SquerylEventAlarmModel extends EventAlarmModel {
+ import EventAlarmModel._
+
+ def eventConfigToProto(row: EventConfigRow): EventConfig = {
+ EventConfig.newBuilder()
+ .setEventType(row.eventType)
+ .setSeverity(row.severity)
+ .setDesignation(EventConfig.Designation.valueOf(row.designation))
+ .setAlarmState(Alarm.State.valueOf(row.alarmState))
+ .setResource(row.resource)
+ .setBuiltIn(row.builtIn)
+ .build()
+ }
+
+ def eventConfigQuery(severities: Seq[Int], severityOrHigher: Option[Int], designations: Seq[EventConfig.Designation], alarmStates: Seq[Alarm.State], lastEventType: Option[String], pageSize: Int): Seq[EventConfig] = {
+
+ val desigs = designations.map(_.getNumber)
+ val alarmStateNums = alarmStates.map(_.getNumber)
+
+ val results =
+ from(eventConfigs)(ec =>
+ where((ec.severity in severities).inhibitWhen(severities.isEmpty) and
+ (ec.severity lte severityOrHigher.?) and
+ (ec.designation in desigs).inhibitWhen(designations.isEmpty) and
+ (ec.alarmState in alarmStateNums).inhibitWhen(alarmStates.isEmpty) and
+ (ec.eventType gt lastEventType.?))
+ select (ec)
+ orderBy (ec.eventType))
+ .page(0, pageSize)
+
+ results.toSeq map eventConfigToProto
+ }
+
+ def getEventConfigs(eventTypes: Seq[String]): Seq[EventConfig] = {
+ val results = eventConfigs.where(ec => ec.eventType in eventTypes).toSeq
+
+ results map eventConfigToProto
+ }
+
+ def putEventConfigs(notifier: ModelNotifier, configs: Seq[EventConfigTemp]): Seq[EventConfig] = {
+
+ val types = configs.map(_.eventType)
+ val existing = eventConfigs.where(ec => ec.eventType in types).toSeq
+
+ val existNameSet = existing.map(_.eventType).toSet
+
+ val (toBeUpdated, toBeCreated) = configs.partition(existNameSet contains _.eventType)
+
+ val updateMap = toBeUpdated.map(u => (u.eventType, u)).toMap
+
+ val creates = toBeCreated.map(c => new EventConfigRow(0, c.eventType, c.severity, c.designation.getNumber, c.alarmState.getNumber, c.resource, false))
+
+ val updates = existing.flatMap { row =>
+ updateMap.get(row.eventType).map { up =>
+ row.copy(
+ eventType = up.eventType,
+ severity = up.severity,
+ designation = up.designation.getNumber,
+ alarmState = up.alarmState.getNumber,
+ resource = up.resource)
+ }
+ }
+
+ eventConfigs.insert(creates)
+ eventConfigs.update(updates)
+
+ val results = getEventConfigs(configs.map(_.eventType))
+
+ val (updated, created) = results.partition(r => existNameSet contains r.getEventType)
+
+ updated.foreach(notifier.notify(Updated, _))
+ created.foreach(notifier.notify(Created, _))
+
+ results
+ }
+
+ def deleteEventConfigs(notifier: ModelNotifier, eventTypes: Seq[String]): Seq[EventConfig] = {
+ val results = getEventConfigs(eventTypes)
+
+ eventConfigs.deleteWhere(ec => ec.eventType in eventTypes)
+
+ results.foreach(notifier.notify(Deleted, _))
+
+ results
+ }
+
+ /*private def argsToArgList(args: Seq[Attribute]): AttributeList = {
+ import scala.collection.JavaConversions._
+ AttributeList.newBuilder()
+ .addAllAttribute(args)
+ .build()
+ }*/
+
+ private def makeEvent(isAlarm: Boolean, template: SysEventTemplate, config: EventConfig): EventRow = {
+
+ val now = System.currentTimeMillis()
+ val args = template.args
+ val argBytes = new Array[Byte](0) // argsToArgList(args).toByteArray
+ val rendered = MessageFormatter.format(config.getResource, args)
+
+ EventRow(
+ 0,
+ template.eventType,
+ isAlarm,
+ now,
+ template.deviceTime,
+ config.getSeverity,
+ template.subsystem.getOrElse(""),
+ template.user,
+ template.entityUuid.map(protoUUIDToUuid),
+ template.modelGroup,
+ argBytes,
+ rendered)
+ }
+
+ private def makeUnconfiguredEvent(template: SysEventTemplate): EventRow = {
+ val now = System.currentTimeMillis
+ val args = template.args
+ val argBytes = new Array[Byte](0) //argsToArgList(args).toByteArray
+
+ EventRow(0,
+ template.eventType,
+ true,
+ now,
+ template.deviceTime,
+ unconfiguredEventSeverity,
+ template.subsystem.getOrElse(""),
+ template.user,
+ template.entityUuid.map(protoUUIDToUuid),
+ None,
+ argBytes,
+ unconfiguredEventMessage)
+ }
+
+ private def eventToProto(row: EventRow): Event = eventToProto(row, false)
+ private def eventToProto(row: EventRow, skipId: Boolean = false): Event = {
+ val b = Event.newBuilder()
+ .setAgentName(row.userId)
+ .setAlarm(row.alarm)
+ .setEventType(row.eventType)
+ .setRendered(row.rendered)
+ .setSeverity(row.severity)
+ .setTime(row.time)
+ .setSubsystem(row.subsystem)
+
+ if (!skipId) b.setId(ModelID.newBuilder.setValue(row.id.toString))
+
+ row.entityId.map(uuidToProtoUUID).foreach(b.setEntityUuid)
+ row.deviceTime.foreach(b.setDeviceTime)
+ row.modelGroup.foreach(b.setModelGroup)
+
+ b.build()
+ }
+
+ private def alarmToProto(row: AlarmRow, eventProto: Event): Alarm = {
+ Alarm.newBuilder()
+ .setId(ModelID.newBuilder.setValue(row.id.toString))
+ .setEvent(eventProto)
+ .setState(Alarm.State.valueOf(row.state))
+ .build
+ }
+
+ def getEvents(eventIds: Seq[Long], filter: Option[EntityFilter] = None): Seq[Event] = {
+ if (eventIds.nonEmpty) {
+ events.where(ev => (ev.id in eventIds) and EntityFilter.optional(filter, ev.entityId)).toSeq map eventToProto
+ } else {
+ Nil
+ }
+ }
+
+ private def eventQueryClause(ev: EventRow, params: EventQueryParams, filter: Option[EntityFilter] = None) = {
+ import params._
+
+ val hasEntitySelector = entityUuids.nonEmpty || entityNames.nonEmpty
+
+ val entitySelector: Query[EntityRow] = if (hasEntitySelector) {
+ from(entities)(ent =>
+ where((ent.id in entityUuids).inhibitWhen(entityUuids.isEmpty) or
+ (ent.name in entityNames).inhibitWhen(entityNames.isEmpty))
+ select (ent))
+ } else {
+ entities.where(t => true === true)
+ }
+
+ EntityFilter.optional(filter, ev.entityId).inhibitWhen(filter.isEmpty) and
+ (ev.eventType in eventTypes).inhibitWhen(eventTypes.isEmpty) and
+ (ev.time gte timeFrom.?) and
+ (ev.time lte timeTo.?) and
+ (ev.severity in severities).inhibitWhen(severities.isEmpty) and
+ (ev.severity lte severityOrHigher.?) and
+ (ev.subsystem in subsystems).inhibitWhen(subsystems.isEmpty) and
+ (ev.userId in agents).inhibitWhen(agents.isEmpty) and
+ (ev.modelGroup in modelGroups).inhibitWhen(modelGroups.isEmpty) and
+ (ev.alarm === isAlarm.?) and
+ (ev.entityId in from(entitySelector)(ent => select(ent.id))).inhibitWhen(!hasEntitySelector)
+ }
+
+ private def queryForEvents(params: EventQueryParams, filter: Option[EntityFilter] = None): Query[EventRow] = {
+ import params._
+
+ import org.squeryl.dsl.ast.{ OrderByArg, ExpressionNode }
+ def timeOrder(time: ExpressionNode) = {
+ if (!latest) {
+ new OrderByArg(time).asc
+ } else {
+ new OrderByArg(time).desc
+ }
+ }
+ def idOrder(id: ExpressionNode) = {
+ if (!latest) {
+ new OrderByArg(id).asc
+ } else {
+ new OrderByArg(id).desc
+ }
+ }
+
+ from(events)(ev =>
+ where(
+ eventQueryClause(ev, params, filter) and
+ (ev.id > params.last.?).inhibitWhen(params.latest) and
+ (ev.id < params.last.?).inhibitWhen(!params.latest))
+ select (ev)
+ orderBy (timeOrder(ev.time), idOrder(ev.id)))
+ }
+
+ def eventQuery(params: EventQueryParams, filter: Option[EntityFilter] = None): Seq[Event] = {
+
+ val query = queryForEvents(params, filter)
+
+ val results: Seq[EventRow] = query.page(0, params.pageSize).toSeq
+
+ val protos = results map eventToProto
+
+ if (!params.latest) protos else protos.reverse
+ }
+
+ def postEvents(notifier: ModelNotifier, templates: Seq[SysEventTemplate], filter: Option[EntityFilter] = None): Seq[Event] = {
+
+ filter.foreach { filt =>
+ if (templates.exists(_.entityUuid.isEmpty)) {
+ throw new ModelPermissionException("Tried to create events with no entities with non-blanket create permissions")
+ }
+
+ val relatedEntIds = templates.flatMap(t => t.entityUuid)
+ .distinct
+ .map(protoUUIDToUuid)
+
+ if (filt.anyOutsideSet(relatedEntIds)) {
+ throw new ModelPermissionException("Tried to create events for restricted entities")
+ }
+ }
+
+ val configTypes = templates.map(_.eventType).distinct
+
+ val configs = getEventConfigs(configTypes)
+
+ val configMap = configs.map(c => (c.getEventType, c)).toMap
+
+ val mapped: Seq[(Boolean, EventRow, Option[Alarm.State])] = templates.map { temp =>
+
+ configMap.get(temp.eventType) match {
+ case None => (true, makeUnconfiguredEvent(temp), Some(Alarm.State.UNACK_SILENT))
+ case Some(config) => config.getDesignation match {
+ case EventConfig.Designation.EVENT =>
+ (true, makeEvent(false, temp, config), None)
+ case EventConfig.Designation.ALARM =>
+ val initialState = config.getAlarmState
+ (true, makeEvent(true, temp, config), Some(initialState))
+ case EventConfig.Designation.LOG =>
+ (false, makeEvent(false, temp, config), None)
+ }
+ }
+ }
+
+ val (real, logged) = mapped.partition(m => m._1)
+
+ val insertResults: Seq[(EventRow, Option[AlarmRow])] = real.map {
+ case (_, eventRow, None) =>
+ (events.insert(eventRow), None)
+ case (_, eventRow, Some(alarmState)) =>
+ val event = events.insert(eventRow)
+ val alarm = alarms.insert(AlarmRow(0, alarmState.getNumber, event.id))
+ (event, Some(alarm))
+ }
+
+ val realResults: Seq[(Event, Option[Alarm])] = insertResults.map {
+ case (eventRow, Some(alarmRow)) =>
+ val eventProto = eventToProto(eventRow)
+ val alarmProto = alarmToProto(alarmRow, eventProto)
+ (eventProto, Some(alarmProto))
+ case (eventRow, None) =>
+ (eventToProto(eventRow), None)
+ }
+
+ val loggedRows = logged.map(_._2)
+
+ val logResults: Seq[Event] = loggedRows.map(eventToProto(_, skipId = true))
+
+ realResults.foreach {
+ case (eventProto, optAlarmProto) =>
+ notifier.notify(Created, eventProto)
+ optAlarmProto.foreach(notifier.notify(Created, _))
+ }
+
+ logResults.foreach(notifier.notify(Created, _))
+
+ realResults.map(_._1) ++ logResults
+ }
+
+ def alarmQuery(alarmStates: Seq[Alarm.State], params: EventQueryParams, filter: Option[EntityFilter] = None): Seq[Alarm] = {
+
+ val eventQuery = queryForEvents(params, filter)
+
+ val stateNums = alarmStates.map(_.getNumber)
+
+ val latest = params.latest
+
+ import org.squeryl.dsl.ast.{ OrderByArg, ExpressionNode }
+ def timeOrder(time: ExpressionNode) = {
+ if (!params.latest) {
+ new OrderByArg(time).asc
+ } else {
+ new OrderByArg(time).desc
+ }
+ }
+ def idOrder(id: ExpressionNode) = {
+ if (!latest) {
+ new OrderByArg(id).asc
+ } else {
+ new OrderByArg(id).desc
+ }
+ }
+
+ val results: Seq[(EventRow, AlarmRow)] =
+ join(events, alarms)((ev, al) =>
+ where(
+ eventQueryClause(ev, params, filter) and
+ (al.state in stateNums).inhibitWhen(stateNums.isEmpty) and
+ (al.id > params.last.?).inhibitWhen(params.latest) and
+ (al.id < params.last.?).inhibitWhen(!params.latest))
+ select ((ev, al))
+ orderBy (timeOrder(ev.time), idOrder(al.id))
+ on (ev.id === al.eventId))
+ .page(0, params.pageSize)
+ .toSeq
+
+ val protos = results.map {
+ case (ev, al) => alarmToProto(al, eventToProto(ev))
+ }
+
+ if (!params.latest) protos else protos.reverse
+ }
+
+ private def validAlarmTransition(prev: Alarm.State, next: Alarm.State): Boolean = {
+ import Alarm.State._
+ (prev, next) match {
+ case (UNACK_AUDIBLE, UNACK_SILENT) => true
+ case (UNACK_AUDIBLE, ACKNOWLEDGED) => true
+ case (UNACK_SILENT, ACKNOWLEDGED) => true
+ case (ACKNOWLEDGED, REMOVED) => true
+ case (p, n) if p == n => true
+ case _ => false
+ }
+ }
+
+ def alarmKeyQuery(ids: Seq[Long], filter: Option[EntityFilter] = None): Seq[Alarm] = {
+
+ val results: Seq[(EventRow, AlarmRow)] =
+ from(events, alarms)((ev, al) =>
+ where(EntityFilter.optional(filter, ev.entityId).inhibitWhen(filter.isEmpty) and
+ (al.id in ids) and
+ ev.id === al.eventId)
+ select (ev, al)).toSeq
+
+ results.map {
+ case (ev, al) => alarmToProto(al, eventToProto(ev))
+ }
+ }
+
+ def putAlarmStates(notifier: ModelNotifier, updates: Seq[(Long, Alarm.State)], filter: Option[EntityFilter] = None): Seq[Alarm] = {
+
+ val alarmIds = updates.map(_._1)
+
+ filter.foreach { filt =>
+
+ val anyWithoutEnt =
+ from(events, alarms)((ev, al) =>
+ where(al.id in alarmIds and
+ al.eventId === ev.id and
+ ev.entityId.isNull)
+ select (al.id)).page(0, 1).nonEmpty
+
+ if (anyWithoutEnt) {
+ throw new ModelPermissionException("Tried to modify alarms with no associated entities without blanked update permissions")
+ }
+
+ val relatedEntQuery =
+ from(events, alarms, entities)((ev, al, ent) =>
+ where(al.id in alarmIds and
+ al.eventId === ev.id and
+ ev.entityId === Some(ent.id))
+ select (ent))
+
+ if (filt.anyOutsideSet(relatedEntQuery)) {
+ throw new ModelPermissionException("Tried to modify alarms for restricted entities")
+ }
+ }
+
+ val currentValues: Seq[AlarmRow] = alarms.where(al => al.id in alarmIds).toSeq
+
+ val idMap = currentValues.map(a => (a.id, a)).toMap
+
+ val toBeUpdated = updates.flatMap {
+ case (id, nextState) =>
+ idMap.get(id) match {
+ case Some(current) =>
+ val currentState = Alarm.State.valueOf(current.state)
+ if (currentState != nextState) {
+ if (validAlarmTransition(currentState, nextState)) {
+ Some(current.copy(state = nextState.getNumber))
+ } else {
+ throw new ModelInputException(s"Invalid transition between alarm states $currentState -> $nextState")
+ }
+ } else {
+ None
+ }
+ case None =>
+ throw new ModelInputException(s"No alarm exists for id $id")
+ }
+ }
+
+ alarms.update(toBeUpdated)
+
+ val results = alarmKeyQuery(alarmIds)
+
+ val actuallyUpdatedIds = toBeUpdated.map(_.id).toSet
+ val actuallyUpdated = results.filter(a => actuallyUpdatedIds.contains(a.getId.getValue.toLong))
+ actuallyUpdated.foreach(up => notifier.notify(Updated, up))
+
+ results
+ }
+}
diff --git a/services/src/main/scala/io/greenbus/services/model/EventSeeding.scala b/services/src/main/scala/io/greenbus/services/model/EventSeeding.scala
new file mode 100644
index 0000000..b1b4421
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/model/EventSeeding.scala
@@ -0,0 +1,70 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.model
+
+import io.greenbus.services.data.{ AlarmRow, EventConfigRow }
+
+object EventSeeding {
+
+ import EventConfigRow.{ ALARM, EVENT }
+
+ case class EventTypeConfig(eventType: String, designation: Int, severity: Int, resource: String)
+
+ object System {
+
+ val userLogin = EventTypeConfig("System.UserLogin", EVENT, 5, "User logged in: {user}")
+ val userLoginFailure = EventTypeConfig("System.UserLoginFailure", ALARM, 1, "User login failed {reason}")
+ val userLogout = EventTypeConfig("System.UserLogout", EVENT, 5, "User logged out")
+
+ val controlExe = EventTypeConfig("System.ControlIssued", EVENT, 3, "Executed control {command}")
+ val updatedSetpoint = EventTypeConfig("System.SetpointIssued", EVENT, 3, "Updated setpoint {command} to {value}")
+ val setOverride = EventTypeConfig("System.SetOverride", EVENT, 3, "Point overridden: {point}")
+ val setNotInService = EventTypeConfig("System.SetNotInService", EVENT, 3, "Point removed from service: {point}")
+ val removeOverride = EventTypeConfig("System.RemoveOverride", EVENT, 3, "Removed override on point: {point}")
+ val removeNotInService = EventTypeConfig("System.RemoveNotInService", EVENT, 3, "Returned point to service: {point}")
+
+ val endpointDisabled = EventTypeConfig("System.EndpointDisabled", EVENT, 3, "Endpoint {name} disabled")
+ val endpointEnabled = EventTypeConfig("System.EndpointEnabled", EVENT, 3, "Endpoint {name} enabled")
+
+ val all = Seq(
+ userLogin,
+ userLoginFailure,
+ controlExe,
+ updatedSetpoint,
+ setOverride,
+ setNotInService,
+ removeOverride,
+ removeNotInService,
+ endpointDisabled,
+ endpointEnabled)
+ }
+
+ def toRow(cfg: EventTypeConfig): EventConfigRow = {
+ import cfg._
+ EventConfigRow(0, eventType, severity, designation, AlarmRow.UNACK_SILENT, resource, true)
+ }
+
+ def seed() {
+
+ val events = System.all.map(toRow)
+
+ import io.greenbus.services.data.ServicesSchema._
+ eventConfigs.insert(events)
+ }
+}
diff --git a/services/src/main/scala/io/greenbus/services/model/FrontEndModel.scala b/services/src/main/scala/io/greenbus/services/model/FrontEndModel.scala
new file mode 100644
index 0000000..9d7a9b6
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/model/FrontEndModel.scala
@@ -0,0 +1,526 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.model
+
+import java.util.UUID
+
+import io.greenbus.client.service.proto.FrontEnd._
+import io.greenbus.client.service.proto.Model._
+import io.greenbus.services.authz.EntityFilter
+import io.greenbus.services.data.ServicesSchema._
+import io.greenbus.services.data.{ CommandRow, EndpointRow, PointRow, _ }
+import io.greenbus.services.framework._
+import io.greenbus.services.model.EntityModel.TypeParams
+import io.greenbus.services.model.UUIDHelpers._
+import org.squeryl.PrimitiveTypeMode._
+import org.squeryl.Query
+import org.squeryl.dsl.ast.LogicalBoolean
+
+import scala.collection.JavaConversions._
+
+object FrontEndModel {
+ val pointType = "Point"
+ val commandType = "Command"
+
+ case class CoreTypeTemplate[A](uuidOpt: Option[UUID], name: String, types: Set[String], info: A)
+
+ case class IdentifiedEntityTemplate[A](uuid: UUID, name: String, types: Set[String], info: A)
+ case class NamedEntityTemplate[A](name: String, types: Set[String], info: A)
+
+ case class PointInfo(pointCategory: PointCategory, unit: String)
+ case class CommandInfo(displayName: String, commandCategory: CommandCategory)
+ case class EndpointInfo(protocol: String, disabled: Option[Boolean])
+ case class ConfigFileInfo(mimeType: String, file: Array[Byte])
+
+ case class FrontEndConnectionTemplate(endpointId: UUID, inputAddress: String, commandAddress: Option[String])
+
+ case class EndpointDisabledUpdate(id: UUID, disabled: Boolean)
+
+ def splitTemplates[A](templates: Seq[CoreTypeTemplate[A]]): (Seq[IdentifiedEntityTemplate[A]], Seq[NamedEntityTemplate[A]]) = {
+
+ val (hasIds, noIds) = templates.partition(_.uuidOpt.nonEmpty)
+
+ val withIds = hasIds.map(t => IdentifiedEntityTemplate(t.uuidOpt.get, t.name, t.types, t.info))
+ val withNames = noIds.map(t => NamedEntityTemplate(t.name, t.types, t.info))
+
+ (withIds, withNames)
+ }
+}
+trait FrontEndModel {
+ import io.greenbus.services.model.FrontEndModel._
+
+ def pointExists(id: UUID, filter: Option[EntityFilter] = None): Boolean
+
+ def pointKeyQuery(uuids: Seq[UUID], names: Seq[String], filter: Option[EntityFilter] = None): Seq[Point]
+ def pointQuery(pointCategories: Seq[PointCategory], units: Seq[String], typeParams: TypeParams, lastUuid: Option[UUID], lastName: Option[String], pageSize: Int, pageByName: Boolean = true, filter: Option[EntityFilter] = None): Seq[Point]
+ def putPoints(notifier: ModelNotifier, templates: Seq[CoreTypeTemplate[PointInfo]], allowCreate: Boolean = true, updateFilter: Option[EntityFilter] = None): Seq[Point]
+ def deletePoints(notifier: ModelNotifier, ids: Seq[UUID], filter: Option[EntityFilter] = None): Seq[Point]
+
+ def commandKeyQuery(uuids: Seq[UUID], names: Seq[String], filter: Option[EntityFilter] = None): Seq[Command]
+ def commandQuery(commandCategories: Seq[CommandCategory], typeParams: TypeParams, lastUuid: Option[UUID], lastName: Option[String], pageSize: Int, pageByName: Boolean = true, filter: Option[EntityFilter] = None): Seq[Command]
+ def putCommands(notifier: ModelNotifier, points: Seq[CoreTypeTemplate[CommandInfo]], allowCreate: Boolean = true, filter: Option[EntityFilter] = None): Seq[Command]
+ def deleteCommands(notifier: ModelNotifier, ids: Seq[UUID], filter: Option[EntityFilter] = None): Seq[Command]
+
+ def endpointKeyQuery(uuids: Seq[UUID], names: Seq[String], filter: Option[EntityFilter] = None): Seq[Endpoint]
+ def endpointQuery(protocols: Seq[String], disabledOpt: Option[Boolean], typeParams: TypeParams, lastUuid: Option[UUID], lastName: Option[String], pageSize: Int, pageByName: Boolean = true, filter: Option[EntityFilter] = None): Seq[Endpoint]
+ def putEndpoints(notifier: ModelNotifier, points: Seq[CoreTypeTemplate[EndpointInfo]], allowCreate: Boolean = true, filter: Option[EntityFilter] = None): Seq[Endpoint]
+ def putEndpointsDisabled(notifier: ModelNotifier, updates: Seq[EndpointDisabledUpdate], filter: Option[EntityFilter] = None): Seq[Endpoint]
+ def deleteEndpoints(notifier: ModelNotifier, ids: Seq[UUID], filter: Option[EntityFilter] = None): Seq[Endpoint]
+
+ def putFrontEndConnections(notifier: ModelNotifier, templates: Seq[FrontEndConnectionTemplate], filter: Option[EntityFilter] = None): Seq[FrontEndRegistration]
+ def addressForCommand(command: UUID): Option[(Option[String], String)]
+
+ def getFrontEndConnectionStatuses(uuids: Seq[UUID], names: Seq[String], filter: Option[EntityFilter] = None): Seq[FrontEndConnectionStatus]
+ def putFrontEndConnectionStatuses(notifier: ModelNotifier, updates: Seq[(UUID, FrontEndConnectionStatus.Status)], filter: Option[EntityFilter] = None): Seq[FrontEndConnectionStatus]
+}
+
+object SquerylFrontEndModel extends FrontEndModel {
+ import io.greenbus.services.model.EntityBasedModels._
+ import io.greenbus.services.model.FrontEndModel._
+
+ def pointExists(id: UUID, filter: Option[EntityFilter] = None): Boolean = {
+ from(points)((pt) =>
+ where(EntityFilter.optional(filter, id) and
+ pt.entityId === id)
+ select (pt.id)).nonEmpty
+ }
+
+ def sourceParentMap(children: Seq[UUID]): Map[UUID, UUID] = {
+ from(entities, edges, entities)((childEnt, edge, endEnt) =>
+ where(childEnt.id in children and
+ edge.childId === childEnt.id and
+ edge.relationship === "source" and
+ edge.parentId === endEnt.id)
+ select (childEnt.id, endEnt.id)).toList.toMap
+ }
+
+ private def buildPointProto(tup: (UUID, String, Seq[String], PointRow)): Point = {
+ val (id, name, types, row) = tup
+ Point.newBuilder()
+ .setUuid(id)
+ .setName(name)
+ .addAllTypes(types)
+ .setPointCategory(PointCategory.valueOf(row.pointCategory))
+ .setUnit(row.unit)
+ .build()
+ }
+
+ def pointKeyQuery(uuids: Seq[UUID], names: Seq[String], filter: Option[EntityFilter] = None): Seq[Point] = {
+
+ val rows: Seq[(UUID, String, Seq[String], PointRow)] = entityBasedKeyQuery(uuids, names, points, filter)
+
+ val pointToEndpointMap: Map[UUID, UUID] = sourceParentMap(rows.map(_._1))
+
+ rows.map {
+ case (id, name, types, row) =>
+ val b = Point.newBuilder()
+ .setUuid(id)
+ .setName(name)
+ .addAllTypes(types)
+ .setPointCategory(PointCategory.valueOf(row.pointCategory))
+ .setUnit(row.unit)
+
+ pointToEndpointMap.get(id).foreach(uuid => b.setEndpointUuid(uuidToProtoUUID(uuid)))
+
+ b.build
+ }
+ }
+
+ def pointQuery(pointCategories: Seq[PointCategory], units: Seq[String], typeParams: TypeParams, lastUuid: Option[UUID], lastName: Option[String], pageSize: Int, pageByName: Boolean = true, filter: Option[EntityFilter] = None): Seq[Point] = {
+
+ def rowClause(row: PointRow): LogicalBoolean = {
+ val pointTypeIds = pointCategories.map(_.getNumber)
+
+ (row.pointCategory in pointTypeIds).inhibitWhen(pointTypeIds.isEmpty) and
+ (row.unit in units).inhibitWhen(units.isEmpty)
+ }
+
+ val results = entityBasedQuery(points, typeParams, rowClause, lastUuid, lastName, pageSize, pageByName, filter)
+
+ val pointToEndpointMap: Map[UUID, UUID] = sourceParentMap(results.map(_._1))
+
+ results.map {
+ case (uuid, name, types, row) =>
+ val endpointUuid = pointToEndpointMap.get(uuid)
+ val b = Point.newBuilder()
+ .setUuid(uuid)
+ .setName(name)
+ .addAllTypes(types)
+ .setPointCategory(PointCategory.valueOf(row.pointCategory))
+ .setUnit(row.unit)
+
+ endpointUuid.map(uuidToProtoUUID).foreach(b.setEndpointUuid)
+
+ b.build()
+ }
+ }
+
+ def putPoints(notifier: ModelNotifier, templates: Seq[CoreTypeTemplate[PointInfo]], allowCreate: Boolean = true, updateFilter: Option[EntityFilter] = None): Seq[Point] = {
+
+ val (withIds, withNames) = splitTemplates(templates.map(t => t.copy(types = t.types + "Point")))
+
+ def create(id: UUID, temp: PointInfo) = PointRow(0, id, temp.pointCategory.getNumber, temp.unit)
+
+ def update(id: UUID, row: PointRow, temp: PointInfo) = {
+ if (row.pointCategory != temp.pointCategory.getNumber || row.unit != temp.unit) {
+ Some(row.copy(pointCategory = temp.pointCategory.getNumber, unit = temp.unit))
+ } else {
+ None
+ }
+ }
+
+ def toUuid(point: Point) = point.getUuid
+
+ putEntityBased(notifier, withIds, withNames, "Point", create, update, pointKeyQuery(_, _, None), toUuid, points, allowCreate, updateFilter)
+ }
+
+ def deletePoints(notifier: ModelNotifier, ids: Seq[UUID], filter: Option[EntityFilter] = None): Seq[Point] = {
+ deleteEntityBased(notifier, ids, pointKeyQuery(_, Nil), points, filter)
+ }
+
+ private def buildCommandProto(tup: (UUID, String, Seq[String], CommandRow)): Command = {
+ val (id, name, types, row) = tup
+ Command.newBuilder()
+ .setUuid(id)
+ .setName(name)
+ .addAllTypes(types)
+ .setDisplayName(row.displayName)
+ .setCommandCategory(CommandCategory.valueOf(row.commandCategory))
+ .build()
+ }
+
+ def commandKeyQuery(uuids: Seq[UUID], names: Seq[String], filter: Option[EntityFilter] = None): Seq[Command] = {
+
+ val rows: Seq[(UUID, String, Seq[String], CommandRow)] = entityBasedKeyQuery(uuids, names, commands, filter)
+
+ val commandToEndpointMap: Map[UUID, UUID] = sourceParentMap(rows.map(_._1))
+
+ rows.map {
+ case (id, name, types, row) =>
+ val b = Command.newBuilder()
+ .setUuid(id)
+ .setName(name)
+ .addAllTypes(types)
+ .setDisplayName(row.displayName)
+ .setCommandCategory(CommandCategory.valueOf(row.commandCategory))
+
+ commandToEndpointMap.get(id).foreach(uuid => b.setEndpointUuid(uuidToProtoUUID(uuid)))
+ b.build
+ }
+ }
+
+ def commandQuery(commandCategories: Seq[CommandCategory], typeParams: TypeParams, lastUuid: Option[UUID], lastName: Option[String], pageSize: Int, pageByName: Boolean = true, filter: Option[EntityFilter] = None): Seq[Command] = {
+
+ def rowClause(row: CommandRow): LogicalBoolean = {
+ val commandTypeIds = commandCategories.map(_.getNumber)
+
+ (row.commandCategory in commandTypeIds).inhibitWhen(commandTypeIds.isEmpty)
+ }
+
+ val results = entityBasedQuery(commands, typeParams, rowClause, lastUuid, lastName, pageSize, pageByName, filter)
+
+ val pointToEndpointMap: Map[UUID, UUID] = sourceParentMap(results.map(_._1))
+
+ results.map {
+ case (uuid, name, types, row) =>
+ val endpointUuid = pointToEndpointMap.get(uuid)
+ val b = Command.newBuilder()
+ .setUuid(uuid)
+ .setName(name)
+ .addAllTypes(types)
+ .setDisplayName(row.displayName)
+ .setCommandCategory(CommandCategory.valueOf(row.commandCategory))
+
+ endpointUuid.map(uuidToProtoUUID).foreach(b.setEndpointUuid)
+
+ b.build()
+ }
+ }
+
+ def putCommands(notifier: ModelNotifier, templates: Seq[CoreTypeTemplate[CommandInfo]], allowCreate: Boolean = true, updateFilter: Option[EntityFilter] = None): Seq[Command] = {
+
+ val (withIds, withNames) = splitTemplates(templates.map(t => t.copy(types = t.types + "Command")))
+
+ def create(id: UUID, temp: CommandInfo) = CommandRow(0, id, temp.displayName, temp.commandCategory.getNumber, None)
+
+ def update(id: UUID, row: CommandRow, temp: CommandInfo) = {
+ if (row.commandCategory != temp.commandCategory.getNumber || row.displayName != temp.displayName) {
+ Some(row.copy(commandCategory = temp.commandCategory.getNumber, displayName = temp.displayName))
+ } else {
+ None
+ }
+ }
+
+ def toUuid(command: Command) = command.getUuid
+
+ putEntityBased(notifier, withIds, withNames, "Command", create, update, commandKeyQuery(_, _, None), toUuid, commands, allowCreate, updateFilter)
+ }
+
+ def deleteCommands(notifier: ModelNotifier, ids: Seq[UUID], filter: Option[EntityFilter] = None): Seq[Command] = {
+ deleteEntityBased(notifier, ids, commandKeyQuery(_, Nil), commands, filter)
+ }
+
+ private def buildEndpoint(tup: (UUID, String, Seq[String], EndpointRow)): Endpoint = tup match {
+ case (id, name, types, row) =>
+ Endpoint.newBuilder()
+ .setUuid(id)
+ .setName(name)
+ .addAllTypes(types)
+ .setProtocol(row.protocol)
+ .setDisabled(row.disabled)
+ .build
+ }
+
+ def endpointKeyQuery(uuids: Seq[UUID], names: Seq[String], filter: Option[EntityFilter] = None): Seq[Endpoint] = {
+ entityBasedKeyQuery(uuids, names, endpoints, filter).map(buildEndpoint)
+ }
+
+ def endpointQuery(protocols: Seq[String], disabledOpt: Option[Boolean], typeParams: TypeParams, lastUuid: Option[UUID], lastName: Option[String], pageSize: Int, pageByName: Boolean = true, filter: Option[EntityFilter] = None): Seq[Endpoint] = {
+
+ def rowClause(row: EndpointRow): LogicalBoolean = {
+
+ (row.protocol in protocols).inhibitWhen(protocols.isEmpty) and
+ (row.disabled === disabledOpt.?)
+ }
+
+ val results = entityBasedQuery(endpoints, typeParams, rowClause, lastUuid, lastName, pageSize, pageByName, filter)
+
+ results.map(buildEndpoint)
+ }
+
+ def putEndpoints(notifier: ModelNotifier, templates: Seq[CoreTypeTemplate[EndpointInfo]], allowCreate: Boolean = true, updateFilter: Option[EntityFilter] = None): Seq[Endpoint] = {
+
+ val (withIds, withNames) = splitTemplates(templates.map(t => t.copy(types = t.types + "Endpoint")))
+
+ def create(id: UUID, temp: EndpointInfo) = EndpointRow(0, id, temp.protocol, temp.disabled.getOrElse(false))
+
+ def update(id: UUID, row: EndpointRow, temp: EndpointInfo) = {
+ val disabledChanged = temp.disabled.nonEmpty && temp.disabled != Some(row.disabled)
+ if (row.protocol != temp.protocol || disabledChanged) {
+ val withDisabled = temp.disabled.map(d => row.copy(disabled = d)).getOrElse(row)
+ Some(withDisabled.copy(protocol = temp.protocol))
+ } else {
+ None
+ }
+ }
+
+ def toUuid(proto: Endpoint) = proto.getUuid
+
+ putEntityBased(notifier, withIds, withNames, "Endpoint", create, update, endpointKeyQuery(_, _, None), toUuid, endpoints, allowCreate, updateFilter)
+ }
+
+ def deleteEndpoints(notifier: ModelNotifier, ids: Seq[UUID], filter: Option[EntityFilter] = None): Seq[Endpoint] = {
+ deleteEntityBased(notifier, ids, endpointKeyQuery(_, Nil), endpoints, filter)
+ }
+
+ def putEndpointsDisabled(notifier: ModelNotifier, updates: Seq[EndpointDisabledUpdate], filter: Option[EntityFilter] = None): Seq[Endpoint] = {
+
+ val ids = updates.map(_.id)
+
+ filter.foreach { filter =>
+ if (filter.anyOutsideSet(ids)) {
+ throw new ModelPermissionException("Tried to modify restricted objects")
+ }
+ }
+
+ val current: Map[UUID, EndpointRow] = endpoints.where(end => end.entityId in ids).toList.map(row => (row.entityId, row)).toMap
+
+ val updateRows = updates.flatMap { up =>
+ current.get(up.id) match {
+ case None => throw new ModelInputException(s"Endpoint with id ${up.id} not found")
+ case Some(row) =>
+ if (row.disabled != up.disabled) {
+ Some(row.copy(disabled = up.disabled))
+ } else {
+ None
+ }
+ }
+ }
+
+ if (updateRows.nonEmpty) {
+ endpoints.update(updateRows)
+ }
+
+ val updateIds = updateRows.map(_.entityId)
+
+ val results = endpointKeyQuery(ids, Nil)
+
+ val updatedResults = results.filter(end => updateIds.contains(protoUUIDToUuid(end.getUuid)))
+
+ updatedResults.foreach(notifier.notify(Updated, _))
+
+ results
+ }
+
+ def frontEndConnectionKeyQuery(endpointUuids: Seq[UUID], endpointNames: Seq[String], filter: Option[EntityFilter] = None): Seq[FrontEndRegistration] = {
+
+ val rows: Seq[(UUID, String, Option[String])] =
+ from(entities, endpoints, frontEndConnections)((ent, end, fep) =>
+ where(EntityFilter.optional(filter, ent).inhibitWhen(filter.isEmpty) and
+ ((ent.id in endpointUuids).inhibitWhen(endpointUuids.isEmpty) or
+ (ent.name in endpointNames).inhibitWhen(endpointNames.isEmpty)) and
+ (end.entityId === ent.id) and
+ (fep.endpointId === end.entityId))
+ select (ent.id, fep.inputAddress, fep.commandAddress)).toSeq
+
+ rows.map {
+ case (uuid, inputAddr, optCmdAddr) =>
+ val b = FrontEndRegistration.newBuilder()
+ .setEndpointUuid(uuid)
+ .setInputAddress(inputAddr)
+
+ optCmdAddr.foreach(b.setCommandAddress)
+ b.build()
+ }
+ }
+
+ def putFrontEndConnections(notifier: ModelNotifier, templates: Seq[FrontEndConnectionTemplate], filter: Option[EntityFilter] = None): Seq[FrontEndRegistration] = {
+ val ids = templates.map(_.endpointId)
+ filter.foreach { filt =>
+ if (filt.anyOutsideSet(ids)) {
+ throw new ModelPermissionException("Tried to register for restricted endpoint")
+ }
+ }
+
+ val current: Map[UUID, Option[FrontEndConnectionRow]] =
+ join(endpoints, frontEndConnections.leftOuter)((end, fep) =>
+ where(end.entityId in ids)
+ select (end.entityId, fep)
+ on (Some(end.entityId) === fep.map(_.endpointId))).toSeq.toMap
+
+ if (ids.exists(id => !current.contains(id))) {
+ throw new ModelInputException("Endpoint for front end connection does not exist")
+ }
+
+ val existing: Seq[(UUID, FrontEndConnectionRow)] = ids.flatMap(id => current(id).map(row => (id, row)))
+
+ val createIds = ids.filter(current(_).isEmpty)
+
+ val templateMap: Map[UUID, FrontEndConnectionTemplate] = templates.map(temp => (temp.endpointId, temp)).toMap
+
+ val updateRows = existing.map {
+ case (id, oldRow) =>
+ val template = templateMap(id)
+ oldRow.copy(inputAddress = template.inputAddress,
+ commandAddress = template.commandAddress)
+ }
+
+ val insertRows = createIds.map { id =>
+ val template = templateMap(id)
+ FrontEndConnectionRow(0, id, template.inputAddress, template.commandAddress)
+ }
+
+ frontEndConnections.update(updateRows)
+ frontEndConnections.insert(insertRows)
+
+ val results = frontEndConnectionKeyQuery(ids, Nil)
+
+ val updateSet = existing.map(_._1).toSet
+ val insertSet = createIds.toSet
+
+ results.filter(r => insertSet.contains(r.getEndpointUuid)).foreach(notifier.notify(Created, _))
+ results.filter(r => updateSet.contains(r.getEndpointUuid)).foreach(notifier.notify(Updated, _))
+
+ results
+ }
+
+ def addressForCommand(command: UUID): Option[(Option[String], String)] = {
+
+ val edgeForCommand =
+ from(edges)(e =>
+ where(e.childId === command and
+ e.relationship === "source" and
+ e.distance === 1)
+ select (e))
+
+ from(commands, endpoints, frontEndConnections, entities)((cmd, end, fep, ent) =>
+ where(cmd.entityId === command and
+ (ent.id === cmd.entityId) and
+ (end.entityId in from(edgeForCommand)(e => select(e.parentId))) and
+ fep.endpointId === end.entityId)
+ select ((fep.commandAddress, ent.name))).headOption
+ }
+
+ private def connectionStatusQuery(uuids: Seq[UUID], names: Seq[String], filter: Option[EntityFilter] = None): Query[(UUID, String, FrontEndCommStatusRow)] = {
+ from(endpoints, entities, frontEndCommStatuses)((end, ent, stat) =>
+ where(
+ EntityFilter.optional(filter, ent).inhibitWhen(filter.isEmpty) and
+ (ent.id in uuids).inhibitWhen(uuids.isEmpty) and
+ (ent.name in names).inhibitWhen(names.isEmpty) and
+ end.entityId === ent.id and
+ stat.endpointId === end.entityId)
+ select (ent.id, ent.name, stat))
+ }
+
+ def getFrontEndConnectionStatuses(uuids: Seq[UUID], names: Seq[String], filter: Option[EntityFilter] = None): Seq[FrontEndConnectionStatus] = {
+
+ val results = connectionStatusQuery(uuids, names, filter).toSeq
+
+ results.map {
+ case (uuid, name, statusRow) =>
+ FrontEndConnectionStatus.newBuilder()
+ .setEndpointUuid(uuid)
+ .setEndpointName(name)
+ .setState(FrontEndConnectionStatus.Status.valueOf(statusRow.status))
+ .setUpdateTime(statusRow.updateTime)
+ .build()
+ }.toList
+ }
+
+ def putFrontEndConnectionStatuses(notifier: ModelNotifier, updates: Seq[(UUID, FrontEndConnectionStatus.Status)], filter: Option[EntityFilter] = None): Seq[FrontEndConnectionStatus] = {
+ val uuids = updates.map(_._1)
+ filter.foreach { filt =>
+ if (filt.anyOutsideSet(uuids)) {
+ throw new ModelPermissionException("Tried to register for restricted endpoint")
+ }
+ }
+
+ val existing: List[(UUID, String, FrontEndCommStatusRow)] = connectionStatusQuery(uuids, Nil).toList
+ val existingUuids = existing.map(_._1).toSet
+
+ val missingUuids = uuids.filterNot(existingUuids.contains)
+
+ val endpointSet: Set[UUID] =
+ from(endpoints, entities)((end, ent) =>
+ where((ent.id in missingUuids) and end.entityId === ent.id)
+ select (ent.id)).toList.toSet
+
+ if (missingUuids.filterNot(endpointSet.contains).nonEmpty) {
+ throw new ModelInputException("Tried to update connection statuses for non-existent endpoints")
+ }
+
+ val uuidToStatusMap = updates.toMap
+ val now = System.currentTimeMillis()
+
+ val missingRecords = missingUuids.flatMap { uuid => uuidToStatusMap.get(uuid).map(status => FrontEndCommStatusRow(0, uuid, status.getNumber, now)) }
+
+ val updateRecords = existing.flatMap {
+ case (uuid, name, row) => uuidToStatusMap.get(uuid).map(status => row.copy(status = status.getNumber, updateTime = now))
+ }
+
+ frontEndCommStatuses.insert(missingRecords)
+ frontEndCommStatuses.update(updateRecords)
+
+ val results = getFrontEndConnectionStatuses(uuids, Nil)
+
+ results.foreach(notifier.notify(Updated, _))
+
+ results
+ }
+}
diff --git a/services/src/main/scala/io/greenbus/services/model/ModelHelpers.scala b/services/src/main/scala/io/greenbus/services/model/ModelHelpers.scala
new file mode 100644
index 0000000..349587b
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/model/ModelHelpers.scala
@@ -0,0 +1,62 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.model
+
+import scala.collection.mutable
+import org.squeryl.dsl.ast.{ BinaryOperatorNodeLogicalBoolean, LogicalBoolean }
+
+object ModelHelpers {
+
+ def groupSortedAndPreserveOrder[A, B](input: Seq[(A, B)]): Seq[(A, Seq[B])] = {
+ val results = mutable.ListBuffer.empty[(A, Seq[B])]
+ var current = Option.empty[A]
+ var currentAccum = mutable.ListBuffer.empty[B]
+ val itr = input.iterator
+ while (itr.hasNext) {
+ val (a, b) = itr.next()
+ current match {
+ case None =>
+ current = Some(a)
+ currentAccum += b
+ case Some(currentA) => {
+ if (currentA != a) {
+ results += ((currentA, currentAccum))
+ current = Some(a)
+ currentAccum = mutable.ListBuffer.empty[B]
+ currentAccum += b
+ } else {
+ currentAccum += b
+ }
+ }
+ }
+ }
+
+ current.foreach { curr =>
+ results += ((curr, currentAccum))
+ }
+ results
+ }
+
+ def logicalCombine(op: String)(a: LogicalBoolean, b: LogicalBoolean) = {
+ new BinaryOperatorNodeLogicalBoolean(a, b, op)
+ }
+
+ def logicalAnd = logicalCombine("and") _
+ def logicalOr = logicalCombine("or") _
+}
diff --git a/services/src/main/scala/io/greenbus/services/model/ModelInputException.scala b/services/src/main/scala/io/greenbus/services/model/ModelInputException.scala
new file mode 100644
index 0000000..096a434
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/model/ModelInputException.scala
@@ -0,0 +1,28 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.model
+
+// State of the database has revealed an error
+class ModelInputException(msg: String) extends Exception(msg)
+
+// Auth forbidden
+class ModelPermissionException(msg: String) extends ModelInputException(msg)
+
+// Breaks data invariant, likely internal service error
+class ModelAssertionException(msg: String) extends ModelInputException(msg)
diff --git a/services/src/main/scala/io/greenbus/services/model/ProcessingModel.scala b/services/src/main/scala/io/greenbus/services/model/ProcessingModel.scala
new file mode 100644
index 0000000..fd824be
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/model/ProcessingModel.scala
@@ -0,0 +1,208 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.model
+
+import java.util.UUID
+
+import com.google.protobuf.GeneratedMessage
+import org.squeryl.PrimitiveTypeMode._
+import io.greenbus.client.service.proto.Model.ModelUUID
+import io.greenbus.client.service.proto.Processing.MeasOverride
+import io.greenbus.services.authz.EntityFilter
+import io.greenbus.services.core.OverrideWithEndpoint
+import io.greenbus.services.data.ServicesSchema._
+import io.greenbus.services.framework._
+import io.greenbus.services.model.UUIDHelpers._
+
+object ProcessingModel {
+ val overrideKey = "meas_override"
+ val triggerSetKey = "trigger_set"
+
+}
+trait ProcessingModel {
+
+ def overrideKeyQuery(uuids: Seq[UUID], names: Seq[String], filter: Option[EntityFilter] = None): Seq[MeasOverride]
+ def putOverrides(notifier: ModelNotifier, overrides: Seq[(UUID, MeasOverride)], filter: Option[EntityFilter]): Seq[MeasOverride]
+ def deleteOverrides(notifier: ModelNotifier, ids: Seq[UUID], filter: Option[EntityFilter] = None): Seq[MeasOverride]
+
+}
+
+object SquerylProcessingModel extends ProcessingModel {
+ import io.greenbus.services.model.ProcessingModel._
+
+ def overrideKeyQuery(uuids: Seq[UUID], names: Seq[String], filter: Option[EntityFilter]): Seq[MeasOverride] = {
+
+ val allIds = allPointIdsForKeyQuery(uuids, names, filter)
+
+ val results = SquerylEntityModel.getKeyValues(allIds, overrideKey)
+
+ results.map { case (_, bytes) => MeasOverride.parseFrom(bytes) }
+ }
+
+ def putOverrides(notifier: ModelNotifier, overrides: Seq[(UUID, MeasOverride)], filter: Option[EntityFilter]): Seq[MeasOverride] = {
+
+ def toNote(obj: MeasOverride, end: Option[ModelUUID]) = OverrideWithEndpoint(obj, end)
+
+ putPointBasedKeyValues(notifier, overrides, overrideKey, MeasOverride.parseFrom, toNote, filter)
+ }
+
+ def deleteOverrides(notifier: ModelNotifier, ids: Seq[UUID], filter: Option[EntityFilter]): Seq[MeasOverride] = {
+ def keyValueToResult(pointUuid: UUID, obj: Array[Byte], end: Option[UUID]) = MeasOverride.parseFrom(obj)
+ def keyValueToNotification(pointUuid: UUID, obj: MeasOverride, end: Option[UUID]) = OverrideWithEndpoint(obj, end.map(uuidToProtoUUID))
+
+ SquerylProcessingModel.deleteKeyValuesWithEndpoint(notifier, ids, overrideKey, keyValueToResult, keyValueToNotification, filter)
+ }
+
+ def allPointIdsForKeyQuery(uuids: Seq[UUID], names: Seq[String], filter: Option[EntityFilter]): Seq[UUID] = {
+
+ val idsForNames: Seq[UUID] =
+ from(entities, points)((ent, pt) =>
+ where(EntityFilter.optional(filter, ent).inhibitWhen(filter.isEmpty) and
+ ent.id === pt.entityId and
+ (ent.name in names))
+ select (ent.id)).toSeq
+
+ uuids ++ idsForNames
+ }
+
+ def putPointBasedKeyValuesWithEndpoint[PayloadType <: GeneratedMessage, ResultType, NoteType](notifier: ModelNotifier,
+ values: Seq[(UUID, PayloadType)],
+ key: String,
+ toResult: (UUID, Array[Byte], Option[UUID]) => ResultType,
+ toNotification: (UUID, ResultType, Option[UUID]) => NoteType,
+ filter: Option[EntityFilter]): Seq[ResultType] = {
+
+ val ids = values.map(_._1)
+ filter.foreach { filt =>
+ if (filt.anyOutsideSet(ids)) {
+ throw new ModelPermissionException("Tried to touch restricted entities")
+ }
+ }
+
+ val existingPoints = from(entities, points)((ent, pt) =>
+ where((ent.id in ids) and pt.entityId === ent.id)
+ select (ent.id)).toSeq
+
+ val existingPointsSet = existingPoints.toSet
+ if (ids.exists(id => !existingPointsSet.contains(id))) {
+ throw new ModelInputException("Tried to put value to non-existent point")
+ }
+
+ val kvs = values.map { case (id, proto) => (id, proto.toByteArray) }
+
+ val (results, creates, updates) = SquerylEntityModel.putKeyValues(kvs, key)
+
+ val endpointMap = SquerylFrontEndModel.sourceParentMap(ids)
+
+ val parsed = results.map { case (id, bytes) => (id, toResult(id, bytes, endpointMap.get(id))) }
+
+ val parsedMap = parsed.toMap
+
+ creates.foreach { uuid =>
+ parsedMap.get(uuid).foreach(obj => notifier.notify(Created, toNotification(uuid, obj, endpointMap.get(uuid))))
+ }
+ updates.foreach { uuid =>
+ parsedMap.get(uuid).foreach(obj => notifier.notify(Updated, toNotification(uuid, obj, endpointMap.get(uuid))))
+ }
+
+ parsed.map(_._2)
+ }
+
+ def putPointBasedKeyValues[A <: GeneratedMessage, NoteType](notifier: ModelNotifier,
+ values: Seq[(UUID, A)],
+ key: String,
+ parse: Array[Byte] => A,
+ noteFunc: (A, Option[ModelUUID]) => NoteType,
+ filter: Option[EntityFilter]): Seq[A] = {
+
+ val ids = values.map(_._1)
+ filter.foreach { filt =>
+ if (filt.anyOutsideSet(ids)) {
+ throw new ModelPermissionException("Tried to touch restricted entities")
+ }
+ }
+
+ val existingPoints = from(entities, points)((ent, pt) =>
+ where((ent.id in ids) and pt.entityId === ent.id)
+ select (ent.id)).toSeq
+
+ val existingPointsSet = existingPoints.toSet
+ if (ids.exists(id => !existingPointsSet.contains(id))) {
+ throw new ModelInputException("Tried to put value to non-existent point")
+ }
+
+ val kvs = values.map { case (id, proto) => (id, proto.toByteArray) }
+
+ val (results, creates, updates) = SquerylEntityModel.putKeyValues(kvs, key)
+
+ val parsed = results.map { case (id, bytes) => (id, parse(bytes)) }
+
+ val parsedMap = parsed.toMap
+
+ val endpointMap = SquerylFrontEndModel.sourceParentMap(ids)
+
+ creates.foreach { uuid =>
+ parsedMap.get(uuid).foreach(obj => notifier.notify(Created, noteFunc(obj, endpointMap.get(uuid).map(uuidToProtoUUID))))
+ }
+ updates.foreach { uuid =>
+ parsedMap.get(uuid).foreach(obj => notifier.notify(Updated, noteFunc(obj, endpointMap.get(uuid).map(uuidToProtoUUID))))
+ }
+
+ parsed.map(_._2)
+ }
+
+ def deleteKeyValues[A](notifier: ModelNotifier, ids: Seq[UUID], key: String, parse: Array[Byte] => A, filter: Option[EntityFilter]): Seq[A] = {
+ val results = SquerylEntityModel.deleteKeyValues(ids, key, filter)
+ val parsed = results.map { case (id, bytes) => (id, parse(bytes)) }
+
+ val values = parsed.map(_._2)
+ values.foreach(notifier.notify(Deleted, _))
+ values
+ }
+
+ def deleteKeyValuesWithEndpoint[PayloadType <: GeneratedMessage, ResultType, NoteType](
+ notifier: ModelNotifier,
+ ids: Seq[UUID],
+ key: String,
+ toResult: (UUID, Array[Byte], Option[UUID]) => ResultType,
+ toNotification: (UUID, ResultType, Option[UUID]) => NoteType,
+ filter: Option[EntityFilter]): Seq[ResultType] = {
+
+ filter.foreach { filt =>
+ if (filt.anyOutsideSet(ids)) {
+ throw new ModelPermissionException("Tried to delete restricted entity key values")
+ }
+ }
+
+ val endpointMap = SquerylFrontEndModel.sourceParentMap(ids)
+
+ val results = SquerylEntityModel.deleteKeyValues(ids, key, None)
+ val resultParts: Seq[(UUID, ResultType, Option[UUID])] = results.map {
+ case (id, bytes) =>
+ val endpointUuid = endpointMap.get(id)
+ (id, toResult(id, bytes, endpointUuid), endpointUuid)
+ }
+
+ resultParts.foreach {
+ case (id, result, endOpt) => notifier.notify(Deleted, toNotification(id, result, endOpt))
+ }
+
+ resultParts.map(_._2)
+ }
+}
diff --git a/services/src/main/scala/io/greenbus/services/model/UUIDHelpers.scala b/services/src/main/scala/io/greenbus/services/model/UUIDHelpers.scala
new file mode 100644
index 0000000..90eb781
--- /dev/null
+++ b/services/src/main/scala/io/greenbus/services/model/UUIDHelpers.scala
@@ -0,0 +1,30 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.model
+
+import java.util.UUID
+import io.greenbus.client.service.proto.Model.{ ModelID, ModelUUID }
+
+object UUIDHelpers {
+ implicit def uuidToProtoUUID(uuid: UUID): ModelUUID = ModelUUID.newBuilder().setValue(uuid.toString).build()
+ implicit def protoUUIDToUuid(uuid: ModelUUID): UUID = UUID.fromString(uuid.getValue)
+
+ implicit def longToProtoId(id: Long): ModelID = ModelID.newBuilder().setValue(id.toString).build()
+ implicit def protoIdToLong(id: ModelID): Long = id.getValue.toLong
+}
diff --git a/services/src/test/scala/io/greenbus/services/DbBenchmarking.scala b/services/src/test/scala/io/greenbus/services/DbBenchmarking.scala
new file mode 100644
index 0000000..ff3adbe
--- /dev/null
+++ b/services/src/test/scala/io/greenbus/services/DbBenchmarking.scala
@@ -0,0 +1,88 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services
+
+import io.greenbus.sql.{ DbConnector, SqlSettings, DbConnection }
+import io.greenbus.services.model.SquerylEntityModel
+import io.greenbus.util.Timing
+import io.greenbus.services.data.ServicesSchema
+import io.greenbus.services.framework.NullModelNotifier
+
+object DbBenchmarking {
+
+ def connectSql(): DbConnection = {
+ val config = SqlSettings.load("../io.greenbus.sql.cfg")
+ DbConnector.connect(config)
+ }
+
+ def buildEntities(count: Int): Seq[(String, Seq[String])] = {
+ Range(0, count).map(i => (s"ent$i", Seq("typeA", "typeB", "typeC")))
+ }
+
+ def trial(sql: DbConnection, entCount: Int, record: Boolean) {
+ val ents = buildEntities(entCount)
+
+ sql.transaction {
+ ServicesSchema.reset()
+ }
+
+ val sepTrans = Timing.benchmark {
+ ents.foreach { ent =>
+ sql.transaction {
+ SquerylEntityModel.putEntities(NullModelNotifier, Nil, Seq((ent._1, ent._2.toSet)))
+ }
+ }
+ }
+
+ val sepTransSingle = Timing.benchmark {
+ ents.foreach { ent =>
+ sql.transaction {
+ SquerylEntityModel.putEntity(NullModelNotifier, ent._1, ent._2, false, false, false)
+ }
+ }
+ }
+
+ val oneTransSeparate = Timing.benchmark {
+ sql.transaction {
+ ents.foreach(ent => SquerylEntityModel.putEntity(NullModelNotifier, ent._1, ent._2, false, false, false))
+ }
+ }
+
+ val batched = Timing.benchmark {
+ sql.transaction {
+ SquerylEntityModel.putEntities(NullModelNotifier, Nil, ents.map(ent => (ent._1, ent._2.toSet)))
+ }
+ }
+
+ if (record) {
+ println(Seq(entCount, sepTrans, sepTransSingle, oneTransSeparate, batched).mkString("\t"))
+ }
+ }
+
+ def main(args: Array[String]) {
+
+ val sql = connectSql()
+
+ // warmup
+ trial(sql, 5, false)
+
+ Range(0, 12).foreach(n => trial(sql, Math.pow(2, n).toInt, true))
+
+ }
+}
diff --git a/services/src/test/scala/io/greenbus/services/authz/EntityFilterTest.scala b/services/src/test/scala/io/greenbus/services/authz/EntityFilterTest.scala
new file mode 100644
index 0000000..587dd55
--- /dev/null
+++ b/services/src/test/scala/io/greenbus/services/authz/EntityFilterTest.scala
@@ -0,0 +1,89 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.authz
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import io.greenbus.services.model.{ ServiceTestBase, ModelTestHelpers }
+import org.squeryl.PrimitiveTypeMode._
+import io.greenbus.services.data.ServicesSchema._
+
+@RunWith(classOf[JUnitRunner])
+class EntityFilterTest extends ServiceTestBase {
+ import ModelTestHelpers._
+
+ def check(results: Set[String], denies: Seq[ResourceSelector], allows: Seq[ResourceSelector]) {
+ val filter = ResourceSelector.buildFilter(denies, allows)
+ val sql = from(entities)(ent => where(filter(ent.id)) select (ent.name))
+ //println(sql.statement)
+ val names = sql.toSeq
+ names.size should equal(results.size)
+ names.toSet should equal(results)
+ }
+
+ /*
+ 5 1 2 6
+ 3 4
+ */
+ test("Set filtering") {
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeB"))
+ val ent03 = createEntity("ent03", Seq("typeX", "typeY"))
+ val ent04 = createEntity("ent04", Seq("typeY", "typeZ"))
+ val ent05 = createEntity("ent05", Seq("typeB"))
+ val ent06 = createEntity("ent06", Seq("typeA"))
+ createEdge(ent01.id, ent03.id, "owns", 1)
+ createEdge(ent01.id, ent04.id, "owns", 1)
+ createEdge(ent02.id, ent04.id, "owns", 1)
+ createEdge(ent02.id, ent06.id, "unrelated", 1)
+
+ val all = Set("ent01", "ent02", "ent03", "ent04", "ent05", "ent06")
+
+ {
+ val denies = Seq(TypeSelector(Seq("typeA", "typeB")))
+ val allows = Nil
+ check(Set("ent03", "ent04"), denies, allows)
+ }
+
+ {
+ val denies = Nil
+ val allows = Seq(TypeSelector(Seq("typeY")))
+ check(Set("ent03", "ent04"), denies, allows)
+ }
+
+ {
+ val denies = Seq(TypeSelector(Seq("typeZ")))
+ val allows = Seq(TypeSelector(Seq("typeY")))
+ check(Set("ent03"), denies, allows)
+ }
+
+ {
+ val denies = Nil
+ val allows = Seq(ParentSelector(Seq("ent01")))
+ check(Set("ent03", "ent04"), denies, allows)
+ }
+
+ {
+ val denies = Seq(ParentSelector(Seq("ent02")))
+ val allows = Seq(ParentSelector(Seq("ent01")))
+ check(Set("ent03"), denies, allows)
+ }
+
+ }
+}
diff --git a/services/src/test/scala/io/greenbus/services/core/LoginTests.scala b/services/src/test/scala/io/greenbus/services/core/LoginTests.scala
new file mode 100644
index 0000000..235d2f1
--- /dev/null
+++ b/services/src/test/scala/io/greenbus/services/core/LoginTests.scala
@@ -0,0 +1,306 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.core
+
+import java.util.UUID
+import java.util.concurrent.TimeoutException
+
+import io.greenbus.client.proto.Envelope
+import io.greenbus.client.service.proto.Events.Event
+import io.greenbus.client.service.proto.LoginRequests.{ LoginRequest, PostLoginRequest, PostLoginResponse }
+import io.greenbus.jmx.MetricsManager
+import io.greenbus.msg.{ SubscriptionBinding, Subscription }
+import io.greenbus.msg.amqp.{ AmqpAddressedMessage, AmqpServiceOperations }
+import io.greenbus.msg.service.{ ServiceHandler, ServiceHandlerSubscription }
+import io.greenbus.services.authz.{ AuthLookup, DefaultAuthLookup }
+import io.greenbus.services.data.ServicesSchema
+import io.greenbus.services.framework._
+import io.greenbus.services.model.ModelTestHelpers.TestModelNotifier
+import io.greenbus.services.model.{ ModelTestHelpers, EventSeeding, SquerylAuthModel, SquerylEventAlarmModel }
+import io.greenbus.services.{ AsyncAuthenticationModule, AuthenticationModule, SqlAuthenticationModule }
+import io.greenbus.sql.test.DatabaseUsingTestBaseNoTransaction
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import org.squeryl.PrimitiveTypeMode._
+
+import scala.concurrent.duration._
+import scala.concurrent.{ Await, Future, promise }
+
+@RunWith(classOf[JUnitRunner])
+class LoginTests extends DatabaseUsingTestBaseNoTransaction {
+ def schemas = List(ServicesSchema)
+
+ override protected def resetDbAfterTestSuite: Boolean = true
+
+ override def beforeAll(): Unit = super.beforeAll()
+
+ class TestSyncAuthModule extends SqlAuthenticationModule {
+
+ var result: (Boolean, Option[UUID]) = (false, None)
+ var attemptOpt = Option.empty[(String, String)]
+
+ def authenticate(name: String, password: String): (Boolean, Option[UUID]) = {
+ attemptOpt = Some((name, password))
+ result
+ }
+ }
+
+ class TestAsyncAuthModule extends AsyncAuthenticationModule {
+ var attemptOpt = Option.empty[(String, String)]
+ var result: Future[Boolean] = Future.failed(new TimeoutException("timed out"))
+ def authenticate(name: String, password: String): Future[Boolean] = {
+ attemptOpt = Some((name, password))
+ result
+ }
+ }
+
+ def request(name: String = "name01", pass: String = "pass01"): PostLoginRequest = {
+ PostLoginRequest.newBuilder()
+ .setRequest(
+ LoginRequest.newBuilder()
+ .setName(name)
+ .setPassword(pass)
+ .setClientVersion("vers01")
+ .setExpirationTime(System.currentTimeMillis() + 30000)
+ .setLoginLocation("loc01")
+ .build())
+ .build()
+ }
+
+ val fakeServiceOps = new AmqpServiceOperations {
+ override def publishEvent(exchange: String, msg: Array[Byte], key: String): Unit = ???
+
+ override def bindRoutedService(handler: ServiceHandler): SubscriptionBinding = ???
+
+ override def competingServiceBinding(exchange: String): ServiceHandlerSubscription = ???
+
+ override def declareExchange(exchange: String): Unit = ???
+
+ override def bindQueue(queue: String, exchange: String, key: String): Unit = ???
+
+ override def bindCompetingService(handler: ServiceHandler, exchange: String): SubscriptionBinding = ???
+
+ override def simpleSubscription(): Subscription[Array[Byte]] = ???
+
+ override def publishBatch(messages: Seq[AmqpAddressedMessage]): Unit = ???
+
+ override def routedServiceBinding(): ServiceHandlerSubscription = ???
+ }
+
+ class TestRig(val testAuthModule: AuthenticationModule) {
+
+ val authLookup: AuthLookup = DefaultAuthLookup
+ val eventMapper = new ModelEventMapper
+ val metricsMgr = MetricsManager("io.greenbus.services")
+
+ val registry = new FullServiceRegistry(dbConnection, fakeServiceOps, authLookup, eventMapper, metricsMgr)
+ val notifier = new TestModelNotifier
+
+ val services = new LoginServices(registry, dbConnection, testAuthModule, SquerylAuthModel, SquerylEventAlarmModel, notifier)
+
+ //var prom = promise[Response[PostLoginResponse]]
+
+ var respOpt = Option.empty[Response[PostLoginResponse]]
+ def callback(resp: Response[PostLoginResponse]): Unit = {
+ respOpt = Some(resp)
+ }
+ }
+
+ class AsyncTestRig {
+ val testAuthModule = new TestAsyncAuthModule
+ val authLookup: AuthLookup = DefaultAuthLookup
+ val eventMapper = new ModelEventMapper
+ val metricsMgr = MetricsManager("io.greenbus.services")
+
+ val registry = new FullServiceRegistry(dbConnection, fakeServiceOps, authLookup, eventMapper, metricsMgr)
+ val notifier = new TestModelNotifier
+
+ val services = new LoginServices(registry, dbConnection, testAuthModule, SquerylAuthModel, SquerylEventAlarmModel, notifier)
+
+ val prom = promise[Response[PostLoginResponse]]
+
+ def callback(resp: Response[PostLoginResponse]): Unit = {
+ prom.complete(scala.util.Success(resp))
+ }
+ }
+
+ test("sync failure") {
+ val req = request()
+ val testAuthModule = new TestSyncAuthModule
+ val r = new TestRig(testAuthModule)
+ import r._
+
+ services.loginAsync(req, Map(), callback)
+
+ testAuthModule.attemptOpt should equal(Some(("name01", "pass01")))
+
+ respOpt match {
+ case Some(Failure(Envelope.Status.UNAUTHORIZED, _)) =>
+ case _ => fail()
+ }
+
+ val events = notifier.getQueue(classOf[Event])
+ events.size should equal(1)
+ events.head._2.getEventType should equal(EventSeeding.System.userLoginFailure.eventType)
+ }
+
+ test("sync success") {
+ val req = request()
+ val testAuthModule = new TestSyncAuthModule
+ val r = new TestRig(testAuthModule)
+ import r._
+
+ val uuid = UUID.randomUUID()
+ testAuthModule.result = (true, Some(uuid))
+
+ services.loginAsync(req, Map(), callback)
+
+ testAuthModule.attemptOpt should equal(Some(("name01", "pass01")))
+
+ respOpt match {
+ case Some(Success(Envelope.Status.OK, resp)) =>
+ resp.getResponse.hasToken should equal(true)
+ case _ => fail()
+ }
+
+ val all = dbConnection.transaction {
+ ServicesSchema.authTokens.where(t => true === true).toVector
+ }
+ all.size should equal(1)
+ all.head.agentId should equal(uuid)
+ all.head.loginLocation should equal(req.getRequest.getLoginLocation)
+
+ val events = notifier.getQueue(classOf[Event])
+ events.size should equal(1)
+ events.head._2.getEventType should equal(EventSeeding.System.userLogin.eventType)
+ }
+
+ test("async failure timeout") {
+ val req = request()
+ val r = new AsyncTestRig
+ import r._
+
+ services.loginAsync(req, Map(), callback)
+
+ testAuthModule.attemptOpt should equal(Some(("name01", "pass01")))
+ val fut = prom.future
+
+ Await.result(fut, 5000.milliseconds) match {
+ case Failure(Envelope.Status.RESPONSE_TIMEOUT, _) =>
+ case _ => fail()
+ }
+
+ val events = notifier.getQueue(classOf[Event])
+ events.size should equal(1)
+ events.head._2.getEventType should equal(EventSeeding.System.userLoginFailure.eventType)
+ }
+
+ test("async rando error") {
+ val req = request()
+ val r = new AsyncTestRig
+ import r._
+
+ testAuthModule.result = Future.failed(new Exception("some random exception"))
+
+ services.loginAsync(req, Map(), callback)
+
+ testAuthModule.attemptOpt should equal(Some(("name01", "pass01")))
+ val fut = prom.future
+
+ Await.result(fut, 5000.milliseconds) match {
+ case Failure(Envelope.Status.UNAUTHORIZED, _) =>
+ case _ => fail()
+ }
+
+ val events = notifier.getQueue(classOf[Event])
+ events.size should equal(1)
+ events.head._2.getEventType should equal(EventSeeding.System.userLoginFailure.eventType)
+ }
+
+ test("async negative response") {
+ val req = request()
+ val r = new AsyncTestRig
+ import r._
+
+ testAuthModule.result = Future.successful(false)
+
+ services.loginAsync(req, Map(), callback)
+
+ testAuthModule.attemptOpt should equal(Some(("name01", "pass01")))
+ val fut = prom.future
+
+ Await.result(fut, 5000.milliseconds) match {
+ case Failure(Envelope.Status.UNAUTHORIZED, _) =>
+ case _ => fail()
+ }
+
+ val events = notifier.getQueue(classOf[Event])
+ events.size should equal(1)
+ events.head._2.getEventType should equal(EventSeeding.System.userLoginFailure.eventType)
+ }
+
+ test("async positive response, no agent") {
+ val req = request()
+ val r = new AsyncTestRig
+ import r._
+
+ testAuthModule.result = Future.successful(true)
+
+ services.loginAsync(req, Map(), callback)
+
+ testAuthModule.attemptOpt should equal(Some(("name01", "pass01")))
+ val fut = prom.future
+
+ Await.result(fut, 5000.milliseconds) match {
+ case Failure(Envelope.Status.UNAUTHORIZED, _) =>
+ case _ => fail()
+ }
+
+ val events = notifier.getQueue(classOf[Event])
+ events.size should equal(1)
+ events.head._2.getEventType should equal(EventSeeding.System.userLoginFailure.eventType)
+ }
+
+ test("async positive response, agent exists") {
+ val req = request(name = "existingAgent", pass = "pass02")
+ val r = new AsyncTestRig
+ import r._
+
+ dbConnection.transaction {
+ ModelTestHelpers.createAgent("existingAgent", "pass02")
+ }
+
+ testAuthModule.result = Future.successful(true)
+
+ services.loginAsync(req, Map(), callback)
+
+ testAuthModule.attemptOpt should equal(Some(("existingAgent", "pass02")))
+ val fut = prom.future
+
+ Await.result(fut, 5000.milliseconds) match {
+ case Success(Envelope.Status.OK, resp) =>
+ resp.getResponse.hasToken should equal(true)
+ case _ => fail()
+ }
+
+ val events = notifier.getQueue(classOf[Event])
+ events.size should equal(1)
+ events.head._2.getEventType should equal(EventSeeding.System.userLogin.eventType)
+ }
+}
diff --git a/services/src/test/scala/io/greenbus/services/framework/ModelNotifierTest.scala b/services/src/test/scala/io/greenbus/services/framework/ModelNotifierTest.scala
new file mode 100644
index 0000000..d9f0b69
--- /dev/null
+++ b/services/src/test/scala/io/greenbus/services/framework/ModelNotifierTest.scala
@@ -0,0 +1,63 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.framework
+
+import io.greenbus.msg.amqp.{ AmqpMessage, AmqpAddressedMessage }
+import org.scalatest.FunSuite
+import org.scalatest.matchers.ShouldMatchers
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import scala.collection.mutable.Queue
+
+@RunWith(classOf[JUnitRunner])
+class ModelNotifierTest extends FunSuite with ShouldMatchers {
+
+ case class First(str: String)
+ case class Second(str: String)
+
+ test("Mapper") {
+ val queue1 = Queue.empty[(ModelEvent, First)]
+ val queue2 = Queue.empty[(ModelEvent, Second)]
+
+ val trans1 = new ModelEventTransformer[First] {
+ def toMessage(typ: ModelEvent, event: First): AmqpAddressedMessage = {
+ queue1 += ((typ, event))
+ AmqpAddressedMessage("exch1", "key1", AmqpMessage("str1".getBytes(), None, None))
+ }
+ }
+
+ val trans2 = new ModelEventTransformer[Second] {
+ def toMessage(typ: ModelEvent, event: Second): AmqpAddressedMessage = {
+ queue2 += ((typ, event))
+ AmqpAddressedMessage("exch2", "key2", AmqpMessage("str2".getBytes(), None, None))
+ }
+ }
+
+ val mapper = new ModelEventMapper
+ mapper.register(classOf[First], trans1)
+ mapper.register(classOf[Second], trans2)
+
+ mapper.toMessage(Created, First("one"))
+ mapper.toMessage(Deleted, Second("one"))
+
+ queue1.toSeq should equal(Seq((Created, First("one"))))
+ queue2.toSeq should equal(Seq((Deleted, Second("one"))))
+ }
+
+}
diff --git a/services/src/test/scala/io/greenbus/services/model/AuthModelTest.scala b/services/src/test/scala/io/greenbus/services/model/AuthModelTest.scala
new file mode 100644
index 0000000..998d05f
--- /dev/null
+++ b/services/src/test/scala/io/greenbus/services/model/AuthModelTest.scala
@@ -0,0 +1,427 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.model
+
+import java.util.UUID
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import org.squeryl.PrimitiveTypeMode._
+import io.greenbus.services.data.ServicesSchema
+import io.greenbus.services.framework._
+import io.greenbus.services.model.AuthModel.{ PermissionSetInfo, AgentInfo, CoreUntypedTemplate }
+
+@RunWith(classOf[JUnitRunner])
+class AuthModelTest extends ServiceTestBase {
+ import io.greenbus.services.model.ModelTestHelpers._
+
+ def checkPermissions(id: UUID, names: Set[String]) {
+ import io.greenbus.services.data.ServicesSchema._
+ val agentId = from(agents)(a => where(a.id === id) select (a.id)).single
+ val setNames = from(agentSetJoins, permissionSets)((j, ps) =>
+ where(j.agentId === agentId and ps.id === j.permissionSetId)
+ select (ps.name)).toVector
+
+ setNames.size should equal(names.size)
+ setNames.toSet should equal(names)
+ }
+ def checkPermissions(agentName: String, names: Set[String]) {
+ import io.greenbus.services.data.ServicesSchema._
+
+ val agentId = from(agents)(a =>
+ where(a.name === agentName)
+ select (a.id)).single
+
+ val permNames = from(agentSetJoins, permissionSets)((j, ps) =>
+ where(j.agentId === agentId and ps.id === j.permissionSetId)
+ select (ps.name)).toVector
+
+ permNames.size should equal(names.size)
+ permNames.toSet should equal(names)
+ }
+
+ class AuthFixture {
+ val perm1 = createPermissionSet("perm01")
+ val perm2 = createPermissionSet("perm02")
+ val perm3 = createPermissionSet("perm03")
+ val agent01 = createAgent("agent01", "pass01")
+ val agent02 = createAgent("agent02")
+ val agent03 = createAgent("agent03")
+ addPermissionToAgent(perm1.id, agent01.id)
+ addPermissionToAgent(perm2.id, agent01.id)
+ addPermissionToAgent(perm1.id, agent02.id)
+ addPermissionToAgent(perm3.id, agent02.id)
+ addPermissionToAgent(perm2.id, agent03.id)
+ addPermissionToAgent(perm3.id, agent03.id)
+ }
+
+ test("Simple login") {
+ val f = new AuthFixture
+ import f._
+
+ val now = System.currentTimeMillis()
+ val result = SquerylAuthModel.simpleLogin("agent01", "pass01", now + 5000, "ver01", "loc01")
+ result.isRight should equal(true)
+ val token = result.right.get._1
+
+ val rows = ServicesSchema.authTokens.where(t => t.token === token).toList
+ rows.size should equal(1)
+ val row = rows.head
+
+ row.agentId should equal(agent01.id)
+ row.clientVersion should equal("ver01")
+ row.loginLocation should equal("loc01")
+ row.expirationTime should equal(now + 5000)
+ row.revoked should equal(false)
+
+ val joins = ServicesSchema.tokenSetJoins.where(t => t.authTokenId === row.id).toList
+ joins.size should equal(2)
+ joins.map(_.permissionSetId).toSet should equal(Set(perm1.id, perm2.id))
+ }
+
+ test("Login validation") {
+ val f = new AuthFixture
+
+ val now = System.currentTimeMillis()
+ val result = SquerylAuthModel.simpleLogin("agent01", "pass01", now + 5000, "ver01", "loc01")
+ result.isRight should equal(true)
+ val token = result.right.get._1
+
+ val validateResult = SquerylAuthModel.authValidate(token)
+ validateResult should equal(true)
+ }
+
+ test("Login validation fail") {
+ val f = new AuthFixture
+
+ val now = System.currentTimeMillis()
+ val result = SquerylAuthModel.simpleLogin("agent01", "pass01", now + 5000, "ver01", "loc01")
+ result.isRight should equal(true)
+ val token = result.right.get._1
+
+ val validateResult = SquerylAuthModel.authValidate("crap")
+ validateResult should equal(false)
+ }
+
+ test("Logout") {
+ val f = new AuthFixture
+ import f._
+
+ val now = System.currentTimeMillis()
+ val result = SquerylAuthModel.simpleLogin("agent01", "pass01", now + 5000, "ver01", "loc01")
+ result.isRight should equal(true)
+ val token = result.right.get._1
+
+ SquerylAuthModel.simpleLogout(token)
+
+ val rows = ServicesSchema.authTokens.where(t => t.token === token).toList
+ rows.size should equal(1)
+ val row = rows.head
+
+ row.agentId should equal(agent01.id)
+ row.clientVersion should equal("ver01")
+ row.loginLocation should equal("loc01")
+ row.expirationTime should equal(now + 5000)
+ row.revoked should equal(true)
+
+ val joins = ServicesSchema.tokenSetJoins.where(t => t.authTokenId === row.id).toList
+ joins.size should equal(2)
+ joins.map(_.permissionSetId).toSet should equal(Set(perm1.id, perm2.id))
+ }
+
+ test("Validate logged out") {
+ val f = new AuthFixture
+
+ val now = System.currentTimeMillis()
+ val result = SquerylAuthModel.simpleLogin("agent01", "pass01", now + 5000, "ver01", "loc01")
+ result.isRight should equal(true)
+ val token = result.right.get._1
+
+ SquerylAuthModel.simpleLogout(token)
+
+ val validateResult = SquerylAuthModel.authValidate(token)
+ validateResult should equal(false)
+ }
+
+ def createAgentAndEntity(setNameToId: Map[String, Long], name: String, types: Set[String], permissionSets: Set[String]) = {
+ val agent01 = createAgent(name)
+ permissionSets.foreach { setName =>
+ val permId = setNameToId(setName)
+ addPermissionToAgent(permId, agent01.id)
+ }
+ }
+
+ test("Agent query") {
+ val f = new AuthFixture
+
+ val agentF = createAgent("agentF", "pass01")
+ val agentE = createAgent("agentE")
+ val agentD = createAgent("agentD")
+ val perm4 = createPermissionSet("perm04")
+ addPermissionToAgent(perm4.id, agentE.id)
+ addPermissionToAgent(perm4.id, agentD.id)
+ addPermissionToAgent(perm4.id, f.agent02.id)
+
+ val p1 = ("agent01", Set("perm01", "perm02"))
+ val p2 = ("agent02", Set("perm01", "perm03", "perm04"))
+ val p3 = ("agent03", Set("perm02", "perm03"))
+ val p4 = ("agentF", Set.empty[String])
+ val p5 = ("agentE", Set("perm04"))
+ val p6 = ("agentD", Set("perm04"))
+
+ check(Set(p1, p2)) {
+ SquerylAuthModel.agentQuery(None, Seq("perm01"), None, None, 100)
+ }
+
+ check(Set(p1, p2, p3)) {
+ SquerylAuthModel.agentQuery(None, Seq("perm01", "perm03"), None, None, 100)
+ }
+
+ check(Set(p1, p2)) {
+ SquerylAuthModel.agentQuery(None, Seq(), None, None, 2, pageByName = true)
+ }
+ check(Set(p3, p6)) {
+ SquerylAuthModel.agentQuery(None, Seq(), None, lastName = Some("agent02"), 2, pageByName = true)
+ }
+ check(Set(p5, p4)) {
+ SquerylAuthModel.agentQuery(None, Seq(), None, lastName = Some("agentD"), 2, pageByName = true)
+ }
+
+ check(Set(p2, p6)) {
+ SquerylAuthModel.agentQuery(None, Seq("perm04"), None, lastName = None, 2, pageByName = true)
+ }
+ check(Set(p5)) {
+ SquerylAuthModel.agentQuery(None, Seq("perm04"), None, lastName = Some("agentD"), 2, pageByName = true)
+ }
+ }
+
+ test("Agent put") {
+ val notifier = new TestModelNotifier
+ createPermissionSet("perm01")
+ createPermissionSet("perm02")
+
+ val perms = Set("perm01", "perm02")
+
+ val puts = Seq(CoreUntypedTemplate(Option.empty[UUID], "agent01", AgentInfo(Some("secret"), Seq("perm01", "perm02"))))
+
+ val r1 = ("agent01", Set("perm01", "perm02"))
+ val all = Set(r1)
+
+ check(all) {
+ SquerylAuthModel.putAgents(notifier, puts)
+ }
+
+ checkPermissions("agent01", perms)
+
+ notifier.checkAgents(Set((Created, ("agent01", Set("perm01", "perm02")))))
+ }
+
+ test("Agent put swaps permissions") {
+ val notifier = new TestModelNotifier
+ createPermissionSet("perm01")
+ createPermissionSet("perm02")
+
+ {
+ val puts = Seq(CoreUntypedTemplate(Option.empty[UUID], "agent01", AgentInfo(Some("secret"), Seq("perm01"))))
+
+ val r1 = ("agent01", Set("perm01"))
+ val all = Set(r1)
+
+ check(all) {
+ SquerylAuthModel.putAgents(notifier, puts)
+ }
+ }
+
+ {
+ val puts = Seq(CoreUntypedTemplate(Option.empty[UUID], "agent01", AgentInfo(Some("secret"), Seq("perm02"))))
+
+ val r1 = ("agent01", Set("perm02"))
+ val all = Set(r1)
+
+ check(all) {
+ SquerylAuthModel.putAgents(notifier, puts)
+ }
+ }
+
+ notifier.checkAgents(Set(
+ (Created, ("agent01", Set("perm01"))),
+ (Updated, ("agent01", Set("perm02")))))
+ }
+
+ test("Agent put with non-existent permissions") {
+ val notifier = new TestModelNotifier
+ createPermissionSet("perm01")
+
+ intercept[ModelInputException] {
+ SquerylAuthModel.putAgents(notifier, Seq(CoreUntypedTemplate(Option.empty[UUID], "agent01", AgentInfo(Some("secret"), Seq("perm01", "nope")))))
+ }
+ }
+
+ test("Agent put battery") {
+
+ val notifier = new TestModelNotifier
+
+ val perm1 = createPermissionSet("perm01")
+ val perm2 = createPermissionSet("perm02")
+ val perm3 = createPermissionSet("perm03")
+ val agent01 = createAgent("agent01", "pass01")
+ val agent02 = createAgent("agent02")
+ addPermissionToAgent(perm1.id, agent01.id)
+ addPermissionToAgent(perm2.id, agent01.id)
+ addPermissionToAgent(perm1.id, agent02.id)
+ addPermissionToAgent(perm3.id, agent02.id)
+
+ val baseName = "agent"
+ val uuid04 = UUID.randomUUID()
+
+ val puts = Seq(
+ CoreUntypedTemplate[UUID, AgentInfo](Some(agent01.id), s"${baseName}11", AgentInfo(Some("pass01"), Seq("perm01"))),
+ CoreUntypedTemplate[UUID, AgentInfo](None, s"${baseName}02", AgentInfo(None, Seq("perm02", "perm03"))),
+ CoreUntypedTemplate[UUID, AgentInfo](None, s"${baseName}03", AgentInfo(Some("pass02"), Seq("perm01", "perm03"))),
+ CoreUntypedTemplate[UUID, AgentInfo](Some(uuid04), s"${baseName}04", AgentInfo(Some("pass44"), Seq("perm02"))))
+
+ val p1 = (s"${baseName}11", Set("perm01"))
+ val p2 = (s"${baseName}02", Set("perm02", "perm03"))
+ val p3 = (s"${baseName}03", Set("perm01", "perm03"))
+ val p4 = (s"${baseName}04", Set("perm02"))
+ val all = Set(p1, p2, p3, p4)
+
+ check(all) {
+ SquerylAuthModel.putAgents(notifier, puts)
+ }
+
+ notifier.checkAgents(
+ Set(
+ (Updated, (s"${baseName}11", Set("perm01"))),
+ (Updated, (s"${baseName}02", Set("perm02", "perm03"))),
+ (Created, (s"${baseName}03", Set("perm01", "perm03"))),
+ (Created, (s"${baseName}04", Set("perm02")))))
+ }
+
+ test("Agent delete") {
+ val notifier = new TestModelNotifier
+ val f = new AuthFixture
+ import f._
+
+ val toBeDeleted = Set(
+ ("agent01", Set("perm01", "perm02")),
+ ("agent02", Set("perm01", "perm03")))
+
+ check(toBeDeleted) {
+ SquerylAuthModel.deleteAgents(notifier, Seq(agent01.id, agent02.id))
+ }
+
+ val agents = ServicesSchema.agents.where(t => true === true).toSeq
+ agents.size should equal(1)
+ agents.head.id should equal(agent03.id)
+
+ val setsFor03 = ServicesSchema.agentSetJoins.where(j => true === true).map(j => (j.agentId, j.permissionSetId))
+ setsFor03.size should equal(2)
+ setsFor03.toSet should equal(Set((agent03.id, perm2.id), (agent03.id, perm3.id)))
+
+ notifier.checkAgents(Set(
+ (Deleted, ("agent01", Set("perm01", "perm02"))),
+ (Deleted, ("agent02", Set("perm01", "perm03")))))
+ }
+
+ test("Permission set query") {
+
+ val perm1 = createPermissionSet("perm01")
+ val perm2 = createPermissionSet("perm02")
+ val perm3 = createPermissionSet("perm03")
+ val perm4 = createPermissionSet("perm04")
+ val perm5 = createPermissionSet("perm05")
+
+ val emptyPerms = Set.empty[(Set[String], Set[String])]
+
+ val p1 = ("perm01", emptyPerms)
+ val p2 = ("perm02", emptyPerms)
+ val p3 = ("perm03", emptyPerms)
+ val p4 = ("perm04", emptyPerms)
+ val p5 = ("perm05", emptyPerms)
+ val all = Seq(p1, p2, p3, p4, p5)
+
+ checkOrder(all) {
+ SquerylAuthModel.permissionSetQuery(None, 100)
+ }
+
+ checkOrder(Seq(p1, p2)) {
+ SquerylAuthModel.permissionSetQuery(None, 2)
+ }
+ checkOrder(Seq(p3, p4)) {
+ SquerylAuthModel.permissionSetQuery(Some(perm2.id), 2)
+ }
+ checkOrder(Seq(p5)) {
+ SquerylAuthModel.permissionSetQuery(Some(perm4.id), 2)
+ }
+
+ }
+
+ test("Permission set put battery") {
+ val notifier = new TestModelNotifier
+ val baseName = "permissionSet"
+
+ val perm3 = createPermissionSet(s"${baseName}03", Seq(createPermission("res03", "get")))
+ val perm4 = createPermissionSet(s"${baseName}04", Seq(createPermission("res04", "get")))
+
+ val puts = Seq(
+ CoreUntypedTemplate[Long, PermissionSetInfo](None, s"${baseName}01", PermissionSetInfo(Seq(createPermission("res01", "get")))),
+ CoreUntypedTemplate[Long, PermissionSetInfo](None, s"${baseName}02", PermissionSetInfo(Seq(createPermission("res02", "get")))),
+ CoreUntypedTemplate[Long, PermissionSetInfo](None, s"${baseName}03", PermissionSetInfo(Seq(createPermission("res33", "get")))),
+ CoreUntypedTemplate[Long, PermissionSetInfo](Some(perm4.id), s"${baseName}44", PermissionSetInfo(Seq(createPermission("res04", "delete")))))
+
+ def simp(name: String, res: String, act: String) = (name, Set((Set(res), Set(act))))
+
+ val p1 = simp(s"${baseName}01", "res01", "get")
+ val p2 = simp(s"${baseName}02", "res02", "get")
+ val p3 = simp(s"${baseName}03", "res33", "get")
+ val p4 = simp(s"${baseName}44", "res04", "delete")
+ val all = Set(p1, p2, p3, p4)
+
+ check(all) {
+ SquerylAuthModel.putPermissionSets(notifier, puts)
+ }
+
+ notifier.checkPermissionSets(
+ Set(
+ (Created, p1),
+ (Created, p2),
+ (Updated, p3),
+ (Updated, p4)))
+ }
+
+ test("Permission set delete") {
+ val notifier = new TestModelNotifier
+ val f = new AuthFixture
+ import f._
+
+ val results = SquerylAuthModel.deletePermissionSets(notifier, Seq(perm1.id, perm2.id))
+ results.size should equal(2)
+ results.map(_.getName).toSet should equal(Set("perm01", "perm02"))
+
+ val setsFor03 = ServicesSchema.agentSetJoins.where(j => true === true).map(j => (j.agentId, j.permissionSetId))
+ setsFor03.size should equal(2)
+ setsFor03.toSet should equal(Set((agent02.id, perm3.id), (agent03.id, perm3.id)))
+
+ notifier.checkPermissionSetNames(Set(
+ (Deleted, "perm01"),
+ (Deleted, "perm02")))
+ }
+
+}
diff --git a/services/src/test/scala/io/greenbus/services/model/CommandModelTest.scala b/services/src/test/scala/io/greenbus/services/model/CommandModelTest.scala
new file mode 100644
index 0000000..c84124d
--- /dev/null
+++ b/services/src/test/scala/io/greenbus/services/model/CommandModelTest.scala
@@ -0,0 +1,255 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.model
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import io.greenbus.client.service.proto.Model.CommandCategory
+import java.util.UUID
+import io.greenbus.services.framework.{ Deleted, Created }
+import io.greenbus.client.service.proto.Commands.CommandLock
+
+@RunWith(classOf[JUnitRunner])
+class CommandModelTest extends ServiceTestBase {
+ import ModelTestHelpers._
+
+ class Fixture {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("command01", Seq("Command"))
+ val ent02 = createEntity("command02", Seq("Command"))
+ val ent03 = createEntity("command03", Seq("Command"))
+ val cmd01 = createCommand(ent01.id, "displayCommand01", CommandCategory.CONTROL)
+ val cmd02 = createCommand(ent02.id, "displayCommand02", CommandCategory.CONTROL)
+ val cmd03 = createCommand(ent03.id, "displayCommand03", CommandCategory.CONTROL)
+ val agent01 = createAgent("agent01")
+ val agent02 = createAgent("agent02")
+
+ val allAgentIds = Seq(agent01, agent02).map(_.id)
+ val allCmdIds = Seq(cmd01, cmd02, cmd03).map(_.entityId)
+
+ def hasSelect(cmd: UUID, agent: UUID, shouldBe: Boolean) {
+ SquerylCommandModel.agentHasSelect(cmd, agent) should equal(shouldBe)
+ }
+ def canIssue(cmd: UUID, agent: UUID, shouldBe: Boolean): Unit = {
+ SquerylCommandModel.agentCanIssueCommand(cmd, agent) should equal(shouldBe)
+ }
+
+ def permutations[A](f: (UUID, UUID) => A) = {
+ allAgentIds.foreach(a => allCmdIds.foreach(c => f(a, c)))
+ }
+ }
+
+ test("Select") {
+ val f = new Fixture
+ import f._
+
+ def allUnselected() {
+ permutations((a, c) => hasSelect(c, a, false))
+ permutations((a, c) => canIssue(c, a, true))
+ }
+
+ allUnselected()
+
+ val select = SquerylCommandModel.selectCommands(notifier, Seq(cmd01.entityId, cmd02.entityId), agent01.id, Long.MaxValue)
+
+ hasSelect(cmd01.entityId, agent01.id, true)
+ hasSelect(cmd02.entityId, agent01.id, true)
+ hasSelect(cmd03.entityId, agent01.id, false)
+ hasSelect(cmd01.entityId, agent02.id, false)
+ hasSelect(cmd02.entityId, agent02.id, false)
+ hasSelect(cmd03.entityId, agent02.id, false)
+
+ allCmdIds.foreach(c => canIssue(c, agent01.id, true))
+ Seq(cmd01, cmd02).map(_.entityId).foreach(c => canIssue(c, agent02.id, false))
+ canIssue(cmd03.entityId, agent02.id, true)
+
+ val deleted = SquerylCommandModel.deleteLocks(notifier, Seq(select.getId.getValue.toLong))
+
+ allUnselected()
+
+ notifier.checkCommandLocks(Set(
+ (Created, (CommandLock.AccessMode.ALLOWED, Set(cmd01.entityId, cmd02.entityId), agent01.id, Some(Long.MaxValue))),
+ (Deleted, (CommandLock.AccessMode.ALLOWED, Set(cmd01.entityId, cmd02.entityId), agent01.id, Some(Long.MaxValue)))))
+ }
+
+ test("Select bars later selects") {
+ val f = new Fixture
+ import f._
+
+ SquerylCommandModel.selectCommands(notifier, Seq(cmd01.entityId, cmd02.entityId), agent01.id, Long.MaxValue)
+
+ intercept[CommandLockException] {
+ SquerylCommandModel.selectCommands(notifier, Seq(cmd02.entityId, cmd03.entityId), agent02.id, Long.MaxValue)
+ }
+
+ }
+
+ test("Block bars later select") {
+ val f = new Fixture
+ import f._
+
+ val block = SquerylCommandModel.blockCommands(notifier, Seq(cmd01.entityId, cmd02.entityId), agent01.id)
+
+ intercept[CommandLockException] {
+ SquerylCommandModel.selectCommands(notifier, Seq(cmd02.entityId, cmd03.entityId), agent01.id, Long.MaxValue)
+ }
+
+ SquerylCommandModel.deleteLocks(notifier, Seq(block.getId.getValue.toLong))
+
+ SquerylCommandModel.selectCommands(notifier, Seq(cmd02.entityId, cmd03.entityId), agent01.id, Long.MaxValue)
+
+ hasSelect(cmd01.entityId, agent01.id, false)
+ hasSelect(cmd02.entityId, agent01.id, true)
+ hasSelect(cmd03.entityId, agent01.id, true)
+
+ canIssue(cmd01.entityId, agent01.id, true)
+ canIssue(cmd02.entityId, agent01.id, true)
+ canIssue(cmd03.entityId, agent01.id, true)
+
+ canIssue(cmd01.entityId, agent02.id, true)
+ canIssue(cmd02.entityId, agent02.id, false)
+ canIssue(cmd03.entityId, agent02.id, false)
+ }
+
+ test("Block occludes earlier select") {
+ val f = new Fixture
+ import f._
+
+ SquerylCommandModel.selectCommands(notifier, Seq(cmd01.entityId, cmd02.entityId), agent01.id, Long.MaxValue)
+
+ hasSelect(cmd01.entityId, agent01.id, true)
+ hasSelect(cmd02.entityId, agent01.id, true)
+ hasSelect(cmd03.entityId, agent01.id, false)
+
+ canIssue(cmd01.entityId, agent01.id, true)
+ canIssue(cmd02.entityId, agent01.id, true)
+ canIssue(cmd03.entityId, agent01.id, true)
+
+ canIssue(cmd01.entityId, agent02.id, false)
+ canIssue(cmd02.entityId, agent02.id, false)
+ canIssue(cmd03.entityId, agent02.id, true)
+
+ SquerylCommandModel.blockCommands(notifier, Seq(cmd02.entityId, cmd03.entityId), agent01.id)
+
+ hasSelect(cmd01.entityId, agent01.id, true)
+ hasSelect(cmd02.entityId, agent01.id, false)
+ hasSelect(cmd03.entityId, agent01.id, false)
+
+ canIssue(cmd01.entityId, agent01.id, true)
+ canIssue(cmd02.entityId, agent01.id, false)
+ canIssue(cmd03.entityId, agent01.id, false)
+
+ canIssue(cmd01.entityId, agent02.id, false)
+ canIssue(cmd02.entityId, agent02.id, false)
+ canIssue(cmd03.entityId, agent02.id, false)
+
+ notifier.checkCommandLocks(Set(
+ (Created, (CommandLock.AccessMode.ALLOWED, Set(cmd01.entityId, cmd02.entityId), agent01.id, Some(Long.MaxValue))),
+ (Created, (CommandLock.AccessMode.BLOCKED, Set(cmd02.entityId, cmd03.entityId), agent01.id, None))))
+ }
+
+ test("Lock query") {
+ val f = new Fixture
+ import f._
+
+ val select = SquerylCommandModel.selectCommands(notifier, Seq(cmd01.entityId, cmd02.entityId), agent01.id, Long.MaxValue)
+ val block = SquerylCommandModel.blockCommands(notifier, Seq(cmd03.entityId), agent01.id)
+
+ val lk1: (CommandLock.AccessMode, Set[UUID], UUID, Option[Long]) = (CommandLock.AccessMode.ALLOWED, Set(cmd01.entityId, cmd02.entityId), agent01.id, Some(Long.MaxValue))
+ val lk2: (CommandLock.AccessMode, Set[UUID], UUID, Option[Long]) = (CommandLock.AccessMode.BLOCKED, Set(cmd03.entityId), agent01.id, None)
+
+ check(Set(lk1)) {
+ SquerylCommandModel.lockQuery(Seq(cmd01.entityId), Nil, None, None, 300)
+ }
+ check(Set(lk2)) {
+ SquerylCommandModel.lockQuery(Seq(cmd03.entityId), Nil, None, None, 300)
+ }
+ }
+
+ test("Lock query overlap") {
+ val f = new Fixture
+ import f._
+
+ val select = SquerylCommandModel.selectCommands(notifier, Seq(cmd01.entityId, cmd02.entityId), agent01.id, Long.MaxValue)
+ val block = SquerylCommandModel.blockCommands(notifier, Seq(cmd02.entityId, cmd03.entityId), agent01.id)
+
+ val lk1: (CommandLock.AccessMode, Set[UUID], UUID, Option[Long]) = (CommandLock.AccessMode.ALLOWED, Set(cmd01.entityId, cmd02.entityId), agent01.id, Some(Long.MaxValue))
+ val lk2: (CommandLock.AccessMode, Set[UUID], UUID, Option[Long]) = (CommandLock.AccessMode.BLOCKED, Set(cmd02.entityId, cmd03.entityId), agent01.id, None)
+
+ check(Set(lk1, lk2)) {
+ SquerylCommandModel.lockQuery(Seq(cmd02.entityId), Nil, None, None, 300)
+ }
+ check(Set(lk2)) {
+ SquerylCommandModel.lockQuery(Seq(cmd02.entityId), Nil, Some(CommandLock.AccessMode.BLOCKED), None, 300)
+ }
+ }
+
+ test("Lock query agents") {
+ val f = new Fixture
+ import f._
+
+ val select = SquerylCommandModel.selectCommands(notifier, Seq(cmd01.entityId, cmd02.entityId), agent01.id, Long.MaxValue)
+ val block = SquerylCommandModel.blockCommands(notifier, Seq(cmd02.entityId, cmd03.entityId), agent02.id)
+
+ val lk1: (CommandLock.AccessMode, Set[UUID], UUID, Option[Long]) = (CommandLock.AccessMode.ALLOWED, Set(cmd01.entityId, cmd02.entityId), agent01.id, Some(Long.MaxValue))
+ val lk2: (CommandLock.AccessMode, Set[UUID], UUID, Option[Long]) = (CommandLock.AccessMode.BLOCKED, Set(cmd02.entityId, cmd03.entityId), agent02.id, None)
+
+ check(Set(lk1, lk2)) {
+ SquerylCommandModel.lockQuery(Nil, Seq(agent01.id, agent02.id), None, None, 300)
+ }
+ check(Set(lk2)) {
+ SquerylCommandModel.lockQuery(Nil, Seq(agent01.id, agent02.id), Some(CommandLock.AccessMode.BLOCKED), None, 300)
+ }
+ check(Set(lk1)) {
+ SquerylCommandModel.lockQuery(Nil, Seq(agent01.id), None, None, 300)
+ }
+ }
+
+ import UUIDHelpers._
+
+ test("Lock query paging") {
+ val f = new Fixture
+ import f._
+
+ val select1 = SquerylCommandModel.selectCommands(notifier, Seq(cmd01.entityId), agent01.id, Long.MaxValue)
+ val select2 = SquerylCommandModel.selectCommands(notifier, Seq(cmd02.entityId), agent01.id, Long.MaxValue)
+ val select3 = SquerylCommandModel.selectCommands(notifier, Seq(cmd03.entityId), agent01.id, Long.MaxValue)
+ val block1 = SquerylCommandModel.blockCommands(notifier, Seq(cmd01.entityId), agent02.id)
+ val block2 = SquerylCommandModel.blockCommands(notifier, Seq(cmd02.entityId), agent02.id)
+ val block3 = SquerylCommandModel.blockCommands(notifier, Seq(cmd03.entityId), agent02.id)
+
+ val lk1: (CommandLock.AccessMode, Set[UUID], UUID, Option[Long]) = (CommandLock.AccessMode.ALLOWED, Set(cmd01.entityId), agent01.id, Some(Long.MaxValue))
+ val lk2: (CommandLock.AccessMode, Set[UUID], UUID, Option[Long]) = (CommandLock.AccessMode.ALLOWED, Set(cmd02.entityId), agent01.id, Some(Long.MaxValue))
+ val lk3: (CommandLock.AccessMode, Set[UUID], UUID, Option[Long]) = (CommandLock.AccessMode.ALLOWED, Set(cmd03.entityId), agent01.id, Some(Long.MaxValue))
+ val lk4: (CommandLock.AccessMode, Set[UUID], UUID, Option[Long]) = (CommandLock.AccessMode.BLOCKED, Set(cmd01.entityId), agent02.id, None)
+ val lk5: (CommandLock.AccessMode, Set[UUID], UUID, Option[Long]) = (CommandLock.AccessMode.BLOCKED, Set(cmd02.entityId), agent02.id, None)
+ val lk6: (CommandLock.AccessMode, Set[UUID], UUID, Option[Long]) = (CommandLock.AccessMode.BLOCKED, Set(cmd03.entityId), agent02.id, None)
+
+ checkOrder(Seq(lk1, lk2)) {
+ SquerylCommandModel.lockQuery(Nil, Nil, None, None, 2)
+ }
+ checkOrder(Seq(lk3, lk4)) {
+ SquerylCommandModel.lockQuery(Nil, Nil, None, Some(protoIdToLong(select2.getId)), 2)
+ }
+ checkOrder(Seq(lk5, lk6)) {
+ SquerylCommandModel.lockQuery(Nil, Nil, None, Some(protoIdToLong(block1.getId)), 2)
+ }
+ }
+
+}
diff --git a/services/src/test/scala/io/greenbus/services/model/EntityModelTest.scala b/services/src/test/scala/io/greenbus/services/model/EntityModelTest.scala
new file mode 100644
index 0000000..916e6e9
--- /dev/null
+++ b/services/src/test/scala/io/greenbus/services/model/EntityModelTest.scala
@@ -0,0 +1,1397 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.model
+
+import java.util.UUID
+
+import io.greenbus.client.service.proto.Model.{ Entity, EntityEdge, EntityKeyValue, StoredValue }
+import io.greenbus.services.authz.EntityFilter
+import io.greenbus.services.data.{ EntityEdgeRow, EntityRow, EntityTypeRow, ServicesSchema }
+import io.greenbus.services.framework._
+import io.greenbus.services.model.EntityModel._
+import io.greenbus.services.model.UUIDHelpers._
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import org.squeryl.PrimitiveTypeMode._
+import org.squeryl.Query
+
+import scala.collection.JavaConversions._
+
+object EntityModelTest {
+ import ModelTestHelpers._
+
+ def bannedTypeTest(banned: String) {
+
+ val ent01 = createEntity("ent02", Seq("typeA"))
+
+ val ent03 = createEntity("ent03", Seq("typeC", banned))
+
+ intercept[ModelInputException] {
+ SquerylEntityModel.putEntities(NullModelNotifier, Nil, Seq(("ent01", Set("typeB", banned))))
+ }
+
+ intercept[ModelInputException] {
+ SquerylEntityModel.putEntities(NullModelNotifier, Nil, Seq(("ent02", Set("typeA", banned))))
+ }
+ intercept[ModelInputException] {
+ SquerylEntityModel.putEntities(NullModelNotifier, Seq((ent01.id, "ent02", Set("typeA", banned))), Nil)
+ }
+
+ intercept[ModelInputException] {
+ SquerylEntityModel.deleteEntities(NullModelNotifier, Seq(ent03.id))
+ }
+ }
+}
+
+@RunWith(classOf[JUnitRunner])
+class EntityModelTest extends ServiceTestBase {
+ import ModelTestHelpers._
+
+ def checkEdges(correct: Set[(UUID, UUID, String, Int)])(query: => Seq[EntityEdge]) {
+ val simplified = query.map(simplify)
+
+ def debugName(uuid: UUID): String = ServicesSchema.entities.where(t => t.id === uuid).single.name
+
+ if (simplified.toSet != correct) {
+ println("Correct: " + correct.map(tup => (debugName(tup._1), debugName(tup._2), tup._3)).mkString("(\n\t", "\n\t", "\n)"))
+ println("Result: " + simplified.map(tup => (debugName(tup._1), debugName(tup._2), tup._3)).toSet.mkString("(\n\t", "\n\t", "\n)"))
+ }
+ simplified.toSet should equal(correct)
+ simplified.size should equal(correct.size)
+ }
+
+ test("Key queries") {
+ val ent01 = createEntity("ent01", Seq("typeA", "typeB"))
+ val ent02 = createEntity("ent02", Seq("typeB", "typeC"))
+ val ent03 = createEntity("ent03", Seq("typeD"))
+ val ent04 = createEntity("ent04", Seq())
+
+ check(Set(("ent01", Set("typeA", "typeB")))) {
+ SquerylEntityModel.keyQuery(Nil, Seq("ent01"))
+ }
+ check(Set(("ent01", Set("typeA", "typeB")), ("ent04", Set()))) {
+ SquerylEntityModel.keyQuery(Nil, Seq("ent01", "ent04"))
+ }
+
+ check(Set(("ent03", Set("typeD")))) {
+ SquerylEntityModel.keyQuery(Seq(ent03.id), Nil)
+ }
+ check(Set(("ent02", Set("typeB", "typeC")), ("ent03", Set("typeD")))) {
+ SquerylEntityModel.keyQuery(Seq(ent02.id, ent03.id), Nil)
+ }
+
+ check(Set(("ent02", Set("typeB", "typeC")), ("ent03", Set("typeD")))) {
+ SquerylEntityModel.keyQuery(Seq(ent02.id), Seq("ent03"))
+ }
+ }
+
+ test("Full queries") {
+ val ent01 = createEntity("ent01", Seq("typeA", "typeB"))
+ val ent02 = createEntity("ent02", Seq("typeB", "typeC"))
+ val ent03 = createEntity("ent03", Seq("typeD"))
+ val ent04 = createEntity("ent04", Seq())
+
+ val e1 = ("ent01", Set("typeA", "typeB"))
+ val e2 = ("ent02", Set("typeB", "typeC"))
+ val e3 = ("ent03", Set("typeD"))
+ val e4 = ("ent04", Set.empty[String])
+
+ val all = Set(e1, e2, e3, e4)
+
+ check(all) {
+ SquerylEntityModel.fullQuery(TypeParams(Nil, Nil, Nil), None, None, 100, false, None)
+ }
+
+ check(Set(e1, e2)) {
+ SquerylEntityModel.fullQuery(TypeParams(Nil, Seq("typeB"), Nil), None, None, 100, false, None)
+ }
+ check(Set(e2)) {
+ SquerylEntityModel.fullQuery(TypeParams(Nil, Seq("typeB", "typeC"), Nil), None, None, 100, false, None)
+ }
+ check(Set()) {
+ SquerylEntityModel.fullQuery(TypeParams(Nil, Seq("typeA", "typeC"), Nil), None, None, 100, false, None)
+ }
+
+ check(Set(e3, e4)) {
+ SquerylEntityModel.fullQuery(TypeParams(Nil, Nil, Seq("typeB")), None, None, 100, false, None)
+ }
+ check(Set(e2, e4)) {
+ SquerylEntityModel.fullQuery(TypeParams(Nil, Nil, Seq("typeA", "typeD")), None, None, 100, false, None)
+ }
+
+ check(Set(e2)) {
+ SquerylEntityModel.fullQuery(TypeParams(Nil, Seq("typeB"), Seq("typeA")), None, None, 100, false, None)
+ }
+
+ check(Set(e1, e2)) {
+ SquerylEntityModel.fullQuery(TypeParams(Seq("typeB"), Nil, Nil), None, None, 100, false, None)
+ }
+ check(Set(e1, e3)) {
+ SquerylEntityModel.fullQuery(TypeParams(Seq("typeA", "typeD"), Nil, Nil), None, None, 100, false, None)
+ }
+ check(Set(e1)) {
+ SquerylEntityModel.fullQuery(TypeParams(Seq("typeB"), Nil, Seq("typeC")), None, None, 100, false, None)
+ }
+ check(Set(e2)) {
+ SquerylEntityModel.fullQuery(TypeParams(Seq("typeB"), Seq("typeC"), Nil), None, None, 100, false, None)
+ }
+
+ }
+
+ test("All paging") {
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA", "typeD"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+ val ent04 = createEntity("ent04", Seq("typeA", "typeD"))
+ val ent05 = createEntity("ent05", Seq("typeA"))
+ val ent06 = createEntity("ent06", Seq("typeA", "typeD"))
+ val ent07 = createEntity("ent07", Seq("typeA"))
+
+ def uuidOf(ent: Entity): UUID = ent.getUuid
+
+ val uuidFull = SquerylEntityModel.fullQuery(TypeParams(Nil, Nil, Nil), None, None, 100, false).map(uuidOf)
+ val nameFull = SquerylEntityModel.fullQuery(TypeParams(Nil, Nil, Nil), None, None, 100, true).map(uuidOf)
+
+ {
+ val page1 = SquerylEntityModel.fullQuery(TypeParams(Nil, Nil, Nil), None, None, 2, false)
+ val page2 = SquerylEntityModel.fullQuery(TypeParams(Nil, Nil, Nil), Some(page1.last.getUuid), None, 3, false)
+ val page3 = SquerylEntityModel.fullQuery(TypeParams(Nil, Nil, Nil), Some(page2.last.getUuid), None, 2, false)
+
+ (page1 ++ page2 ++ page3).map(uuidOf) should equal(uuidFull)
+ }
+ {
+ val page1 = SquerylEntityModel.fullQuery(TypeParams(Nil, Nil, Nil), None, None, 2, false)
+ val page2 = SquerylEntityModel.fullQuery(TypeParams(Nil, Nil, Nil), None, Some(page1.last.getName), 3, false)
+ val page3 = SquerylEntityModel.fullQuery(TypeParams(Nil, Nil, Nil), None, Some(page2.last.getName), 2, false)
+
+ (page1 ++ page2 ++ page3).map(uuidOf) should equal(uuidFull)
+ }
+
+ {
+ val page1 = SquerylEntityModel.fullQuery(TypeParams(Nil, Nil, Nil), None, None, 2, true)
+ val page2 = SquerylEntityModel.fullQuery(TypeParams(Nil, Nil, Nil), Some(page1.last.getUuid), None, 3, true)
+ val page3 = SquerylEntityModel.fullQuery(TypeParams(Nil, Nil, Nil), Some(page2.last.getUuid), None, 2, true)
+
+ (page1 ++ page2 ++ page3).map(uuidOf) should equal(nameFull)
+ }
+ {
+ val page1 = SquerylEntityModel.fullQuery(TypeParams(Nil, Nil, Nil), None, None, 2, true)
+ val page2 = SquerylEntityModel.fullQuery(TypeParams(Nil, Nil, Nil), None, Some(page1.last.getName), 3, true)
+ val page3 = SquerylEntityModel.fullQuery(TypeParams(Nil, Nil, Nil), None, Some(page2.last.getName), 2, true)
+
+ (page1 ++ page2 ++ page3).map(uuidOf) should equal(nameFull)
+ }
+
+ }
+
+ test("Types paging") {
+ val ent01 = createEntity("ent01", Seq("typeA", "typeD"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+ val ent04 = createEntity("ent04", Seq("typeA", "typeD"))
+ val ent05 = createEntity("ent05", Seq("typeA"))
+ val ent06 = createEntity("ent06", Seq("typeA"))
+ val ent07 = createEntity("ent07", Seq("typeA", "typeD"))
+ val other01 = createEntity("other01", Seq("typeB"))
+ val other02 = createEntity("other02", Seq("typeB"))
+
+ def uuidOf(ent: Entity): UUID = ent.getUuid
+
+ val full = SquerylEntityModel.fullQuery(TypeParams(Seq("typeA"), Nil, Nil), None, None, 100).map(uuidOf)
+
+ val page1 = SquerylEntityModel.fullQuery(TypeParams(Seq("typeA"), Nil, Nil), None, None, 2).map(uuidOf)
+ val page2 = SquerylEntityModel.fullQuery(TypeParams(Seq("typeA"), Nil, Nil), Some(page1.last), None, 3).map(uuidOf)
+ val page3 = SquerylEntityModel.fullQuery(TypeParams(Seq("typeA"), Nil, Nil), Some(page2.last), None, 2).map(uuidOf)
+
+ (page1 ++ page2 ++ page3) should equal(full)
+
+ }
+
+ test("Put one") {
+ val notifier = new TestModelNotifier
+
+ check(Set(("ent01", Set("typeA")))) {
+ SquerylEntityModel.putEntities(notifier, Nil, Seq(("ent01", Set("typeA"))))
+ }
+
+ val entRows = ServicesSchema.entities.where(t => true === true).toSeq
+ entRows.size should equal(1)
+ entRows.map(_.name).toSet should equal(Set("ent01"))
+ val uuid = entRows.head.id
+
+ val typeJoins = ServicesSchema.entityTypes.where(t => true === true)
+ typeJoins.size should equal(1)
+ typeJoins.toSet should equal(Set(EntityTypeRow(uuid, "typeA")))
+
+ val notes = notifier.getQueue(classOf[Entity]).toSeq.map(tup => (tup._1, simplify(tup._2)))
+ notes.size should equal(1)
+ notes.toSet should equal(Set((Created, ("ent01", Set("typeA")))))
+ }
+
+ test("Put one with multiple types") {
+ val notifier = new TestModelNotifier
+
+ check(Set(("ent01", Set("typeA", "typeB")))) {
+ SquerylEntityModel.putEntities(notifier, Nil, Seq(("ent01", Set("typeA", "typeB"))))
+ }
+
+ val entRows = ServicesSchema.entities.where(t => true === true).toSeq
+ entRows.size should equal(1)
+ entRows.map(_.name).toSet should equal(Set("ent01"))
+ val uuid = entRows.head.id
+
+ val typeJoins = ServicesSchema.entityTypes.where(t => true === true)
+ typeJoins.size should equal(2)
+ typeJoins.toSet should equal(Set(EntityTypeRow(uuid, "typeA"), EntityTypeRow(uuid, "typeB")))
+
+ val notes = notifier.getQueue(classOf[Entity]).toSeq.map(tup => (tup._1, simplify(tup._2)))
+ notes.size should equal(1)
+ notes.toSet should equal(Set((Created, ("ent01", Set("typeA", "typeB")))))
+ }
+
+ test("Put multiple") {
+ val notifier = new TestModelNotifier
+
+ check(Set(("ent01", Set("typeA", "typeB")), ("ent02", Set("typeB", "typeC")))) {
+ SquerylEntityModel.putEntities(notifier, Nil, Seq(("ent01", Set("typeA", "typeB")), ("ent02", Set("typeB", "typeC"))))
+ }
+
+ val entRows = ServicesSchema.entities.where(t => true === true).toSeq
+ entRows.size should equal(2)
+ entRows.map(_.name).toSet should equal(Set("ent01", "ent02"))
+ val entId01 = entRows.find(_.name == "ent01").get.id
+ val entId02 = entRows.find(_.name == "ent02").get.id
+
+ val typeJoins = ServicesSchema.entityTypes.where(t => true === true)
+ typeJoins.size should equal(4)
+ typeJoins.toSet should equal(
+ Set(EntityTypeRow(entId01, "typeA"), EntityTypeRow(entId01, "typeB"),
+ EntityTypeRow(entId02, "typeB"), EntityTypeRow(entId02, "typeC")))
+
+ val notifications = Set(
+ (Created, ("ent01", Set("typeA", "typeB"))),
+ (Created, ("ent02", Set("typeB", "typeC"))))
+
+ val notes = notifier.getQueue(classOf[Entity]).toSeq.map(tup => (tup._1, simplify(tup._2)))
+ notes.size should equal(2)
+ notes.toSet should equal(notifications)
+ }
+
+ test("Put multiple, add and delete types") {
+ val notifier = new TestModelNotifier
+
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA", "typeB"))
+ val ent03 = createEntity("ent03", Seq("typeB"))
+
+ val resulting = Set(
+ ("ent01", Set("typeA", "typeB")),
+ ("ent02", Set("typeB")),
+ ("ent03", Set("typeA")),
+ ("ent04", Set("typeC")))
+
+ val puts = Seq(
+ ("ent01", Set("typeA", "typeB")),
+ ("ent02", Set("typeB")),
+ ("ent03", Set("typeA")),
+ ("ent04", Set("typeC")))
+
+ check(resulting) {
+ SquerylEntityModel.putEntities(notifier, Nil, puts)
+ }
+
+ val names = Set("ent01", "ent02", "ent03", "ent04")
+ val entRows = ServicesSchema.entities.where(t => true === true).toSeq
+ entRows.size should equal(4)
+ entRows.map(_.name).toSet should equal(names)
+
+ val nameToId = entRows.map(row => (row.name, row.id)).toMap
+
+ nameToId("ent01") should equal(ent01.id)
+ nameToId("ent02") should equal(ent02.id)
+ nameToId("ent03") should equal(ent03.id)
+
+ val typeRows = Set(
+ EntityTypeRow(nameToId("ent01"), "typeA"),
+ EntityTypeRow(nameToId("ent01"), "typeB"),
+ EntityTypeRow(nameToId("ent02"), "typeB"),
+ EntityTypeRow(nameToId("ent03"), "typeA"),
+ EntityTypeRow(nameToId("ent04"), "typeC"))
+
+ val typeJoins = ServicesSchema.entityTypes.where(t => true === true).toSeq
+ typeJoins.size should equal(typeRows.size)
+ typeJoins.toSet should equal(typeRows)
+
+ val notifications = Set(
+ (Updated, ("ent01", Set("typeA", "typeB"))),
+ (Updated, ("ent02", Set("typeB"))),
+ (Updated, ("ent03", Set("typeA"))),
+ (Created, ("ent04", Set("typeC"))))
+
+ val notes = notifier.getQueue(classOf[Entity]).toSeq.map(tup => (tup._1, simplify(tup._2)))
+ notes.size should equal(4)
+ notes.toSet should equal(notifications)
+ }
+
+ test("Put one with predefined id") {
+ val notifier = new TestModelNotifier
+
+ val id = UUID.randomUUID()
+
+ check(Set(("ent01", Set("typeA")))) {
+ SquerylEntityModel.putEntities(notifier, Seq((id, "ent01", Set("typeA"))), Nil)
+ }
+
+ val entRows = ServicesSchema.entities.where(t => true === true).toSeq
+ entRows.size should equal(1)
+ entRows.map(_.name).toSet should equal(Set("ent01"))
+ val uuid = entRows.head.id
+
+ uuid should equal(id)
+
+ val typeJoins = ServicesSchema.entityTypes.where(t => true === true)
+ typeJoins.size should equal(1)
+ typeJoins.toSet should equal(Set(EntityTypeRow(uuid, "typeA")))
+
+ val notes = notifier.getQueue(classOf[Entity]).toSeq.map(tup => (tup._1, simplify(tup._2)))
+ notes.size should equal(1)
+ notes.toSet should equal(Set((Created, ("ent01", Set("typeA")))))
+ }
+
+ test("Put, rename, add and remove types") {
+ val notifier = new TestModelNotifier
+
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA", "typeB"))
+ val ent03 = createEntity("ent03", Seq("typeB"))
+
+ val ent04id = UUID.randomUUID()
+
+ val resulting = Set(
+ ("ent07", Set("typeA", "typeB")),
+ ("ent02", Set("typeB")),
+ ("ent03", Set("typeA")),
+ ("ent04", Set("typeC")))
+
+ val puts = Seq(
+ (ent01.id, "ent07", Set("typeA", "typeB")),
+ (ent02.id, "ent02", Set("typeB")),
+ (ent03.id, "ent03", Set("typeA")),
+ (ent04id, "ent04", Set("typeC")))
+
+ check(resulting) {
+ SquerylEntityModel.putEntities(notifier, puts, Nil)
+ }
+
+ val names = Set("ent07", "ent02", "ent03", "ent04")
+ val entRows = ServicesSchema.entities.where(t => true === true).toSeq
+ entRows.size should equal(4)
+ entRows.map(_.name).toSet should equal(names)
+
+ val nameToId = entRows.map(row => (row.name, row.id)).toMap
+
+ nameToId("ent07") should equal(ent01.id)
+ nameToId("ent02") should equal(ent02.id)
+ nameToId("ent03") should equal(ent03.id)
+ nameToId("ent04") should equal(ent04id)
+
+ val typeRows = Set(
+ EntityTypeRow(nameToId("ent07"), "typeA"),
+ EntityTypeRow(nameToId("ent07"), "typeB"),
+ EntityTypeRow(nameToId("ent02"), "typeB"),
+ EntityTypeRow(nameToId("ent03"), "typeA"),
+ EntityTypeRow(nameToId("ent04"), "typeC"))
+
+ val typeJoins = ServicesSchema.entityTypes.where(t => true === true).toSeq
+ typeJoins.size should equal(typeRows.size)
+ typeJoins.toSet should equal(typeRows)
+
+ val notifications = Set(
+ (Updated, (ent01.id, "ent07", Set("typeA", "typeB"))),
+ (Updated, (ent02.id, "ent02", Set("typeB"))),
+ (Updated, (ent03.id, "ent03", Set("typeA"))),
+ (Created, (ent04id, "ent04", Set("typeC"))))
+
+ val notes = notifier.getQueue(classOf[Entity]).toSeq.map(tup => (tup._1, (protoUUIDToUuid(tup._2.getUuid), tup._2.getName, tup._2.getTypesList.toSet)))
+ notes.size should equal(4)
+ notes.toSet should equal(notifications)
+ }
+
+ test("Put filtering") {
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA", "typeB"))
+
+ val filter = new EntityFilter {
+ protected def filterQuery: Query[EntityRow] = {
+ from(ServicesSchema.entities)(ent => where(ent.name === "ent01") select (ent))
+ }
+ }
+
+ intercept[ModelInputException] {
+ SquerylEntityModel.putEntities(NullModelNotifier, Nil, Seq(("ent03", Set("typeC"))), false, Some(filter))
+ }
+ intercept[ModelInputException] {
+ SquerylEntityModel.putEntities(NullModelNotifier, Nil, Seq(("ent01", Set("typeC")), ("ent03", Set("typeC"))), false, Some(filter))
+ }
+ intercept[ModelInputException] {
+ SquerylEntityModel.putEntities(NullModelNotifier, Nil, Seq(("ent02", Set("typeC"))), true, Some(filter))
+ }
+ intercept[ModelInputException] {
+ SquerylEntityModel.putEntities(NullModelNotifier, Nil, Seq(("ent01", Set("typeC")), ("ent02", Set("typeC"))), true, Some(filter))
+ }
+
+ check(Set(("ent01", Set("typeC")))) {
+ SquerylEntityModel.putEntities(NullModelNotifier, Nil, Seq(("ent01", Set("typeC"))), true, Some(filter))
+ }
+ }
+
+ test("Delete entity") {
+ val notifier = new TestModelNotifier
+
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA", "typeB"))
+ val ent04 = createEntity("ent04", Seq("typeA"))
+ val ent05 = createEntity("ent05", Seq("typeA"))
+ val edge1To2 = createEdge(ent01.id, ent02.id, "relatesTo", 1)
+ createEdge(ent01.id, ent03.id, "relatesTo", 2)
+ createEdge(ent01.id, ent04.id, "relatesTo", 3)
+ createEdge(ent01.id, ent05.id, "relatesTo", 4)
+ createEdge(ent02.id, ent03.id, "relatesTo", 1)
+ createEdge(ent02.id, ent04.id, "relatesTo", 2)
+ createEdge(ent02.id, ent05.id, "relatesTo", 3)
+ createEdge(ent03.id, ent04.id, "relatesTo", 1)
+ createEdge(ent03.id, ent05.id, "relatesTo", 2)
+ val edge4To5 = createEdge(ent04.id, ent05.id, "relatesTo", 1)
+ createEdge(ent01.id, ent03.id, "another", 1)
+ createEdge(ent03.id, ent05.id, "extraneous", 1)
+
+ val kv01 = createEntityKeyValue(ent03.id, "keyA", strToStored("value01").toByteArray)
+ val kv02 = createEntityKeyValue(ent05.id, "keyA", strToStored("value02").toByteArray)
+
+ val result = Set((ent03.name, Set("typeA", "typeB")))
+
+ check(result) {
+ SquerylEntityModel.deleteEntities(notifier, Seq(ent03.id))
+ }
+
+ val allEntities = ServicesSchema.entities.where(t => true === true).map(_.name)
+ allEntities.size should equal(4)
+ allEntities.toSet should equal(Set("ent01", "ent02", "ent04", "ent05"))
+
+ ServicesSchema.entityTypes.where(t => t.entityId === ent03.id).size should equal(0)
+
+ val allEdges = ServicesSchema.edges.where(t => true === true).toSeq
+ allEdges.size should equal(2)
+ allEdges.map(_.id).toSet should equal(Set(edge1To2.id, edge4To5.id))
+
+ val notifications = Set(
+ (Deleted, ("ent03", Set("typeA", "typeB"))))
+
+ val notes = notifier.getQueue(classOf[Entity]).toSeq.map(tup => (tup._1, simplify(tup._2)))
+ notes.size should equal(1)
+ notes.toSet should equal(notifications)
+
+ notifier.checkEntityEdges(Set(
+ (Deleted, (ent01.id, ent03.id, "relatesTo", 2)),
+ (Deleted, (ent01.id, ent04.id, "relatesTo", 3)),
+ (Deleted, (ent01.id, ent05.id, "relatesTo", 4)),
+ (Deleted, (ent02.id, ent03.id, "relatesTo", 1)),
+ (Deleted, (ent02.id, ent04.id, "relatesTo", 2)),
+ (Deleted, (ent02.id, ent05.id, "relatesTo", 3)),
+ (Deleted, (ent03.id, ent04.id, "relatesTo", 1)),
+ (Deleted, (ent03.id, ent05.id, "relatesTo", 2)),
+ (Deleted, (ent01.id, ent03.id, "another", 1)),
+ (Deleted, (ent03.id, ent05.id, "extraneous", 1))))
+
+ val v1 = (ent03.id, "keyA", "value01")
+
+ notifier.checkEntityKeyValues(Set((Deleted, v1)))
+ }
+
+ /*
+ 1 2
+ 3 4
+ */
+
+ test("Delete entity, only correct edges") {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+ val ent04 = createEntity("ent04", Seq("typeA"))
+ createEdge(ent01.id, ent03.id, "relatesTo", 1)
+ createEdge(ent02.id, ent04.id, "relatesTo", 1)
+ val edge2To3 = createEdge(ent02.id, ent03.id, "relatesTo", 1)
+
+ val result = Set(("ent01", Set("typeA")), ("ent04", Set("typeA")))
+
+ check(result) {
+ SquerylEntityModel.deleteEntities(notifier, Seq(ent01.id, ent04.id))
+ }
+
+ val allEntities = ServicesSchema.entities.where(t => true === true).map(_.name)
+ allEntities.size should equal(2)
+ allEntities.toSet should equal(Set("ent02", "ent03"))
+
+ val allEdges = ServicesSchema.edges.where(t => true === true).toSeq
+ allEdges.size should equal(1)
+ allEdges.map(_.id).toSet should equal(Set(edge2To3.id))
+
+ notifier.checkEntities(Set(
+ (Deleted, ("ent01", Set("typeA"))),
+ (Deleted, ("ent04", Set("typeA")))))
+
+ notifier.checkEntityEdges(Set(
+ (Deleted, (ent01.id, ent03.id, "relatesTo", 1)),
+ (Deleted, (ent02.id, ent04.id, "relatesTo", 1))))
+ }
+
+ test("Put edge, simple") {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+
+ val resultEdges = Set(
+ (ent01.id, ent02.id, "relatesTo", 1),
+ (ent01.id, ent03.id, "relatesTo", 1))
+
+ checkEdges(resultEdges) {
+ SquerylEntityModel.putEdges(notifier, Seq((ent01.id, ent02.id), (ent01.id, ent03.id)), "relatesTo")
+ }
+
+ val allEdges = ServicesSchema.edges.where(t => true === true).toSeq
+
+ allEdges.size should equal(2)
+ allEdges.map(row => (row.parentId, row.childId, row.relationship, row.distance)).toSet should equal(resultEdges)
+
+ notifier.checkEntityEdges(resultEdges.map(e => (Created, e)))
+ }
+
+ test("Put edge, simple, with existing") {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+ val edge01 = createEdge(ent01.id, ent03.id, "relatesTo", 1)
+
+ val resultEdges = Set(
+ (ent01.id, ent02.id, "relatesTo", 1),
+ (ent01.id, ent03.id, "relatesTo", 1))
+
+ checkEdges(resultEdges) {
+ SquerylEntityModel.putEdges(notifier, Seq((ent01.id, ent02.id), (ent01.id, ent03.id)), "relatesTo")
+ }
+
+ val allEdges = ServicesSchema.edges.where(t => true === true).toSeq
+ allEdges.size should equal(2)
+ val toMatch = allEdges.map(row => (row.parentId, row.childId, row.relationship, row.distance)).toSet
+ toMatch should equal(resultEdges)
+
+ notifier.checkEntityEdges(Set(
+ (Created, (ent01.id, ent02.id, "relatesTo", 1))))
+ }
+
+ test("Put edge, simple, not confused by different edge type") {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+ val edge01 = createEdge(ent01.id, ent03.id, "totallyUnrelated", 1)
+
+ val resultEdges = Set(
+ (ent01.id, ent02.id, "relatesTo", 1),
+ (ent01.id, ent03.id, "relatesTo", 1))
+
+ checkEdges(resultEdges) {
+ SquerylEntityModel.putEdges(notifier, Seq((ent01.id, ent02.id), (ent01.id, ent03.id)), "relatesTo")
+ }
+
+ val allEdges = ServicesSchema.edges.where(t => t.relationship === "relatesTo").toSeq
+ allEdges.size should equal(resultEdges.size)
+ val toMatch = allEdges.map(row => (row.parentId, row.childId, row.relationship, row.distance)).toSet
+ toMatch should equal(resultEdges)
+
+ notifier.checkEntityEdges(resultEdges.map(e => (Created, e)))
+ }
+
+ test("Put edges with derived edges") {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+ val ent04 = createEntity("ent04", Seq("typeA"))
+ val edge01To02 = createEdge(ent01.id, ent02.id, "relatesTo", 1)
+ val edge03To04 = createEdge(ent03.id, ent04.id, "relatesTo", 1)
+
+ val resultEdges = Set(
+ (ent02.id, ent03.id, "relatesTo", 1),
+ (ent02.id, ent04.id, "relatesTo", 2),
+ (ent01.id, ent03.id, "relatesTo", 2),
+ (ent01.id, ent04.id, "relatesTo", 3))
+
+ checkEdges(resultEdges) {
+ SquerylEntityModel.putEdges(notifier, Seq((ent02.id, ent03.id)), "relatesTo")
+ }
+
+ val allEdges = ServicesSchema.edges.where(t => true === true).toSeq
+ allEdges.size should equal(6)
+ val toMatch = allEdges
+ .filterNot(a => Seq(edge01To02, edge03To04).exists(_ == a))
+ .map(row => (row.parentId, row.childId, row.relationship, row.distance))
+ .toSet
+
+ toMatch should equal(resultEdges)
+
+ notifier.checkEntityEdges(resultEdges.map(e => (Created, e)))
+ }
+
+ test("Put edge, batch derived") {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+
+ val resultEdges = Set(
+ (ent01.id, ent02.id, "relatesTo", 1),
+ (ent02.id, ent03.id, "relatesTo", 1),
+ (ent01.id, ent03.id, "relatesTo", 2))
+
+ checkEdges(resultEdges) {
+ SquerylEntityModel.putEdges(notifier, Seq((ent01.id, ent02.id), (ent02.id, ent03.id)), "relatesTo")
+ }
+
+ val allEdges = ServicesSchema.edges.where(t => true === true).toSeq
+
+ allEdges.size should equal(resultEdges.size)
+ allEdges.map(row => (row.parentId, row.childId, row.relationship, row.distance)).toSet should equal(resultEdges)
+
+ notifier.checkEntityEdges(resultEdges.map(e => (Created, e)))
+ }
+
+ test("Put edge, batch derived extended") {
+ /* 1 - 2 [] 3 [] 4 - 5 - 6 */
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+ val ent04 = createEntity("ent04", Seq("typeA"))
+ val ent05 = createEntity("ent05", Seq("typeA"))
+ val ent06 = createEntity("ent06", Seq("typeA"))
+
+ val extant = List(
+ (ent01.id, ent02.id, "relatesTo", 1),
+ (ent04.id, ent05.id, "relatesTo", 1),
+ (ent04.id, ent06.id, "relatesTo", 2),
+ (ent05.id, ent06.id, "relatesTo", 1))
+
+ extant.foreach { case (parent, child, relation, int) => createEdge(parent, child, relation, int) }
+
+ val allTups = Set(
+ (ent01.id, ent02.id, "relatesTo", 1),
+ (ent01.id, ent03.id, "relatesTo", 2),
+ (ent01.id, ent04.id, "relatesTo", 3),
+ (ent01.id, ent05.id, "relatesTo", 4),
+ (ent01.id, ent06.id, "relatesTo", 5),
+ (ent02.id, ent03.id, "relatesTo", 1),
+ (ent02.id, ent04.id, "relatesTo", 2),
+ (ent02.id, ent05.id, "relatesTo", 3),
+ (ent02.id, ent06.id, "relatesTo", 4),
+ (ent03.id, ent04.id, "relatesTo", 1),
+ (ent03.id, ent05.id, "relatesTo", 2),
+ (ent03.id, ent06.id, "relatesTo", 3),
+ (ent04.id, ent05.id, "relatesTo", 1),
+ (ent04.id, ent06.id, "relatesTo", 2),
+ (ent05.id, ent06.id, "relatesTo", 1))
+
+ val resultEdges = allTups &~ extant.toSet
+
+ checkEdges(resultEdges) {
+ SquerylEntityModel.putEdges(notifier, Seq((ent02.id, ent03.id), (ent03.id, ent04.id)), "relatesTo")
+ }
+
+ val allEdges = ServicesSchema.edges.where(t => true === true).toSeq
+
+ allEdges.size should equal(allTups.size)
+ allEdges.map(row => (row.parentId, row.childId, row.relationship, row.distance)).toSet should equal(allTups)
+
+ notifier.checkEntityEdges(resultEdges.map(e => (Created, e)))
+ }
+
+ test("Put edge, batch derived two holes") {
+ /* 1 - 2 [] 3 - 4 [] 5 - 6 */
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+ val ent04 = createEntity("ent04", Seq("typeA"))
+ val ent05 = createEntity("ent05", Seq("typeA"))
+ val ent06 = createEntity("ent06", Seq("typeA"))
+
+ val extant = List(
+ (ent01.id, ent02.id, "relatesTo", 1),
+ (ent03.id, ent04.id, "relatesTo", 1),
+ (ent05.id, ent06.id, "relatesTo", 1))
+
+ extant.foreach { case (parent, child, relation, int) => createEdge(parent, child, relation, int) }
+
+ val allTups = Set(
+ (ent01.id, ent02.id, "relatesTo", 1),
+ (ent01.id, ent03.id, "relatesTo", 2),
+ (ent01.id, ent04.id, "relatesTo", 3),
+ (ent01.id, ent05.id, "relatesTo", 4),
+ (ent01.id, ent06.id, "relatesTo", 5),
+ (ent02.id, ent03.id, "relatesTo", 1),
+ (ent02.id, ent04.id, "relatesTo", 2),
+ (ent02.id, ent05.id, "relatesTo", 3),
+ (ent02.id, ent06.id, "relatesTo", 4),
+ (ent03.id, ent04.id, "relatesTo", 1),
+ (ent03.id, ent05.id, "relatesTo", 2),
+ (ent03.id, ent06.id, "relatesTo", 3),
+ (ent04.id, ent05.id, "relatesTo", 1),
+ (ent04.id, ent06.id, "relatesTo", 2),
+ (ent05.id, ent06.id, "relatesTo", 1))
+
+ val resultEdges = allTups &~ extant.toSet
+
+ checkEdges(resultEdges) {
+ SquerylEntityModel.putEdges(notifier, Seq((ent02.id, ent03.id), (ent04.id, ent05.id)), "relatesTo")
+ }
+
+ val allEdges = ServicesSchema.edges.where(t => true === true).toSeq
+
+ allEdges.size should equal(allTups.size)
+ allEdges.map(row => (row.parentId, row.childId, row.relationship, row.distance)).toSet should equal(allTups)
+
+ notifier.checkEntityEdges(resultEdges.map(e => (Created, e)))
+ }
+
+ /*
+ 1
+ 2 3
+ 4
+ */
+ test("Diamond, from above") {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+ val ent04 = createEntity("ent04", Seq("typeA"))
+ createEdge(ent01.id, ent02.id, "relatesTo", 1)
+ createEdge(ent01.id, ent03.id, "relatesTo", 1)
+ createEdge(ent02.id, ent04.id, "relatesTo", 1)
+ createEdge(ent01.id, ent04.id, "relatesTo", 2)
+
+ intercept[DiamondException] {
+ SquerylEntityModel.putEdges(notifier, Seq((ent03.id, ent04.id)), "relatesTo")
+ }
+ }
+ test("Diamond, from below") {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+ val ent04 = createEntity("ent04", Seq("typeA"))
+ createEdge(ent01.id, ent03.id, "relatesTo", 1)
+ createEdge(ent02.id, ent04.id, "relatesTo", 1)
+ createEdge(ent03.id, ent04.id, "relatesTo", 1)
+ createEdge(ent01.id, ent04.id, "relatesTo", 2)
+
+ intercept[DiamondException] {
+ SquerylEntityModel.putEdges(notifier, Seq((ent01.id, ent02.id)), "relatesTo")
+ }
+ }
+ test("Diamond, from above, different relation ok") {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+ val ent04 = createEntity("ent04", Seq("typeA"))
+ createEdge(ent01.id, ent02.id, "relatesTo", 1)
+ createEdge(ent01.id, ent03.id, "relatesTo", 1)
+ createEdge(ent02.id, ent04.id, "relatesTo", 1)
+ createEdge(ent01.id, ent04.id, "relatesTo", 2)
+
+ val results = SquerylEntityModel.putEdges(notifier, Seq((ent03.id, ent04.id)), "totallyUnrelated")
+ results.toSeq.size > 0 should equal(true)
+ }
+
+ test("Delete edges") {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+ val ent04 = createEntity("ent04", Seq("typeA"))
+ val edge01To02 = createEdge(ent01.id, ent02.id, "relatesTo", 1)
+ val edge01To03 = createEdge(ent01.id, ent03.id, "relatesTo", 1)
+ val edge01To04 = createEdge(ent01.id, ent04.id, "relatesTo", 1)
+
+ val resultEdges = Set(
+ (ent01.id, ent02.id, "relatesTo", 1),
+ (ent01.id, ent03.id, "relatesTo", 1))
+
+ checkEdges(resultEdges) {
+ SquerylEntityModel.deleteEdges(notifier, Seq((ent01.id, ent02.id), (ent01.id, ent03.id)), "relatesTo")
+ }
+
+ val allEdges = ServicesSchema.edges.where(t => true === true).toSeq
+ allEdges.size should equal(1)
+ allEdges.head.id should equal(edge01To04.id)
+
+ notifier.checkEntityEdges(resultEdges.map(e => (Deleted, e)))
+ }
+
+ /*
+ 5 1 2
+ 3 4 6
+ */
+ test("Relation flat query") {
+ val ent01 = createEntity("ent01", Seq("typeA", "top"))
+ val ent02 = createEntity("ent02", Seq("typeA", "top"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+ val ent04 = createEntity("ent04", Seq("typeA", "special"))
+ val ent05 = createEntity("ent05", Seq("typeA"))
+ val ent06 = createEntity("ent06", Seq("typeA"))
+ createEdge(ent01.id, ent03.id, "relatesTo", 1)
+ createEdge(ent01.id, ent04.id, "relatesTo", 1)
+ createEdge(ent02.id, ent04.id, "relatesTo", 1)
+ createEdge(ent02.id, ent06.id, "unrelated", 1)
+
+ val results = Set(
+ ("ent03", Set("typeA")),
+ ("ent04", Set("typeA", "special")))
+
+ check(results) {
+ SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id, ent02.id), "relatesTo", descendantOf = true, Seq(), None, None, None, 100, false, filter = None)
+ }
+ check(results) {
+ SquerylEntityModel.namesRelationFlatQuery(Seq("ent01", "ent02"), "relatesTo", descendantOf = true, Seq(), None, None, None, 100, false, filter = None)
+ }
+ check(results) {
+ SquerylEntityModel.typesRelationFlatQuery(Seq("top"), "relatesTo", descendantOf = true, Seq(), None, None, None, 100, false, filter = None)
+ }
+
+ check(Set(("ent04", Set("typeA", "special")))) {
+ SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id, ent02.id), "relatesTo", descendantOf = true, Seq("special"), None, None, None, 100, false, filter = None)
+ }
+
+ val reverse = Set(
+ ("ent01", Set("typeA", "top")),
+ ("ent02", Set("typeA", "top")))
+
+ check(reverse) {
+ SquerylEntityModel.idsRelationFlatQuery(Seq(ent03.id, ent04.id), "relatesTo", descendantOf = false, Seq(), None, None, None, 100, false, filter = None)
+ }
+ }
+
+ test("Relation flat query, depth limit") {
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA", "typeB"))
+ createEdge(ent01.id, ent02.id, "relatesTo", 1)
+ createEdge(ent01.id, ent03.id, "relatesTo", 2)
+ createEdge(ent02.id, ent03.id, "relatesTo", 1)
+
+ val m1 = ("ent02", Set("typeA"))
+ val m2 = ("ent03", Set("typeA", "typeB"))
+
+ val all = Set(m1, m2)
+
+ val results = Set(m1)
+
+ check(all) {
+ SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id), "relatesTo", descendantOf = true, Seq(), Some(100), None, None, 100, false, filter = None)
+ }
+ check(all) {
+ SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id), "relatesTo", descendantOf = true, Seq(), None, None, None, 100, false, filter = None)
+ }
+ check(all) {
+ SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id), "relatesTo", descendantOf = true, Seq("typeA"), Some(2), None, None, 100, false, filter = None)
+ }
+ check(all) {
+ SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id), "relatesTo", descendantOf = true, Seq(), Some(Int.MaxValue), None, None, 100, false, filter = None)
+ }
+
+ check(Set(m2)) {
+ SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id), "relatesTo", descendantOf = true, Seq("typeB"), None, None, None, 100, false, filter = None)
+ }
+ check(Set(m2)) {
+ SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id), "relatesTo", descendantOf = true, Seq("typeB"), Some(2), None, None, 100, false, filter = None)
+ }
+
+ check(results) {
+ SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id), "relatesTo", descendantOf = true, Seq(), Some(1), None, None, 100, false, filter = None)
+ }
+
+ check(Set()) {
+ SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id), "relatesTo", descendantOf = true, Seq("typeB"), Some(1), None, None, 100, false, filter = None)
+ }
+ check(Set()) {
+ SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id), "relatesTo", descendantOf = true, Seq("typeC"), None, None, None, 100, false, filter = None)
+ }
+ }
+
+ test("Relation flat query, paging") {
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+ val ent04 = createEntity("ent04", Seq("typeA"))
+ createEdge(ent01.id, ent02.id, "relatesTo", 1)
+ createEdge(ent01.id, ent03.id, "relatesTo", 1)
+ createEdge(ent01.id, ent04.id, "relatesTo", 1)
+
+ {
+ val first = SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id), "relatesTo", descendantOf = true, Seq(), None, None, None, 1, false, filter = None)
+ val second = SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id), "relatesTo", descendantOf = true, Seq(), None, Some(first.head.getUuid), None, 1, false, filter = None)
+ val third = SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id), "relatesTo", descendantOf = true, Seq(), None, Some(second.head.getUuid), None, 1, false, filter = None)
+
+ val all = (first ++ second ++ third).map(_.getName)
+ all.size should equal(3)
+ all.toSet should equal(Set("ent02", "ent03", "ent04"))
+ }
+
+ {
+ val first = SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id), "relatesTo", descendantOf = true, Seq(), None, None, None, 1, false, filter = None)
+ val second = SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id), "relatesTo", descendantOf = true, Seq(), None, None, Some(first.last.getName), 1, false, filter = None)
+ val third = SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id), "relatesTo", descendantOf = true, Seq(), None, None, Some(second.last.getName), 1, false, filter = None)
+
+ val all = (first ++ second ++ third).map(_.getName)
+ all.size should equal(3)
+ all.toSet should equal(Set("ent02", "ent03", "ent04"))
+ }
+
+ {
+ val first = SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id), "relatesTo", descendantOf = true, Seq(), None, None, None, 1, true, filter = None)
+ val second = SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id), "relatesTo", descendantOf = true, Seq(), None, Some(first.head.getUuid), None, 1, true, filter = None)
+ val third = SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id), "relatesTo", descendantOf = true, Seq(), None, Some(second.head.getUuid), None, 1, true, filter = None)
+
+ val all = (first ++ second ++ third).map(_.getName)
+ all should equal(Seq("ent02", "ent03", "ent04"))
+ }
+
+ {
+ val first = SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id), "relatesTo", descendantOf = true, Seq(), None, None, None, 1, true, filter = None)
+ val second = SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id), "relatesTo", descendantOf = true, Seq(), None, None, Some(first.last.getName), 1, true, filter = None)
+ val third = SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id), "relatesTo", descendantOf = true, Seq(), None, None, Some(second.last.getName), 1, true, filter = None)
+
+ val all = (first ++ second ++ third).map(_.getName)
+ all should equal(Seq("ent02", "ent03", "ent04"))
+ }
+ }
+
+ /*
+ 1x 2
+ 5 3x 4
+ */
+ test("Relation flat query filtering") {
+ val ent01 = createEntity("ent01", Seq("top", "banned"))
+ val ent02 = createEntity("ent02", Seq("top"))
+ val ent03 = createEntity("ent03", Seq("banned"))
+ val ent04 = createEntity("ent04", Seq("typeA"))
+ val ent05 = createEntity("ent05", Seq("typeA"))
+ createEdge(ent01.id, ent03.id, "relatesTo", 1)
+ createEdge(ent01.id, ent05.id, "relatesTo", 1)
+ createEdge(ent02.id, ent03.id, "relatesTo", 1)
+ createEdge(ent02.id, ent04.id, "relatesTo", 1)
+
+ val entFilter = new EntityFilter {
+ protected def filterQuery: Query[EntityRow] = {
+ from(ServicesSchema.entities)(ent =>
+ where(notExists(
+ from(ServicesSchema.entityTypes)(t =>
+ where(t.entType === "banned" and t.entityId === ent.id)
+ select (t))))
+ select (ent))
+ }
+ }
+
+ val results = Set(
+ ("ent04", Set("typeA")))
+
+ check(results) {
+ SquerylEntityModel.idsRelationFlatQuery(Seq(ent01.id, ent02.id), "relatesTo", descendantOf = true, Seq(), None, None, None, 100, false, Some(entFilter))
+ }
+ check(results) {
+ SquerylEntityModel.namesRelationFlatQuery(Seq("ent01", "ent02"), "relatesTo", descendantOf = true, Seq(), None, None, None, 100, false, Some(entFilter))
+ }
+ check(results) {
+ SquerylEntityModel.typesRelationFlatQuery(Seq("top"), "relatesTo", descendantOf = true, Seq(), None, None, None, 100, false, Some(entFilter))
+ }
+ }
+
+ test("Edge query") {
+ val ent01 = createEntity("ent01", Seq("typeA", "top"))
+ val ent02 = createEntity("ent02", Seq("typeA", "banned"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+ val ent04 = createEntity("ent04", Seq("typeA", "banned"))
+ val ent05 = createEntity("ent05", Seq("typeA"))
+ val ent06 = createEntity("ent06", Seq("typeA"))
+
+ val eg1 = (ent01.id, ent03.id, "relatesTo", 1)
+ val eg2 = (ent01.id, ent04.id, "relatesTo", 1)
+ val eg3 = (ent02.id, ent04.id, "relatesTo", 1)
+ val eg4 = (ent02.id, ent06.id, "unrelated", 1)
+ val eg5 = (ent03.id, ent05.id, "rules", 1)
+ val eg6 = (ent05.id, ent06.id, "rules", 1)
+ val eg7 = (ent03.id, ent06.id, "rules", 2)
+
+ val all = Set(eg1, eg2, eg3, eg4, eg5, eg6, eg7)
+
+ all.foreach(eg => createEdge(eg._1, eg._2, eg._3, eg._4))
+
+ checkEdges(all) {
+ SquerylEntityModel.edgeQuery(Nil, Nil, Nil, None, None, 100, None)
+ }
+ checkEdges(Set(eg3)) {
+ SquerylEntityModel.edgeQuery(Seq(ent02.id), Seq("relatesTo"), Nil, None, None, 100, None)
+ }
+ checkEdges(Set(eg3, eg4)) {
+ SquerylEntityModel.edgeQuery(Seq(ent02.id), Seq("relatesTo", "unrelated"), Nil, None, None, 100, None)
+ }
+ checkEdges(Set(eg1, eg2, eg3)) {
+ SquerylEntityModel.edgeQuery(Seq(ent01.id, ent02.id), Seq("relatesTo"), Nil, None, None, 100, None)
+ }
+ checkEdges(Set(eg1)) {
+ SquerylEntityModel.edgeQuery(Seq(ent01.id, ent02.id), Seq("relatesTo"), Seq(ent03.id), None, None, 100, None)
+ }
+ checkEdges(Set(eg1)) {
+ SquerylEntityModel.edgeQuery(Nil, Seq("relatesTo"), Seq(ent03.id), None, None, 100, None)
+ }
+ checkEdges(Set(eg1)) {
+ SquerylEntityModel.edgeQuery(Nil, Nil, Seq(ent03.id), None, None, 100, None)
+ }
+ checkEdges(Set(eg1, eg5)) {
+ SquerylEntityModel.edgeQuery(Nil, Nil, Seq(ent03.id, ent05.id), None, None, 100, None)
+ }
+
+ checkEdges(Set(eg5, eg7)) {
+ SquerylEntityModel.edgeQuery(Seq(ent03.id), Seq("rules"), Nil, None, None, 100, None)
+ }
+ checkEdges(Set(eg5)) {
+ SquerylEntityModel.edgeQuery(Seq(ent03.id), Seq("rules"), Nil, Some(1), None, 100, None)
+ }
+
+ val first = SquerylEntityModel.edgeQuery(Nil, Nil, Nil, None, None, 3, None)
+ val second = SquerylEntityModel.edgeQuery(Nil, Nil, Nil, None, Some(first.last.getId.getValue.toLong), 2, None)
+ val third = SquerylEntityModel.edgeQuery(Nil, Nil, Nil, None, Some(second.last.getId.getValue.toLong), 2, None)
+
+ val paged = first ++ second ++ third
+ paged.size should equal(all.size)
+ val pageSimple = paged.map(proto => (protoUUIDToUuid(proto.getParent), protoUUIDToUuid(proto.getChild), proto.getRelationship, proto.getDistance))
+ pageSimple.toSet should equal(all)
+
+ val entFilter = new EntityFilter {
+ protected def filterQuery: Query[EntityRow] = {
+ from(ServicesSchema.entities)(ent =>
+ where(notExists(
+ from(ServicesSchema.entityTypes)(t =>
+ where(t.entType === "banned" and t.entityId === ent.id)
+ select (t))))
+ select (ent))
+ }
+ }
+
+ checkEdges(Set(eg1, eg5, eg6, eg7)) {
+ SquerylEntityModel.edgeQuery(Nil, Nil, Nil, None, None, 100, Some(entFilter))
+ }
+ checkEdges(Set(eg1)) {
+ SquerylEntityModel.edgeQuery(Seq(ent01.id), Seq("relatesTo"), Nil, None, None, 100, Some(entFilter))
+ }
+ checkEdges(Set(eg6, eg7)) {
+ SquerylEntityModel.edgeQuery(Nil, Nil, Seq(ent06.id), None, None, 100, Some(entFilter))
+ }
+
+ }
+
+ test("Edge query paging") {
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+ val ent04 = createEntity("ent04", Seq("typeA"))
+ val ent05 = createEntity("ent05", Seq("typeA"))
+ val ent06 = createEntity("ent06", Seq("typeA"))
+ val ent07 = createEntity("ent07", Seq("typeA"))
+ val ent08 = createEntity("ent08", Seq("typeA"))
+
+ val relation = "relatesTo"
+ val eg1 = (ent01.id, ent02.id, relation, 1)
+ val eg2 = (ent01.id, ent03.id, relation, 1)
+ val eg3 = (ent01.id, ent04.id, relation, 1)
+ val eg4 = (ent01.id, ent05.id, relation, 1)
+ val eg5 = (ent01.id, ent06.id, relation, 1)
+ val eg6 = (ent01.id, ent07.id, relation, 1)
+ val eg7 = (ent01.id, ent08.id, relation, 1)
+
+ val all = Set(eg1, eg2, eg3, eg4, eg5, eg6, eg7)
+
+ ServicesSchema.edges.insert(EntityEdgeRow(3, ent01.id, ent02.id, relation, 1))
+ ServicesSchema.edges.insert(EntityEdgeRow(4, ent01.id, ent03.id, relation, 1))
+ ServicesSchema.edges.insert(EntityEdgeRow(6, ent01.id, ent04.id, relation, 1))
+ ServicesSchema.edges.insert(EntityEdgeRow(2, ent01.id, ent05.id, relation, 1))
+ ServicesSchema.edges.insert(EntityEdgeRow(7, ent01.id, ent06.id, relation, 1))
+ ServicesSchema.edges.insert(EntityEdgeRow(5, ent01.id, ent07.id, relation, 1))
+ ServicesSchema.edges.insert(EntityEdgeRow(1, ent01.id, ent08.id, relation, 1))
+
+ checkEdges(all) {
+ SquerylEntityModel.edgeQuery(Nil, Nil, Nil, None, None, 100, None)
+ }
+
+ val r1 = SquerylEntityModel.edgeQuery(Seq(ent01.id), Nil, Nil, None, None, 2, None).toVector
+ r1.size should equal(2)
+ val r2 = SquerylEntityModel.edgeQuery(Seq(ent01.id), Nil, Nil, None, Some(r1.last.getId), 2, None).toVector
+ r2.size should equal(2)
+ val r3 = SquerylEntityModel.edgeQuery(Seq(ent01.id), Nil, Nil, None, Some(r2.last.getId), 2, None).toVector
+ r3.size should equal(2)
+ val r4 = SquerylEntityModel.edgeQuery(Seq(ent01.id), Nil, Nil, None, Some(r3.last.getId), 2, None).toVector
+ r4.size should equal(1)
+
+ val concatResults = r1 ++ r2 ++ r3 ++ r4
+ concatResults.size should equal(all.size)
+
+ checkEdges(all) { r1 ++ r2 ++ r3 ++ r4 }
+ }
+
+ def checkKeyValues(correct: Set[(UUID, String)])(query: => Seq[(UUID, Array[Byte])]) {
+ val results = query
+ results.size should equal(correct.size)
+ results.map(tup => (tup._1, new String(tup._2, "UTF-8"))).toSet should equal(correct)
+ }
+
+ test("Entity key store get") {
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+ val ent04 = createEntity("ent04", Seq("typeA"))
+
+ val kv01 = createEntityKeyValue(ent01.id, "keyA", "value01".getBytes("UTF-8"))
+ val kv02 = createEntityKeyValue(ent02.id, "keyA", "value02".getBytes("UTF-8"))
+ val kv03 = createEntityKeyValue(ent03.id, "keyB", "value03".getBytes("UTF-8"))
+ val kv04 = createEntityKeyValue(ent02.id, "keyB", "value04".getBytes("UTF-8"))
+
+ val v1 = (ent01.id, "value01")
+ val v2 = (ent02.id, "value02")
+ val v3 = (ent03.id, "value03")
+ val v4 = (ent02.id, "value04")
+
+ checkKeyValues(Set(v1)) {
+ SquerylEntityModel.getKeyValues(Seq(ent01.id), "keyA")
+ }
+
+ checkKeyValues(Set(v1, v2)) {
+ SquerylEntityModel.getKeyValues(Seq(ent01.id, ent02.id), "keyA")
+ }
+
+ checkKeyValues(Set(v1, v2)) {
+ SquerylEntityModel.getKeyValues(Seq(ent01.id, ent02.id, ent03.id, ent04.id), "keyA")
+ }
+
+ checkKeyValues(Set(v3, v4)) {
+ SquerylEntityModel.getKeyValues(Seq(ent01.id, ent02.id, ent03.id, ent04.id), "keyB")
+ }
+ }
+
+ def checkPutKeyValues(correct: Set[(UUID, String)], correctCreates: Set[UUID], correctUpdates: Set[UUID])(query: => (Seq[(UUID, Array[Byte])], Seq[UUID], Seq[UUID])) {
+ val (results, created, updated) = query
+ results.size should equal(correct.size)
+ results.map(tup => (tup._1, new String(tup._2, "UTF-8"))).toSet should equal(correct)
+
+ created.size should equal(correctCreates.size)
+ created.toSet should equal(correctCreates)
+
+ updated.size should equal(correctUpdates.size)
+ updated.toSet should equal(correctUpdates)
+ }
+
+ test("Entity key store put") {
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+
+ val kv02 = createEntityKeyValue(ent02.id, "keyA", "value55".getBytes("UTF-8"))
+ val kv03 = createEntityKeyValue(ent03.id, "keyB", "value03".getBytes("UTF-8"))
+
+ val v1 = (ent01.id, "value01")
+ val v2 = (ent02.id, "value02")
+ val v3 = (ent03.id, "value03")
+
+ val putsKeyA = Seq(
+ (ent01.id, "value01".getBytes),
+ (ent02.id, "value02".getBytes))
+
+ checkPutKeyValues(Set(v1, v2), Set(ent01.id), Set(ent02.id)) {
+ SquerylEntityModel.putKeyValues(putsKeyA, "keyA")
+ }
+
+ checkPutKeyValues(Set(v3), Set.empty[UUID], Set.empty[UUID]) {
+ SquerylEntityModel.putKeyValues(Seq((ent03.id, "value03".getBytes)), "keyB")
+ }
+ }
+
+ test("Entity key store delete") {
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+ val ent04 = createEntity("ent04", Seq("typeA"))
+
+ val kv01 = createEntityKeyValue(ent01.id, "keyA", "value01".getBytes("UTF-8"))
+ val kv02 = createEntityKeyValue(ent02.id, "keyA", "value02".getBytes("UTF-8"))
+ val kv03 = createEntityKeyValue(ent03.id, "keyB", "value03".getBytes("UTF-8"))
+ val kv04 = createEntityKeyValue(ent02.id, "keyB", "value04".getBytes("UTF-8"))
+
+ val v1 = (ent01.id, "value01")
+ val v2 = (ent02.id, "value02")
+ val v3 = (ent03.id, "value03")
+ val v4 = (ent02.id, "value04")
+
+ checkKeyValues(Set(v1)) {
+ SquerylEntityModel.deleteKeyValues(Seq(ent01.id), "keyA")
+ }
+ checkKeyValues(Set(v2)) {
+ SquerylEntityModel.deleteKeyValues(Seq(ent02.id, ent03.id), "keyA")
+ }
+ checkKeyValues(Set(v3, v4)) {
+ SquerylEntityModel.deleteKeyValues(Seq(ent02.id, ent03.id), "keyB")
+ }
+
+ checkKeyValues(Set()) {
+ SquerylEntityModel.deleteKeyValues(Seq(ent04.id), "keyB")
+ }
+ checkKeyValues(Set()) {
+ SquerylEntityModel.deleteKeyValues(Seq(UUID.randomUUID()), "keyB")
+ }
+ }
+
+ def checkKeyValues2(correct: Set[(UUID, String, String)])(query: => Seq[EntityKeyValue]) {
+ val results = query
+ results.size should equal(correct.size)
+ results.map(row => (protoUUIDToUuid(row.getUuid), row.getKey, row.getValue.getStringValue)).toSet should equal(correct)
+ }
+
+ def strToStored(str: String): StoredValue = {
+ StoredValue.newBuilder()
+ .setStringValue(str)
+ .build()
+ }
+
+ test("Entity key store get 2") {
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+ val ent04 = createEntity("ent04", Seq("typeA"))
+
+ val kv01 = createEntityKeyValue(ent01.id, "keyA", strToStored("value01").toByteArray)
+ val kv02 = createEntityKeyValue(ent02.id, "keyA", strToStored("value02").toByteArray)
+ val kv03 = createEntityKeyValue(ent03.id, "keyB", strToStored("value03").toByteArray)
+ val kv04 = createEntityKeyValue(ent02.id, "keyB", strToStored("value04").toByteArray)
+
+ val v1 = (ent01.id, "keyA", "value01")
+ val v2 = (ent02.id, "keyA", "value02")
+ val v3 = (ent03.id, "keyB", "value03")
+ val v4 = (ent02.id, "keyB", "value04")
+
+ check(Set(v1)) {
+ SquerylEntityModel.getKeyValues2(Seq((ent01.id, "keyA")))
+ }
+
+ check(Set(v1, v2)) {
+ SquerylEntityModel.getKeyValues2(Seq((ent01.id, "keyA"), (ent02.id, "keyA")))
+ }
+
+ check(Set(v1, v2)) {
+ SquerylEntityModel.getKeyValues2(Seq((ent01.id, "keyA"), (ent02.id, "keyA"), (ent03.id, "keyA"), (ent04.id, "keyA")))
+ }
+
+ check(Set(v3, v4)) {
+ SquerylEntityModel.getKeyValues2(Seq((ent01.id, "keyB"), (ent02.id, "keyB"), (ent03.id, "keyB"), (ent04.id, "keyB")))
+ }
+ }
+
+ test("Entity key store put 2") {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+
+ val kv02 = createEntityKeyValue(ent02.id, "keyA", strToStored("value55").toByteArray)
+ val kv03 = createEntityKeyValue(ent03.id, "keyB", strToStored("value03").toByteArray)
+
+ val v1 = (ent01.id, "keyA", "value01")
+ val v2 = (ent02.id, "keyA", "value02")
+ val v3 = (ent03.id, "keyB", "value03")
+
+ val putsKeyA = Seq(
+ (ent01.id, "keyA", strToStored("value01")),
+ (ent02.id, "keyA", strToStored("value02")))
+
+ check(Set(v1, v2)) {
+ SquerylEntityModel.putKeyValues2(notifier, putsKeyA)
+ }
+
+ check(Set(v3)) {
+ SquerylEntityModel.putKeyValues2(notifier, Seq((ent03.id, "keyB", strToStored("value03"))))
+ }
+
+ notifier.checkEntityKeyValues(Set(
+ (Created, v1),
+ (Updated, v2)))
+ }
+
+ test("Entity key store delete 2") {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("ent01", Seq("typeA"))
+ val ent02 = createEntity("ent02", Seq("typeA"))
+ val ent03 = createEntity("ent03", Seq("typeA"))
+ val ent04 = createEntity("ent04", Seq("typeA"))
+
+ val kv01 = createEntityKeyValue(ent01.id, "keyA", strToStored("value01").toByteArray)
+ val kv02 = createEntityKeyValue(ent02.id, "keyA", strToStored("value02").toByteArray)
+ val kv03 = createEntityKeyValue(ent03.id, "keyB", strToStored("value03").toByteArray)
+ val kv04 = createEntityKeyValue(ent02.id, "keyB", strToStored("value04").toByteArray)
+
+ val v1 = (ent01.id, "keyA", "value01")
+ val v2 = (ent02.id, "keyA", "value02")
+ val v3 = (ent03.id, "keyB", "value03")
+ val v4 = (ent02.id, "keyB", "value04")
+
+ check(Set(v1)) {
+ SquerylEntityModel.deleteKeyValues2(notifier, Seq((ent01.id, "keyA")))
+ }
+ check(Set(v2)) {
+ SquerylEntityModel.deleteKeyValues2(notifier, Seq((ent02.id, "keyA"), (ent03.id, "keyA")))
+ }
+ check(Set(v3, v4)) {
+ SquerylEntityModel.deleteKeyValues2(notifier, Seq((ent02.id, "keyB"), (ent03.id, "keyB")))
+ }
+
+ check(Set()) {
+ SquerylEntityModel.deleteKeyValues2(notifier, Seq((ent04.id, "keyB")))
+ }
+ check(Set()) {
+ SquerylEntityModel.deleteKeyValues2(notifier, Seq((UUID.randomUUID(), "keyB")))
+ }
+
+ notifier.checkEntityKeyValues(Set(
+ (Deleted, v1),
+ (Deleted, v2),
+ (Deleted, v3),
+ (Deleted, v4)))
+ }
+
+}
diff --git a/services/src/test/scala/io/greenbus/services/model/EventModelTest.scala b/services/src/test/scala/io/greenbus/services/model/EventModelTest.scala
new file mode 100644
index 0000000..cb52a25
--- /dev/null
+++ b/services/src/test/scala/io/greenbus/services/model/EventModelTest.scala
@@ -0,0 +1,688 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.model
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import io.greenbus.services.framework.{ Deleted, Updated, Created }
+import io.greenbus.client.service.proto.Events.{ Alarm, EventConfig, Attribute }
+import io.greenbus.services.model.EventAlarmModel.{ EventQueryParams, SysEventTemplate, EventConfigTemp }
+import java.util.UUID
+import io.greenbus.services.authz.EntityFilter
+import org.squeryl.Query
+import io.greenbus.services.data.{ EventRow, ServicesSchema, EntityRow }
+
+@RunWith(classOf[JUnitRunner])
+class EventModelTest extends ServiceTestBase {
+ import ModelTestHelpers._
+
+ test("EventConfig query") {
+ val ec01 = createEventConfig("type01", 9, EventConfig.Designation.EVENT, Alarm.State.UNACK_SILENT, "res01", false)
+ val ec02 = createEventConfig("type02", 8, EventConfig.Designation.ALARM, Alarm.State.ACKNOWLEDGED, "res01", false)
+ val ec03 = createEventConfig("type03", 7, EventConfig.Designation.LOG, Alarm.State.UNACK_AUDIBLE, "res01", false)
+ val ec04 = createEventConfig("type04", 6, EventConfig.Designation.EVENT, Alarm.State.UNACK_SILENT, "res01", false)
+ val ec05 = createEventConfig("type05", 5, EventConfig.Designation.ALARM, Alarm.State.ACKNOWLEDGED, "res01", false)
+ val ec06 = createEventConfig("type06", 4, EventConfig.Designation.LOG, Alarm.State.UNACK_AUDIBLE, "res01", false)
+
+ val r1 = ("type01", 9, EventConfig.Designation.EVENT, Alarm.State.UNACK_SILENT, "res01", false)
+ val r2 = ("type02", 8, EventConfig.Designation.ALARM, Alarm.State.ACKNOWLEDGED, "res01", false)
+ val r3 = ("type03", 7, EventConfig.Designation.LOG, Alarm.State.UNACK_AUDIBLE, "res01", false)
+ val r4 = ("type04", 6, EventConfig.Designation.EVENT, Alarm.State.UNACK_SILENT, "res01", false)
+ val r5 = ("type05", 5, EventConfig.Designation.ALARM, Alarm.State.ACKNOWLEDGED, "res01", false)
+ val r6 = ("type06", 4, EventConfig.Designation.LOG, Alarm.State.UNACK_AUDIBLE, "res01", false)
+ val all = Set(r1, r2, r3, r4, r5, r6)
+
+ check(Set(r1, r3, r5)) {
+ SquerylEventAlarmModel.eventConfigQuery(List(9, 7, 5), None, Nil, Nil, None, 100)
+ }
+ check(Set(r4, r5, r6)) {
+ SquerylEventAlarmModel.eventConfigQuery(Nil, Some(6), Nil, Nil, None, 100)
+ }
+ check(Set(r2, r3, r5, r6)) {
+ SquerylEventAlarmModel.eventConfigQuery(Nil, None, List(EventConfig.Designation.ALARM, EventConfig.Designation.LOG), Nil, None, 100)
+ }
+ check(Set(r1, r2, r4, r5)) {
+ SquerylEventAlarmModel.eventConfigQuery(Nil, None, Nil, List(Alarm.State.UNACK_SILENT, Alarm.State.ACKNOWLEDGED), None, 100)
+ }
+
+ check(Set(r1, r2)) {
+ SquerylEventAlarmModel.eventConfigQuery(Nil, None, Nil, Nil, None, 2)
+ }
+ check(Set(r3, r4)) {
+ SquerylEventAlarmModel.eventConfigQuery(Nil, None, Nil, Nil, Some("type02"), 2)
+ }
+ check(Set(r5, r6)) {
+ SquerylEventAlarmModel.eventConfigQuery(Nil, None, Nil, Nil, Some("type04"), 2)
+ }
+ }
+
+ test("EventConfig get") {
+ val ec01 = createEventConfig("type01", 5, EventConfig.Designation.EVENT, Alarm.State.UNACK_SILENT, "res01", true)
+ val ec02 = createEventConfig("type02", 20, EventConfig.Designation.ALARM, Alarm.State.ACKNOWLEDGED, "res02", false)
+
+ val r1 = ("type01", 5, EventConfig.Designation.EVENT, Alarm.State.UNACK_SILENT, "res01", true)
+ val r2 = ("type02", 20, EventConfig.Designation.ALARM, Alarm.State.ACKNOWLEDGED, "res02", false)
+ val all = Set(r1, r2)
+
+ check(all) {
+ SquerylEventAlarmModel.getEventConfigs(Seq("type01", "type02"))
+ }
+
+ check(Set(r1)) {
+ SquerylEventAlarmModel.getEventConfigs(Seq("type01"))
+ }
+ check(Set(r2)) {
+ SquerylEventAlarmModel.getEventConfigs(Seq("type02"))
+ }
+
+ }
+
+ test("EventConfig put") {
+ val notifier = new TestModelNotifier
+ val ec01 = createEventConfig("type01", 1, EventConfig.Designation.LOG, Alarm.State.UNACK_AUDIBLE, "resA", false)
+
+ val r1 = ("type01", 5, EventConfig.Designation.EVENT, Alarm.State.UNACK_SILENT, "res01", false)
+ val r2 = ("type02", 20, EventConfig.Designation.ALARM, Alarm.State.ACKNOWLEDGED, "res02", false)
+ val all = Set(r1, r2)
+
+ val puts = Seq(
+ EventConfigTemp("type01", 5, EventConfig.Designation.EVENT, Alarm.State.UNACK_SILENT, "res01"),
+ EventConfigTemp("type02", 20, EventConfig.Designation.ALARM, Alarm.State.ACKNOWLEDGED, "res02"))
+
+ check(all) {
+ SquerylEventAlarmModel.putEventConfigs(notifier, puts)
+ }
+
+ notifier.checkEventConfigs(Set(
+ (Updated, r1),
+ (Created, r2)))
+ }
+
+ test("EventConfig delete") {
+ val notifier = new TestModelNotifier
+ val ec01 = createEventConfig("type01", 5, EventConfig.Designation.EVENT, Alarm.State.UNACK_SILENT, "res01", true)
+ val ec02 = createEventConfig("type02", 20, EventConfig.Designation.ALARM, Alarm.State.ACKNOWLEDGED, "res02", false)
+
+ val r1 = ("type01", 5, EventConfig.Designation.EVENT, Alarm.State.UNACK_SILENT, "res01", true)
+ val r2 = ("type02", 20, EventConfig.Designation.ALARM, Alarm.State.ACKNOWLEDGED, "res02", false)
+ val all = Set(r1, r2)
+
+ check(all) {
+ SquerylEventAlarmModel.deleteEventConfigs(notifier, Seq("type01", "type02"))
+ }
+
+ notifier.checkEventConfigs(Set(
+ (Deleted, r1),
+ (Deleted, r2)))
+ }
+
+ test("Event post simple") {
+ val notifier = new TestModelNotifier
+ val ec01 = createEventConfig("type01", 9, EventConfig.Designation.EVENT, Alarm.State.UNACK_SILENT, "res01", false)
+ val ec02 = createEventConfig("type02", 3, EventConfig.Designation.EVENT, Alarm.State.UNACK_SILENT, "res02", false)
+
+ val e1: (String, Int, String, Boolean, String, String, Option[UUID], Option[String], Option[Long]) = ("type01", 9, "user01", false, "sub01", "res01", Option.empty[UUID], None, Some(3L))
+ val e2 = ("type02", 3, "user02", false, "sub01", "res02", None, None, Some(5L))
+
+ val puts = Seq(
+ SysEventTemplate("user01", "type01", Some("sub01"), Some(3), None, None, Seq()),
+ SysEventTemplate("user02", "type02", Some("sub01"), Some(5), None, None, Seq()))
+
+ check(Set(e1, e2)) {
+ SquerylEventAlarmModel.postEvents(notifier, puts)
+ }
+
+ notifier.checkEvents(Set(
+ (Created, e1),
+ (Created, e2)))
+ }
+
+ test("Event post all fields") {
+ val notifier = new TestModelNotifier
+ val ec01 = createEventConfig("type01", 9, EventConfig.Designation.EVENT, Alarm.State.UNACK_SILENT, "res01", false)
+ val ec02 = createEventConfig("type02", 3, EventConfig.Designation.EVENT, Alarm.State.UNACK_SILENT, "res02", false)
+
+ val uuid1 = UUID.randomUUID()
+ val uuid2 = UUID.randomUUID()
+
+ val e1: (String, Int, String, Boolean, String, String, Option[UUID], Option[String], Option[Long]) = ("type01", 9, "user01", false, "sub01", "res01", Some(uuid1), Some("group01"), Some(3L))
+ val e2 = ("type02", 3, "user02", false, "sub01", "res02", Some(uuid2), Some("group02"), Some(5L))
+
+ val puts = Seq(
+ SysEventTemplate("user01", "type01", Some("sub01"), Some(3), Some(UUIDHelpers.uuidToProtoUUID(uuid1)), Some("group01"), Seq()),
+ SysEventTemplate("user02", "type02", Some("sub01"), Some(5), Some(UUIDHelpers.uuidToProtoUUID(uuid2)), Some("group02"), Seq()))
+
+ check(Set(e1, e2)) {
+ SquerylEventAlarmModel.postEvents(notifier, puts)
+ }
+
+ notifier.checkEvents(Set(
+ (Created, e1),
+ (Created, e2)))
+ }
+
+ test("Event post multiple types") {
+ val notifier = new TestModelNotifier
+ val ec01 = createEventConfig("type01", 9, EventConfig.Designation.EVENT, Alarm.State.UNACK_SILENT, "res01", false)
+ val ec02 = createEventConfig("type02", 20, EventConfig.Designation.ALARM, Alarm.State.UNACK_SILENT, "res02", false)
+ val ec03 = createEventConfig("type03", 1, EventConfig.Designation.LOG, Alarm.State.UNACK_SILENT, "res03", false)
+
+ val e1: (String, Int, String, Boolean, String, String, Option[UUID], Option[String], Option[Long]) = ("type01", 9, "user01", false, "sub01", "res01", None, None, Some(3L))
+ val e2 = ("type02", 20, "user02", true, "sub01", "res02", None, None, Some(5L))
+ val e3 = ("type03", 1, "user03", false, "sub01", "res03", None, None, Some(8L))
+
+ val a1 = (Alarm.State.UNACK_SILENT, "type02", 20, "user02", true, "sub01", "res02", None, Some(5L))
+
+ val puts = Seq(
+ SysEventTemplate("user01", "type01", Some("sub01"), Some(3), None, None, Seq()),
+ SysEventTemplate("user02", "type02", Some("sub01"), Some(5), None, None, Seq()),
+ SysEventTemplate("user03", "type03", Some("sub01"), Some(8), None, None, Seq()))
+
+ check(Set(e1, e2, e3)) {
+ SquerylEventAlarmModel.postEvents(notifier, puts)
+ }
+
+ notifier.checkEvents(Set(
+ (Created, e1),
+ (Created, e2),
+ (Created, e3)))
+
+ notifier.checkAlarms(Set(
+ (Created, a1)))
+ }
+
+ test("Event post alarm initial") {
+ val notifier = new TestModelNotifier
+ val ec01 = createEventConfig("type01", 9, EventConfig.Designation.ALARM, Alarm.State.UNACK_SILENT, "res01", false)
+ val ec02 = createEventConfig("type02", 3, EventConfig.Designation.ALARM, Alarm.State.UNACK_AUDIBLE, "res02", false)
+ val ec03 = createEventConfig("type03", 5, EventConfig.Designation.ALARM, Alarm.State.ACKNOWLEDGED, "res03", false)
+
+ val e1: (String, Int, String, Boolean, String, String, Option[UUID], Option[String], Option[Long]) =
+ ("type01", 9, "user01", true, "sub01", "res01", Option.empty[UUID], None, Some(3L))
+ val e2 = ("type02", 3, "user02", true, "sub01", "res02", None, None, Some(5L))
+ val e3 = ("type03", 5, "user03", true, "sub01", "res03", None, None, Some(8L))
+
+ val a1: (Alarm.State, String, Int, String, Boolean, String, String, Option[UUID], Option[Long]) =
+ (Alarm.State.UNACK_SILENT, "type01", 9, "user01", true, "sub01", "res01", Option.empty[UUID], Some(3L))
+ val a2 = (Alarm.State.UNACK_AUDIBLE, "type02", 3, "user02", true, "sub01", "res02", None, Some(5L))
+ val a3 = (Alarm.State.ACKNOWLEDGED, "type03", 5, "user03", true, "sub01", "res03", None, Some(8L))
+
+ val allEvents = Set(e1, e2, e3)
+ val allAlarms = Set(a1, a2, a3)
+
+ val puts = Seq(
+ SysEventTemplate("user01", "type01", Some("sub01"), Some(3), None, None, Seq()),
+ SysEventTemplate("user02", "type02", Some("sub01"), Some(5), None, None, Seq()),
+ SysEventTemplate("user03", "type03", Some("sub01"), Some(8), None, None, Seq()))
+
+ check(allEvents) {
+ SquerylEventAlarmModel.postEvents(notifier, puts)
+ }
+
+ notifier.checkEvents(allEvents.map(e => (Created, e)))
+ notifier.checkAlarms(allAlarms.map(a => (Created, a)))
+ }
+
+ test("Event post without config is alarm") {
+ val notifier = new TestModelNotifier
+ val ec02 = createEventConfig("type02", 3, EventConfig.Designation.EVENT, Alarm.State.UNACK_SILENT, "res02", false)
+
+ val e1: (String, Int, String, Boolean, String, String, Option[UUID], Option[String], Option[Long]) =
+ ("NON_EXISTENT_TYPE", EventAlarmModel.unconfiguredEventSeverity, "user01", true, "sub01", EventAlarmModel.unconfiguredEventMessage, Option.empty[UUID], None, Some(3L))
+ val e2 = ("type02", 3, "user02", false, "sub01", "res02", None, None, Some(5L))
+
+ val a1: (Alarm.State, String, Int, String, Boolean, String, String, Option[UUID], Option[Long]) =
+ (Alarm.State.UNACK_SILENT, "NON_EXISTENT_TYPE", EventAlarmModel.unconfiguredEventSeverity, "user01", true, "sub01", EventAlarmModel.unconfiguredEventMessage, Option.empty[UUID], Some(3L))
+
+ val puts = Seq(
+ SysEventTemplate("user01", "NON_EXISTENT_TYPE", Some("sub01"), Some(3), None, None, Seq()),
+ SysEventTemplate("user02", "type02", Some("sub01"), Some(5), None, None, Seq()))
+
+ check(Set(e1, e2)) {
+ SquerylEventAlarmModel.postEvents(notifier, puts)
+ }
+
+ notifier.checkEvents(Set(
+ (Created, e1),
+ (Created, e2)))
+
+ notifier.checkAlarms(Set(
+ (Created, a1)))
+ }
+
+ test("Event post message") {
+ val notifier = new TestModelNotifier
+ val ec01 = createEventConfig("type01", 9, EventConfig.Designation.EVENT, Alarm.State.UNACK_SILENT, "test resource {key01} {key02}", false)
+
+ val alist = Seq(Attribute.newBuilder()
+ .setName("key01")
+ .setValueString("v01")
+ .build(),
+ Attribute.newBuilder()
+ .setName("key02")
+ .setValueString("v02")
+ .build())
+
+ val e1: (String, Int, String, Boolean, String, String, Option[UUID], Option[String], Option[Long]) =
+ ("type01", 9, "user01", false, "sub01", "test resource v01 v02", Option.empty[UUID], None, Some(3L))
+
+ val puts = Seq(
+ SysEventTemplate("user01", "type01", Some("sub01"), Some(3), None, None, alist))
+
+ check(Set(e1)) {
+ SquerylEventAlarmModel.postEvents(notifier, puts)
+ }
+
+ notifier.checkEvents(Set(
+ (Created, e1)))
+ }
+
+ test("Event post create filter") {
+ import UUIDHelpers._
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("point01", Seq("Point"))
+ val ent02 = createEntity("point02", Seq("Point"))
+ val ec01 = createEventConfig("type01", 9, EventConfig.Designation.EVENT, Alarm.State.UNACK_SILENT, "res01", false)
+
+ val filter = new EntityFilter {
+ import org.squeryl.PrimitiveTypeMode._
+ protected def filterQuery: Query[EntityRow] = {
+ ServicesSchema.entities.where(ent => ent.id === ent01.id)
+ }
+ }
+
+ val e1: (String, Int, String, Boolean, String, String, Option[UUID], Option[String], Option[Long]) = ("type01", 9, "user01", false, "sub01", "res01", Some(ent01.id), None, Some(3L))
+
+ check(Set(e1)) {
+ SquerylEventAlarmModel.postEvents(notifier, Seq(SysEventTemplate("user01", "type01", Some("sub01"), Some(3), Some(uuidToProtoUUID(ent01.id)), None, Seq())), Some(filter))
+ }
+
+ intercept[ModelPermissionException] {
+ SquerylEventAlarmModel.postEvents(notifier, Seq(SysEventTemplate("user01", "type01", Some("sub01"), Some(3), Some(uuidToProtoUUID(ent02.id)), None, Seq())), Some(filter))
+ }
+ intercept[ModelPermissionException] {
+ SquerylEventAlarmModel.postEvents(notifier, Seq(SysEventTemplate("user01", "type01", Some("sub01"), Some(3), None, None, Seq())), Some(filter))
+ }
+
+ }
+
+ class EventQueryFixture {
+ val ent01 = createEntity("point01", Seq("Point"))
+ val ent02 = createEntity("point02", Seq("Point"))
+ val ev01 = createEvent("type01", false, 1, None, 2, "sub01", "agent01", None, None, Array.empty[Byte], "message01")
+ val ev02 = createEvent("type02", true, 2, None, 5, "sub02", "agent01", Some(ent01.id), Some("group01"), Array.empty[Byte], "message02")
+ val ev03 = createEvent("type03", false, 3, None, 5, "sub01", "agent02", Some(ent02.id), Some("group02"), Array.empty[Byte], "message03")
+ val ev04 = createEvent("type01", false, 4, None, 7, "sub01", "agent02", Some(ent01.id), Some("group01"), Array.empty[Byte], "message04")
+ val ev05 = createEvent("type02", true, 5, None, 8, "sub03", "agent03", Some(ent02.id), Some("group02"), Array.empty[Byte], "message05")
+ val ev06 = createEvent("type03", false, 6, None, 8, "sub01", "agent03", None, None, Array.empty[Byte], "message06")
+
+ val e1: (String, Int, Long, String, Boolean, String, String, Option[UUID], Option[String], Option[Long]) = ("type01", 2, 1, "agent01", false, "sub01", "message01", None, None, None)
+ val e2: (String, Int, Long, String, Boolean, String, String, Option[UUID], Option[String], Option[Long]) = ("type02", 5, 2, "agent01", true, "sub02", "message02", Some(ent01.id), Some("group01"), None)
+ val e3: (String, Int, Long, String, Boolean, String, String, Option[UUID], Option[String], Option[Long]) = ("type03", 5, 3, "agent02", false, "sub01", "message03", Some(ent02.id), Some("group02"), None)
+ val e4: (String, Int, Long, String, Boolean, String, String, Option[UUID], Option[String], Option[Long]) = ("type01", 7, 4, "agent02", false, "sub01", "message04", Some(ent01.id), Some("group01"), None)
+ val e5: (String, Int, Long, String, Boolean, String, String, Option[UUID], Option[String], Option[Long]) = ("type02", 8, 5, "agent03", true, "sub03", "message05", Some(ent02.id), Some("group02"), None)
+ val e6: (String, Int, Long, String, Boolean, String, String, Option[UUID], Option[String], Option[Long]) = ("type03", 8, 6, "agent03", false, "sub01", "message06", None, None, None)
+ }
+
+ test("Event query") {
+ val f = new EventQueryFixture
+ import f._
+
+ val all = Seq(e1, e2, e3, e4, e5, e6)
+
+ checkOrder(all) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams())
+ }
+
+ checkOrder(Seq(e1, e2, e4, e5)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(eventTypes = Seq("type01", "type02")))
+ }
+ checkOrder(Seq(e1, e5, e6)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(severities = List(2, 8)))
+ }
+ checkOrder(Seq(e1, e2, e3)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(severityOrHigher = Some(5)))
+ }
+ checkOrder(Seq(e2, e5)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(subsystems = List("sub02", "sub03")))
+ }
+ checkOrder(Seq(e1, e2, e5, e6)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(agents = List("agent01", "agent03")))
+ }
+
+ checkOrder(Seq(e2, e4)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(entityUuids = List(ent01.id)))
+ }
+ checkOrder(Seq(e3, e5)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(entityNames = List("point02")))
+ }
+
+ checkOrder(Seq(e2, e4)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(modelGroups = Seq("group01")))
+ }
+ checkOrder(Seq(e2, e3, e4, e5)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(modelGroups = Seq("group01", "group02")))
+ }
+
+ checkOrder(Seq(e2, e5)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(isAlarm = Some(true)))
+ }
+ checkOrder(Seq(e1, e3, e4, e6)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(isAlarm = Some(false)))
+ }
+
+ checkOrder(Seq(e4, e5, e6)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(timeFrom = Some(4)))
+ }
+ checkOrder(Seq(e1, e2, e3)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(timeTo = Some(3)))
+ }
+ checkOrder(Seq(e3, e4, e5)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(timeFrom = Some(3), timeTo = Some(5)))
+ }
+
+ checkOrder(Seq(e5, e6)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(pageSize = 2))
+ }
+ checkOrder(Seq(e1, e2)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(pageSize = 2, latest = false))
+ }
+ }
+
+ test("Event read filtering") {
+ val f = new EventQueryFixture
+ import f._
+
+ val filter = new EntityFilter {
+ import org.squeryl.PrimitiveTypeMode._
+ protected def filterQuery: Query[EntityRow] = {
+ ServicesSchema.entities.where(ent => ent.id === ent01.id)
+ }
+ }
+
+ checkOrder(Seq(e2, e4)) {
+ SquerylEventAlarmModel.getEvents(Seq(ev01.id, ev02.id, ev03.id, ev04.id, ev05.id, ev06.id), filter = Some(filter))
+ }
+
+ checkOrder(Seq(e2, e4)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(), filter = Some(filter))
+ }
+
+ checkOrder(Seq(e4)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(eventTypes = Seq("type01")), filter = Some(filter))
+ }
+
+ }
+
+ test("Event sub-milli paging") {
+
+ def createSimpleEvent(id: Int, time: Long) = createEvent("type" + id, false, time, None, 2, "sub01", "agent01", None, None, Array.empty[Byte], "message01")
+
+ def simpleEventTuple(id: Int, time: Long): (String, Int, Long, String, Boolean, String, String, Option[UUID], Option[String], Option[Long]) = ("type" + id, 2, time, "agent01", false, "sub01", "message01", None, None, None)
+
+ val ev01 = createSimpleEvent(1, 1)
+ val ev02 = createSimpleEvent(2, 2)
+ val ev03 = createSimpleEvent(3, 2)
+ val ev04 = createSimpleEvent(4, 3)
+ val ev05 = createSimpleEvent(5, 3)
+ val ev06 = createSimpleEvent(6, 3)
+
+ val e1 = simpleEventTuple(1, 1)
+ val e2 = simpleEventTuple(2, 2)
+ val e3 = simpleEventTuple(3, 2)
+ val e4 = simpleEventTuple(4, 3)
+ val e5 = simpleEventTuple(5, 3)
+ val e6 = simpleEventTuple(6, 3)
+
+ checkOrder(Seq(e1, e2)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(pageSize = 2, latest = false))
+ }
+ checkOrder(Seq(e3, e4)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(pageSize = 2, last = Some(ev02.id), latest = false))
+ }
+ checkOrder(Seq(e5, e6)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(pageSize = 2, last = Some(ev04.id), latest = false))
+ }
+
+ checkOrder(Seq(e5, e6)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(pageSize = 2, latest = true))
+ }
+ checkOrder(Seq(e3, e4)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(pageSize = 2, last = Some(ev05.id), latest = true))
+ }
+ checkOrder(Seq(e1, e2)) {
+ SquerylEventAlarmModel.eventQuery(EventQueryParams(pageSize = 2, last = Some(ev03.id), latest = true))
+ }
+ }
+
+ class AlarmFixture {
+ val ent01 = createEntity("point01", Seq("Point"))
+ val ent02 = createEntity("point02", Seq("Point"))
+ val ev01 = createEvent("type01", true, 1, None, 2, "sub01", "agent01", None, None, Array.empty[Byte], "message01")
+ val ev02 = createEvent("type02", true, 2, None, 5, "sub02", "agent01", Some(ent01.id), None, Array.empty[Byte], "message02")
+ val ev03 = createEvent("type03", true, 3, None, 5, "sub01", "agent02", Some(ent02.id), None, Array.empty[Byte], "message03")
+ val ev04 = createEvent("type01", true, 4, None, 7, "sub01", "agent02", Some(ent01.id), None, Array.empty[Byte], "message04")
+ val ev05 = createEvent("type02", true, 5, None, 8, "sub03", "agent03", Some(ent02.id), None, Array.empty[Byte], "message05")
+ val ev06 = createEvent("type03", true, 6, None, 8, "sub01", "agent03", None, None, Array.empty[Byte], "message06")
+
+ val al01 = createAlarm(ev01.id, Alarm.State.UNACK_SILENT)
+ val al02 = createAlarm(ev02.id, Alarm.State.UNACK_SILENT)
+ val al03 = createAlarm(ev03.id, Alarm.State.UNACK_AUDIBLE)
+ val al04 = createAlarm(ev04.id, Alarm.State.UNACK_AUDIBLE)
+ val al05 = createAlarm(ev05.id, Alarm.State.ACKNOWLEDGED)
+ val al06 = createAlarm(ev06.id, Alarm.State.REMOVED)
+
+ val a1 = (ev01.id, Alarm.State.UNACK_SILENT)
+ val a2 = (ev02.id, Alarm.State.UNACK_SILENT)
+ val a3 = (ev03.id, Alarm.State.UNACK_AUDIBLE)
+ val a4 = (ev04.id, Alarm.State.UNACK_AUDIBLE)
+ val a5 = (ev05.id, Alarm.State.ACKNOWLEDGED)
+ val a6 = (ev06.id, Alarm.State.REMOVED)
+
+ val all = Seq(a1, a2, a3, a4, a5, a6)
+ }
+
+ test("Alarm query") {
+ val f = new AlarmFixture
+ import f._
+
+ checkOrder(all) {
+ SquerylEventAlarmModel.alarmQuery(Nil, EventQueryParams())
+ }
+
+ checkOrder(Seq(a3, a4, a5)) {
+ SquerylEventAlarmModel.alarmQuery(List(Alarm.State.UNACK_AUDIBLE, Alarm.State.ACKNOWLEDGED), EventQueryParams())
+ }
+
+ checkOrder(Seq(a1, a2)) {
+ SquerylEventAlarmModel.alarmQuery(List(Alarm.State.UNACK_SILENT), EventQueryParams(severityOrHigher = Some(5)))
+ }
+
+ checkOrder(Seq(a5, a6)) {
+ SquerylEventAlarmModel.alarmQuery(Nil, EventQueryParams(pageSize = 2))
+ }
+ checkOrder(Seq(a1, a2)) {
+ SquerylEventAlarmModel.alarmQuery(Nil, EventQueryParams(pageSize = 2, latest = false))
+ }
+
+ }
+
+ test("Alarm sub-milli paging") {
+
+ def createSimpleEvent(id: Int, time: Long) = createEvent("type" + id, false, time, None, 2, "sub01", "agent01", None, None, Array.empty[Byte], "message01")
+
+ val ev01 = createSimpleEvent(1, 1)
+ val ev02 = createSimpleEvent(2, 2)
+ val ev03 = createSimpleEvent(3, 2)
+ val ev04 = createSimpleEvent(4, 3)
+ val ev05 = createSimpleEvent(5, 3)
+ val ev06 = createSimpleEvent(6, 3)
+
+ def createSimpleAlarm(e: EventRow) = {
+ createAlarm(e.id, Alarm.State.UNACK_SILENT)
+ }
+
+ def simpleAlarmTuple(id: Long): (Long, Alarm.State) = (id, Alarm.State.UNACK_SILENT)
+
+ val al01 = createSimpleAlarm(ev01)
+ val al02 = createSimpleAlarm(ev02)
+ val al03 = createSimpleAlarm(ev03)
+ val al04 = createSimpleAlarm(ev04)
+ val al05 = createSimpleAlarm(ev05)
+ val al06 = createSimpleAlarm(ev06)
+
+ val e1 = simpleAlarmTuple(ev01.id)
+ val e2 = simpleAlarmTuple(ev02.id)
+ val e3 = simpleAlarmTuple(ev03.id)
+ val e4 = simpleAlarmTuple(ev04.id)
+ val e5 = simpleAlarmTuple(ev05.id)
+ val e6 = simpleAlarmTuple(ev06.id)
+
+ checkOrder(Seq(e1, e2)) {
+ SquerylEventAlarmModel.alarmQuery(Seq(), EventQueryParams(pageSize = 2, latest = false))
+ }
+ checkOrder(Seq(e3, e4)) {
+ SquerylEventAlarmModel.alarmQuery(Seq(), EventQueryParams(pageSize = 2, last = Some(al02.id), latest = false))
+ }
+ checkOrder(Seq(e5, e6)) {
+ SquerylEventAlarmModel.alarmQuery(Seq(), EventQueryParams(pageSize = 2, last = Some(al04.id), latest = false))
+ }
+
+ checkOrder(Seq(e5, e6)) {
+ SquerylEventAlarmModel.alarmQuery(Seq(), EventQueryParams(pageSize = 2, latest = true))
+ }
+ checkOrder(Seq(e3, e4)) {
+ SquerylEventAlarmModel.alarmQuery(Seq(), EventQueryParams(pageSize = 2, last = Some(al05.id), latest = true))
+ }
+ checkOrder(Seq(e1, e2)) {
+ SquerylEventAlarmModel.alarmQuery(Seq(), EventQueryParams(pageSize = 2, last = Some(al03.id), latest = true))
+ }
+ }
+
+ test("Alarm read filtering") {
+ val f = new AlarmFixture
+ import f._
+
+ val filter = new EntityFilter {
+ import org.squeryl.PrimitiveTypeMode._
+ protected def filterQuery: Query[EntityRow] = {
+ ServicesSchema.entities.where(ent => ent.id === ent01.id)
+ }
+ }
+
+ checkOrder(Seq(a2, a4)) {
+ SquerylEventAlarmModel.alarmKeyQuery(Seq(al01.id, al02.id, al03.id, al04.id, al05.id, al06.id), filter = Some(filter))
+ }
+
+ checkOrder(Seq(a2, a4)) {
+ SquerylEventAlarmModel.alarmQuery(Nil, EventQueryParams(), filter = Some(filter))
+ }
+
+ checkOrder(Seq(a4)) {
+ SquerylEventAlarmModel.alarmQuery(Nil, EventQueryParams(eventTypes = Seq("type01")), filter = Some(filter))
+ }
+
+ }
+
+ test("Alarm update") {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("point01", Seq("Point"))
+ val ev01 = createEvent("type01", true, 1, None, 2, "sub01", "agent01", None, None, Array.empty[Byte], "message01")
+ val al01 = createAlarm(ev01.id, Alarm.State.UNACK_SILENT)
+
+ intercept[ModelInputException] {
+ SquerylEventAlarmModel.putAlarmStates(notifier, Seq((al01.id, Alarm.State.UNACK_AUDIBLE)))
+ }
+
+ check(Set((ev01.id, Alarm.State.ACKNOWLEDGED))) {
+ SquerylEventAlarmModel.putAlarmStates(notifier, Seq((al01.id, Alarm.State.ACKNOWLEDGED)))
+ }
+
+ intercept[ModelInputException] {
+ SquerylEventAlarmModel.putAlarmStates(notifier, Seq((al01.id, Alarm.State.UNACK_SILENT)))
+ }
+
+ intercept[ModelInputException] {
+ SquerylEventAlarmModel.putAlarmStates(notifier, Seq((al01.id, Alarm.State.UNACK_AUDIBLE)))
+ }
+
+ check(Set((ev01.id, Alarm.State.REMOVED))) {
+ SquerylEventAlarmModel.putAlarmStates(notifier, Seq((al01.id, Alarm.State.REMOVED)))
+ }
+
+ intercept[ModelInputException] {
+ SquerylEventAlarmModel.putAlarmStates(notifier, Seq((al01.id, Alarm.State.ACKNOWLEDGED)))
+ }
+ }
+
+ class AlarmMultiFixture {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("point01", Seq("Point"))
+ val ent02 = createEntity("point02", Seq("Point"))
+ val ev01 = createEvent("type01", true, 1, None, 2, "sub01", "agent01", None, None, Array.empty[Byte], "message01")
+ val ev02 = createEvent("type02", true, 2, None, 5, "sub02", "agent01", Some(ent01.id), None, Array.empty[Byte], "message02")
+ val ev03 = createEvent("type03", true, 3, None, 5, "sub01", "agent02", Some(ent02.id), None, Array.empty[Byte], "message03")
+
+ val al01 = createAlarm(ev01.id, Alarm.State.UNACK_SILENT)
+ val al02 = createAlarm(ev02.id, Alarm.State.UNACK_SILENT)
+ val al03 = createAlarm(ev03.id, Alarm.State.UNACK_AUDIBLE)
+ }
+
+ test("Alarm update multi") {
+ val f = new AlarmMultiFixture
+ import f._
+
+ val a1 = (ev01.id, Alarm.State.UNACK_SILENT)
+ val a2 = (ev02.id, Alarm.State.UNACK_SILENT)
+ val a3 = (ev03.id, Alarm.State.UNACK_AUDIBLE)
+
+ check(Set((ev01.id, Alarm.State.ACKNOWLEDGED), (ev02.id, Alarm.State.UNACK_SILENT), (ev03.id, Alarm.State.ACKNOWLEDGED))) {
+ SquerylEventAlarmModel.putAlarmStates(notifier, Seq((al01.id, Alarm.State.ACKNOWLEDGED), (al02.id, Alarm.State.UNACK_SILENT), (al03.id, Alarm.State.ACKNOWLEDGED)))
+ }
+
+ notifier.checkAlarmsSimple(Set(
+ (Updated, (ev01.id, Alarm.State.ACKNOWLEDGED)),
+ (Updated, (ev03.id, Alarm.State.ACKNOWLEDGED))))
+ }
+
+ test("Alarm update auth") {
+ val f = new AlarmMultiFixture
+ import f._
+
+ val filter = new EntityFilter {
+ import org.squeryl.PrimitiveTypeMode._
+ protected def filterQuery: Query[EntityRow] = {
+ ServicesSchema.entities.where(ent => ent.id === ent01.id)
+ }
+ }
+
+ check(Set((ev02.id, Alarm.State.UNACK_SILENT))) {
+ SquerylEventAlarmModel.putAlarmStates(notifier, Seq((al02.id, Alarm.State.UNACK_SILENT)), Some(filter))
+ }
+
+ intercept[ModelPermissionException] {
+ SquerylEventAlarmModel.putAlarmStates(notifier, Seq((al01.id, Alarm.State.ACKNOWLEDGED)), Some(filter))
+ }
+
+ intercept[ModelPermissionException] {
+ SquerylEventAlarmModel.putAlarmStates(notifier, Seq((al03.id, Alarm.State.ACKNOWLEDGED)), Some(filter))
+ }
+ }
+
+}
diff --git a/services/src/test/scala/io/greenbus/services/model/FrontEndModelTest.scala b/services/src/test/scala/io/greenbus/services/model/FrontEndModelTest.scala
new file mode 100644
index 0000000..172822a
--- /dev/null
+++ b/services/src/test/scala/io/greenbus/services/model/FrontEndModelTest.scala
@@ -0,0 +1,786 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.model
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import io.greenbus.client.service.proto.FrontEnd.FrontEndConnectionStatus
+import io.greenbus.services.data._
+import org.squeryl.PrimitiveTypeMode._
+import UUIDHelpers._
+import io.greenbus.services.framework._
+import io.greenbus.client.service.proto.Model._
+import io.greenbus.services.model.FrontEndModel._
+import java.util.UUID
+import org.scalatest.matchers.ShouldMatchers
+import io.greenbus.services.model.EntityModel.TypeParams
+import io.greenbus.client.service.proto.Model.ModelUUID
+
+object EntityBasedTestHelpers extends ShouldMatchers {
+ import ModelTestHelpers._
+
+ def genericPutTest[A, TupType, InfoType, ProtoType](
+ baseName: String,
+ typ: String,
+ dbCreates: Vector[UUID => A],
+ targetTups: Vector[(String, Set[String]) => TupType],
+ putInfos: Vector[InfoType],
+ notificationCheck: (TestModelNotifier, Set[(ModelEvent, TupType)]) => Unit)(doCheck: (ModelNotifier, Set[TupType], Seq[CoreTypeTemplate[InfoType]]) => Unit) {
+
+ val notifier = new TestModelNotifier
+
+ val ent03 = createEntity(s"${baseName}03", Seq(typ))
+ dbCreates(0)(ent03.id)
+ val ent04 = createEntity(s"${baseName}04", Seq(typ))
+ dbCreates(1)(ent04.id)
+ val ent05 = createEntity(s"${baseName}05", Seq(typ))
+ dbCreates(2)(ent05.id)
+ val ent06 = createEntity(s"${baseName}06", Seq(typ))
+ dbCreates(3)(ent06.id)
+
+ val p1 = targetTups(0)(s"${baseName}01", Set(typ))
+ val p2 = targetTups(1)(s"${baseName}02", Set(typ, "AnotherType"))
+ val p3 = targetTups(2)(s"${baseName}03", Set(typ))
+ val p4 = targetTups(3)(s"${baseName}44", Set(typ))
+ val p5 = targetTups(4)(s"${baseName}05", Set(typ, "Type05"))
+ val p6 = targetTups(5)(s"${baseName}06", Set(typ))
+ val all = Set(p1, p2, p3, p4, p5, p6)
+
+ val puts = Seq(
+ CoreTypeTemplate(None, s"${baseName}01", Set.empty[String], putInfos(0)),
+ CoreTypeTemplate(None, s"${baseName}02", Set("AnotherType"), putInfos(1)),
+ CoreTypeTemplate(None, s"${baseName}03", Set.empty[String], putInfos(2)),
+ CoreTypeTemplate(Some(ent04.id), s"${baseName}44", Set.empty[String], putInfos(3)),
+ CoreTypeTemplate(Some(ent05.id), s"${baseName}05", Set("Type05"), putInfos(4)),
+ CoreTypeTemplate(Some(ent06.id), s"${baseName}06", Set.empty[String], putInfos(5)))
+
+ doCheck(notifier, all, puts)
+
+ val correctEnts = Set((s"${baseName}01", Set(typ)),
+ (s"${baseName}02", Set(typ, "AnotherType")),
+ (s"${baseName}03", Set(typ)),
+ (s"${baseName}44", Set(typ)),
+ (s"${baseName}05", Set(typ, "Type05")),
+ (s"${baseName}06", Set(typ)))
+ val allEnts = SquerylEntityModel.allQuery(None, 100).map(simplify)
+ val typEnts = allEnts.filter(_._2.contains(typ))
+ typEnts.size should equal(correctEnts.size)
+ typEnts.toSet should equal(correctEnts)
+
+ notifier.checkEntities(Set(
+ (Created, (s"${baseName}01", Set(typ))),
+ (Created, (s"${baseName}02", Set(typ, "AnotherType"))),
+ (Updated, (s"${baseName}44", Set(typ))),
+ (Updated, (s"${baseName}05", Set(typ, "Type05")))))
+ notificationCheck(notifier, Set(
+ (Created, p1),
+ (Created, p2),
+ (Updated, p3),
+ (Updated, p4),
+ (Updated, p5),
+ (Updated, p6)))
+ }
+
+ class EntityBasedQueryFixture[Simplified, Proto](
+ namePrefix: String,
+ typ: String,
+ vec: Vector[(String, Set[String]) => Simplified],
+ create: Simplified => Unit,
+ entQuery: (TypeParams, Option[UUID], Option[String], Int, Boolean) => Seq[Proto],
+ getUuid: Proto => ModelUUID,
+ getName: Proto => String,
+ simplify: Proto => Simplified) {
+
+ val p1 = vec(0)(namePrefix + "01", Set(typ))
+ val p2 = vec(1)(namePrefix + "02", Set(typ, "TypeA", "TypeB"))
+ val p3 = vec(2)(namePrefix + "03", Set(typ))
+ val p4 = vec(3)(namePrefix + "04", Set(typ, "TypeA"))
+ val p5 = vec(4)(namePrefix + "05", Set(typ))
+ val p6 = vec(5)(namePrefix + "06", Set(typ, "TypeA"))
+ val all = Set(p1, p2, p3, p4, p5, p6)
+
+ all.foreach(create)
+
+ def entityBasedTests() {
+ implicit val simp = simplify
+
+ check(all) {
+ entQuery(TypeParams(Nil, Nil, Nil), None, None, 100, true)
+ }
+
+ check(Set(p2, p4, p6)) {
+ entQuery(TypeParams(Seq("TypeA"), Nil, Nil), None, None, 100, true)
+ }
+ check(Set(p1, p3, p5)) {
+ entQuery(TypeParams(Nil, Nil, Seq("TypeA")), None, None, 100, true)
+ }
+ check(Set(p2, p4, p6)) {
+ entQuery(TypeParams(Nil, Seq(typ, "TypeA"), Nil), None, None, 100, true)
+ }
+ check(Set(p2)) {
+ entQuery(TypeParams(Nil, Seq(typ, "TypeA", "TypeB"), Nil), None, None, 100, true)
+ }
+
+ {
+ val q1 = entQuery(TypeParams(Nil, Nil, Nil), None, None, 2, false)
+ val q2 = entQuery(TypeParams(Nil, Nil, Nil), Some(getUuid(q1.last)), None, 2, false)
+ val q3 = entQuery(TypeParams(Nil, Nil, Nil), Some(getUuid(q2.last)), None, 100, false)
+ val results = q1 ++ q2 ++ q3
+
+ results.size should equal(all.size)
+ results.map(simplify).toSet should equal(all)
+ }
+
+ {
+ val q1 = entQuery(TypeParams(Nil, Nil, Nil), None, None, 2, false)
+ val q2 = entQuery(TypeParams(Nil, Nil, Nil), None, Some(getName(q1.last)), 2, false)
+ val q3 = entQuery(TypeParams(Nil, Nil, Nil), None, Some(getName(q2.last)), 100, false)
+ val results = q1 ++ q2 ++ q3
+
+ results.size should equal(all.size)
+ results.map(simplify).toSet should equal(all)
+ }
+
+ {
+ val n1 = entQuery(TypeParams(Nil, Nil, Nil), None, None, 2, true)
+ val n2 = entQuery(TypeParams(Nil, Nil, Nil), None, Some(getName(n1.last)), 2, true)
+ val n3 = entQuery(TypeParams(Nil, Nil, Nil), None, Some(getName(n2.last)), 100, true)
+
+ n1.map(simplify) should equal(Seq(p1, p2))
+ n2.map(simplify) should equal(Seq(p3, p4))
+ n3.map(simplify) should equal(Seq(p5, p6))
+ }
+
+ {
+ val n1 = entQuery(TypeParams(Nil, Nil, Nil), None, None, 2, true)
+ val n2 = entQuery(TypeParams(Nil, Nil, Nil), Some(getUuid(n1.last)), None, 2, true)
+ val n3 = entQuery(TypeParams(Nil, Nil, Nil), Some(getUuid(n2.last)), None, 100, true)
+
+ n1.map(simplify) should equal(Seq(p1, p2))
+ n2.map(simplify) should equal(Seq(p3, p4))
+ n3.map(simplify) should equal(Seq(p5, p6))
+ }
+ }
+
+ }
+}
+
+@RunWith(classOf[JUnitRunner])
+class FrontEndModelTest extends ServiceTestBase {
+ import ModelTestHelpers._
+ import EntityBasedTestHelpers._
+
+ test("Point get") {
+ val ent01 = createEntity("point01", Seq("Point"))
+ val ent02 = createEntity("point02", Seq("Point"))
+ val ent03 = createEntity("point03", Seq("Point"))
+ val pt01 = createPoint(ent01.id, PointCategory.ANALOG, "unit1")
+ val pt02 = createPoint(ent02.id, PointCategory.COUNTER, "unit2")
+ val pt03 = createPoint(ent03.id, PointCategory.STATUS, "unit3")
+
+ val p1 = ("point01", Set("Point"), PointCategory.ANALOG, "unit1")
+ val p2 = ("point02", Set("Point"), PointCategory.COUNTER, "unit2")
+ val p3 = ("point03", Set("Point"), PointCategory.STATUS, "unit3")
+ val all = Set(p1, p2, p3)
+
+ check(all) {
+ SquerylFrontEndModel.pointKeyQuery(Seq(ent01.id, ent02.id, ent03.id), Nil)
+ }
+
+ check(all) {
+ SquerylFrontEndModel.pointKeyQuery(Seq(ent01.id, ent02.id), Seq("point03"))
+ }
+
+ check(Set(p1)) {
+ SquerylFrontEndModel.pointKeyQuery(Seq(ent01.id), Nil)
+ }
+ }
+
+ def createPointAndEntity(name: String, types: Set[String], pointCategory: PointCategory, unit: String) = {
+ val ent01 = createEntity(name, types.toSeq)
+ val pt01 = createPoint(ent01.id, pointCategory, unit)
+ }
+
+ test("Point query") {
+
+ type TupleType = (String, Set[String], PointCategory, String)
+
+ def toUuid(proto: Point): ModelUUID = proto.getUuid
+ def toName(proto: Point): String = proto.getName
+
+ val data = Vector[(String, Set[String]) => TupleType](
+ (_, _, PointCategory.ANALOG, "unit1"),
+ (_, _, PointCategory.COUNTER, "unit2"),
+ (_, _, PointCategory.STATUS, "unit3"),
+ (_, _, PointCategory.ANALOG, "unit2"),
+ (_, _, PointCategory.COUNTER, "unit3"),
+ (_, _, PointCategory.STATUS, "unit1"))
+
+ val f = new EntityBasedQueryFixture[TupleType, Point](
+ "point",
+ "Point",
+ data,
+ Function.tupled(createPointAndEntity _),
+ SquerylFrontEndModel.pointQuery(Nil, Nil, _, _, _, _, _),
+ toUuid,
+ toName,
+ simplify)
+
+ import f._
+
+ entityBasedTests()
+
+ check(Set(p1, p4)) {
+ SquerylFrontEndModel.pointQuery(Seq(PointCategory.ANALOG), Nil, TypeParams(Nil, Nil, Nil), None, None, 100)
+ }
+ check(Set(p1, p2, p4, p5)) {
+ SquerylFrontEndModel.pointQuery(Seq(PointCategory.ANALOG, PointCategory.COUNTER), Nil, TypeParams(Nil, Nil, Nil), None, None, 100)
+ }
+
+ check(Set(p1, p6)) {
+ SquerylFrontEndModel.pointQuery(Nil, Seq("unit1"), TypeParams(Nil, Nil, Nil), None, None, 100)
+ }
+ check(Set(p1, p2, p4, p6)) {
+ SquerylFrontEndModel.pointQuery(Nil, Seq("unit1", "unit2"), TypeParams(Nil, Nil, Nil), None, None, 100)
+ }
+ }
+
+ test("Point put") {
+
+ val creates = Vector[UUID => PointRow](
+ createPoint(_, PointCategory.ANALOG, "unit100"),
+ createPoint(_, PointCategory.ANALOG, "unit14"),
+ createPoint(_, PointCategory.ANALOG, "unit15"),
+ createPoint(_, PointCategory.ANALOG, "unit16"))
+
+ val targetTups = Vector[(String, Set[String]) => (String, Set[String], PointCategory, String)](
+ (_, _, PointCategory.ANALOG, "unit1"),
+ (_, _, PointCategory.COUNTER, "unit2"),
+ (_, _, PointCategory.STATUS, "unit3"),
+ (_, _, PointCategory.ANALOG, "unit14"),
+ (_, _, PointCategory.ANALOG, "unit15"),
+ (_, _, PointCategory.COUNTER, "unit66"))
+
+ val putInfos = Vector(
+ PointInfo(PointCategory.ANALOG, "unit1"),
+ PointInfo(PointCategory.COUNTER, "unit2"),
+ PointInfo(PointCategory.STATUS, "unit3"),
+ PointInfo(PointCategory.ANALOG, "unit14"),
+ PointInfo(PointCategory.ANALOG, "unit15"),
+ PointInfo(PointCategory.COUNTER, "unit66"))
+
+ def notifyCheck(notifier: TestModelNotifier, set: Set[(ModelEvent, (String, Set[String], PointCategory, String))]) {
+ notifier.checkPoints(set)
+ }
+
+ genericPutTest(
+ "point",
+ "Point",
+ creates,
+ targetTups,
+ putInfos,
+ notifyCheck) { (notifier, all, puts) =>
+
+ check(all) {
+ SquerylFrontEndModel.putPoints(notifier, puts)
+ }
+ }
+ }
+
+ test("Point put to non-point entity") {
+ val ent01 = createEntity("point01", Seq("AnotherType"))
+
+ intercept[ModelInputException] {
+ SquerylFrontEndModel.putPoints(NullModelNotifier, Seq(CoreTypeTemplate(None, "point01", Set.empty[String], PointInfo(PointCategory.ANALOG, "unit1"))))
+ }
+ }
+
+ test("Point type can't be added through entity service") {
+ EntityModelTest.bannedTypeTest("Point")
+ }
+
+ test("Point put cannot remove point type") {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("point01", Seq("Point", "AnotherType"))
+ val pt01 = createPoint(ent01.id, PointCategory.ANALOG, "unit100")
+
+ val p1 = ("point01", Set("Point"), PointCategory.ANALOG, "unit1")
+
+ val puts = Seq(
+ CoreTypeTemplate(None, "point01", Set.empty[String], PointInfo(PointCategory.ANALOG, "unit1")))
+
+ check(Set(p1)) {
+ SquerylFrontEndModel.putPoints(notifier, puts)
+ }
+ notifier.checkEntities(Set(
+ (Updated, ("point01", Set("Point")))))
+ notifier.checkPoints(Set(
+ (Updated, p1)))
+ }
+
+ test("Point delete") {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("point01", Seq("Point"))
+ val ent02 = createEntity("point02", Seq("Point"))
+ val ent03 = createEntity("point03", Seq("Point"))
+ val pt01 = createPoint(ent01.id, PointCategory.ANALOG, "unit1")
+ val pt02 = createPoint(ent02.id, PointCategory.COUNTER, "unit2")
+ val pt03 = createPoint(ent03.id, PointCategory.STATUS, "unit3")
+
+ val p1 = ("point01", Set("Point"), PointCategory.ANALOG, "unit1")
+ val p2 = ("point02", Set("Point"), PointCategory.COUNTER, "unit2")
+ val p3 = ("point03", Set("Point"), PointCategory.STATUS, "unit3")
+ val all = Set(p1, p2, p3)
+
+ check(Set(p2, p3)) {
+ SquerylFrontEndModel.deletePoints(notifier, Seq(ent02.id, ent03.id))
+ }
+
+ val correctEnts = Set(("point01", Set("Point")))
+ val allEnts = SquerylEntityModel.allQuery(None, 100).map(simplify)
+ allEnts.size should equal(correctEnts.size)
+ allEnts.toSet should equal(correctEnts)
+
+ val allPoints = ServicesSchema.points.where(t => true === true).toSeq
+ allPoints.size should equal(1)
+ allPoints.head.id should equal(pt01.id)
+
+ notifier.checkEntities(Set(
+ (Deleted, ("point02", Set("Point"))),
+ (Deleted, ("point03", Set("Point")))))
+ notifier.checkPoints(Set(
+ (Deleted, p2),
+ (Deleted, p3)))
+ }
+
+ test("Command get") {
+ val ent01 = createEntity("command01", Seq("Command"))
+ val ent02 = createEntity("command02", Seq("Command"))
+ val ent03 = createEntity("command03", Seq("Command"))
+ val cmd01 = createCommand(ent01.id, "displayCommand01", CommandCategory.SETPOINT_INT)
+ val cmd02 = createCommand(ent02.id, "displayCommand02", CommandCategory.SETPOINT_DOUBLE)
+ val cmd03 = createCommand(ent03.id, "displayCommand03", CommandCategory.CONTROL)
+
+ val c1 = ("command01", Set("Command"), "displayCommand01", CommandCategory.SETPOINT_INT)
+ val c2 = ("command02", Set("Command"), "displayCommand02", CommandCategory.SETPOINT_DOUBLE)
+ val c3 = ("command03", Set("Command"), "displayCommand03", CommandCategory.CONTROL)
+ val all = Set(c1, c2, c3)
+
+ check(all) {
+ SquerylFrontEndModel.commandKeyQuery(Seq(ent01.id, ent02.id, ent03.id), Nil)
+ }
+
+ check(all) {
+ SquerylFrontEndModel.commandKeyQuery(Nil, Seq("command01", "command02", "command03"))
+ }
+
+ check(all) {
+ SquerylFrontEndModel.commandKeyQuery(Seq(ent01.id, ent02.id), Seq("command03"))
+ }
+
+ check(Set(c1, c3)) {
+ SquerylFrontEndModel.commandKeyQuery(Seq(ent01.id), Seq("command03"))
+ }
+ }
+
+ def createCommandAndEntity(name: String, types: Set[String], displayName: String, commandCategory: CommandCategory) = {
+ val ent01 = createEntity(name, types.toSeq)
+ val pt01 = createCommand(ent01.id, displayName, commandCategory)
+ }
+
+ test("Command query") {
+
+ type TupleType = (String, Set[String], String, CommandCategory)
+
+ def toUuid(proto: Command): ModelUUID = proto.getUuid
+ def toName(proto: Command): String = proto.getName
+
+ val data = Vector[(String, Set[String]) => TupleType](
+ (_, _, "diplay01", CommandCategory.CONTROL),
+ (_, _, "diplay02", CommandCategory.SETPOINT_DOUBLE),
+ (_, _, "diplay03", CommandCategory.SETPOINT_INT),
+ (_, _, "diplay04", CommandCategory.CONTROL),
+ (_, _, "diplay05", CommandCategory.CONTROL),
+ (_, _, "diplay06", CommandCategory.SETPOINT_INT))
+
+ val f = new EntityBasedQueryFixture[TupleType, Command](
+ "command",
+ "Command",
+ data,
+ Function.tupled(createCommandAndEntity _),
+ SquerylFrontEndModel.commandQuery(Nil, _, _, _, _, _),
+ toUuid,
+ toName,
+ simplify)
+
+ import f._
+
+ entityBasedTests()
+
+ check(Set(p1, p4, p5)) {
+ SquerylFrontEndModel.commandQuery(Seq(CommandCategory.CONTROL), TypeParams(Nil, Nil, Nil), None, None, 100)
+ }
+
+ check(Set(p2, p3, p6)) {
+ SquerylFrontEndModel.commandQuery(Seq(CommandCategory.SETPOINT_DOUBLE, CommandCategory.SETPOINT_INT), TypeParams(Nil, Nil, Nil), None, None, 100)
+ }
+ }
+
+ test("Command put") {
+
+ val creates = Vector[UUID => CommandRow](
+ createCommand(_, "display03", CommandCategory.CONTROL),
+ createCommand(_, "display04", CommandCategory.CONTROL),
+ createCommand(_, "display05", CommandCategory.CONTROL),
+ createCommand(_, "display06", CommandCategory.CONTROL))
+
+ val targetTups = Vector[(String, Set[String]) => (String, Set[String], String, CommandCategory)](
+ (_, _, "display01", CommandCategory.SETPOINT_INT),
+ (_, _, "display02", CommandCategory.SETPOINT_DOUBLE),
+ (_, _, "display03", CommandCategory.SETPOINT_STRING),
+ (_, _, "display04", CommandCategory.CONTROL),
+ (_, _, "display05", CommandCategory.CONTROL),
+ (_, _, "display66", CommandCategory.SETPOINT_DOUBLE))
+
+ val putInfos = Vector(
+ CommandInfo("display01", CommandCategory.SETPOINT_INT),
+ CommandInfo("display02", CommandCategory.SETPOINT_DOUBLE),
+ CommandInfo("display03", CommandCategory.SETPOINT_STRING),
+ CommandInfo("display04", CommandCategory.CONTROL),
+ CommandInfo("display05", CommandCategory.CONTROL),
+ CommandInfo("display66", CommandCategory.SETPOINT_DOUBLE))
+
+ def notifyCheck(notifier: TestModelNotifier, set: Set[(ModelEvent, (String, Set[String], String, CommandCategory))]) {
+ notifier.checkCommands(set)
+ }
+
+ genericPutTest(
+ "command",
+ "Command",
+ creates,
+ targetTups,
+ putInfos,
+ notifyCheck) { (notifier, all, puts) =>
+
+ check(all) {
+ SquerylFrontEndModel.putCommands(notifier, puts)
+ }
+ }
+ }
+
+ test("Command type can't be added through entity service") {
+ EntityModelTest.bannedTypeTest("Command")
+ }
+
+ test("Command delete") {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("command01", Seq("Command"))
+ val ent02 = createEntity("command02", Seq("Command"))
+ val ent03 = createEntity("command03", Seq("Command"))
+ val cmd01 = createCommand(ent01.id, "displayCommand01", CommandCategory.SETPOINT_INT)
+ val cmd02 = createCommand(ent02.id, "displayCommand02", CommandCategory.SETPOINT_DOUBLE)
+ val cmd03 = createCommand(ent03.id, "displayCommand03", CommandCategory.CONTROL)
+
+ val c1 = ("command01", Set("Command"), "displayCommand01", CommandCategory.SETPOINT_INT)
+ val c2 = ("command02", Set("Command"), "displayCommand02", CommandCategory.SETPOINT_DOUBLE)
+ val c3 = ("command03", Set("Command"), "displayCommand03", CommandCategory.CONTROL)
+ val all = Set(c1, c2, c3)
+
+ check(Set(c2, c3)) {
+ SquerylFrontEndModel.deleteCommands(notifier, Seq(ent02.id, ent03.id))
+ }
+
+ val correctEnts = Set(("command01", Set("Command")))
+ val allEnts = SquerylEntityModel.allQuery(None, 100).map(simplify)
+ allEnts.size should equal(correctEnts.size)
+ allEnts.toSet should equal(correctEnts)
+
+ val allPoints = ServicesSchema.commands.where(t => true === true).toSeq
+ allPoints.size should equal(1)
+ allPoints.head.id should equal(cmd01.id)
+
+ notifier.checkEntities(Set(
+ (Deleted, ("command02", Set("Command"))),
+ (Deleted, ("command03", Set("Command")))))
+ notifier.checkCommands(Set(
+ (Deleted, c2),
+ (Deleted, c3)))
+ }
+
+ test("Endpoint get") {
+ val ent01 = createEntity("endpoint01", Seq("Endpoint"))
+ val ent02 = createEntity("endpoint02", Seq("Endpoint"))
+ val ent03 = createEntity("endpoint03", Seq("Endpoint"))
+ val end01 = createEndpoint(ent01.id, "protocol01", false)
+ val end02 = createEndpoint(ent02.id, "protocol02", false)
+ val end03 = createEndpoint(ent03.id, "protocol03", false)
+
+ val c1 = ("endpoint01", Set("Endpoint"), "protocol01", false)
+ val c2 = ("endpoint02", Set("Endpoint"), "protocol02", false)
+ val c3 = ("endpoint03", Set("Endpoint"), "protocol03", false)
+ val all = Set(c1, c2, c3)
+
+ check(all) {
+ SquerylFrontEndModel.endpointKeyQuery(Seq(ent01.id, ent02.id, ent03.id), Nil)
+ }
+
+ check(all) {
+ SquerylFrontEndModel.endpointKeyQuery(Nil, Seq("endpoint01", "endpoint02", "endpoint03"))
+ }
+
+ check(all) {
+ SquerylFrontEndModel.endpointKeyQuery(Seq(ent01.id, ent02.id), Seq("endpoint03"))
+ }
+
+ check(Set(c1, c3)) {
+ SquerylFrontEndModel.endpointKeyQuery(Seq(ent01.id), Seq("endpoint03"))
+ }
+ }
+
+ private def createEntAndEndpoint(name: String, protocol: String): EntityRow = {
+ val ent = createEntity(name, Seq("Endpoint"))
+ createEndpoint(ent.id, protocol, false)
+ ent
+ }
+
+ def createEndpointAndEntity(name: String, types: Set[String], protocol: String, disabled: Boolean) = {
+ val ent01 = createEntity(name, types.toSeq)
+ val pt01 = createEndpoint(ent01.id, protocol, disabled)
+ }
+
+ test("Endpoint query") {
+
+ type TupleType = (String, Set[String], String, Boolean)
+
+ def toUuid(proto: Endpoint): ModelUUID = proto.getUuid
+ def toName(proto: Endpoint): String = proto.getName
+
+ val data = Vector[(String, Set[String]) => TupleType](
+ (_, _, "protocolA", false),
+ (_, _, "protocolB", true),
+ (_, _, "protocolC", false),
+ (_, _, "protocolC", false),
+ (_, _, "protocolA", true),
+ (_, _, "protocolB", true))
+
+ val f = new EntityBasedQueryFixture[TupleType, Endpoint](
+ "endpoint",
+ "Endpoint",
+ data,
+ Function.tupled(createEndpointAndEntity _),
+ SquerylFrontEndModel.endpointQuery(Nil, None, _, _, _, _, _),
+ toUuid,
+ toName,
+ simplify)
+
+ import f._
+
+ entityBasedTests()
+
+ check(Set(p1, p5)) {
+ SquerylFrontEndModel.endpointQuery(Seq("protocolA"), None, TypeParams(Nil, Nil, Nil), None, None, 100)
+ }
+ check(Set(p1, p2, p5, p6)) {
+ SquerylFrontEndModel.endpointQuery(Seq("protocolA", "protocolB"), None, TypeParams(Nil, Nil, Nil), None, None, 100)
+ }
+
+ check(Set(p1, p3, p4)) {
+ SquerylFrontEndModel.endpointQuery(Nil, Some(false), TypeParams(Nil, Nil, Nil), None, None, 100)
+ }
+ check(Set(p5)) {
+ SquerylFrontEndModel.endpointQuery(Seq("protocolA"), Some(true), TypeParams(Nil, Nil, Nil), None, None, 100)
+ }
+ }
+
+ test("Endpoint put") {
+
+ val creates = Vector[UUID => EndpointRow](
+ createEndpoint(_, "protocol100", false),
+ createEndpoint(_, "protocol04", false),
+ createEndpoint(_, "protocol05", false),
+ createEndpoint(_, "protocol06", false))
+
+ val targetTups = Vector[(String, Set[String]) => (String, Set[String], String, Boolean)](
+ (_, _, "protocol01", false),
+ (_, _, "protocol02", false),
+ (_, _, "protocol03", false),
+ (_, _, "protocol04", false),
+ (_, _, "protocol55", false),
+ (_, _, "protocol66", false))
+
+ val putInfos = Vector(
+ EndpointInfo("protocol01", None),
+ EndpointInfo("protocol02", None),
+ EndpointInfo("protocol03", None),
+ EndpointInfo("protocol04", None),
+ EndpointInfo("protocol55", None),
+ EndpointInfo("protocol66", None))
+
+ def notifyCheck(notifier: TestModelNotifier, set: Set[(ModelEvent, (String, Set[String], String, Boolean))]) {
+ notifier.checkEndpoints(set)
+ }
+
+ genericPutTest(
+ "endpoint",
+ "Endpoint",
+ creates,
+ targetTups,
+ putInfos,
+ notifyCheck) { (notifier, all, puts) =>
+
+ check(all) {
+ SquerylFrontEndModel.putEndpoints(notifier, puts)
+ }
+ }
+ }
+
+ test("Endpoint type can't be added through entity service") {
+ EntityModelTest.bannedTypeTest("Endpoint")
+ }
+
+ test("Endpoint delete") {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("endpoint01", Seq("Endpoint"))
+ val ent02 = createEntity("endpoint02", Seq("Endpoint"))
+ val ent03 = createEntity("endpoint03", Seq("Endpoint"))
+ val cmd01 = createEndpoint(ent01.id, "protocol01", false)
+ val cmd02 = createEndpoint(ent02.id, "protocol02", false)
+ val cmd03 = createEndpoint(ent03.id, "protocol03", false)
+
+ val c1 = ("endpoint01", Set("Endpoint"), "protocol01", false)
+ val c2 = ("endpoint02", Set("Endpoint"), "protocol02", false)
+ val c3 = ("endpoint03", Set("Endpoint"), "protocol03", false)
+ val all = Set(c1, c2, c3)
+
+ check(Set(c2, c3)) {
+ SquerylFrontEndModel.deleteEndpoints(notifier, Seq(ent02.id, ent03.id))
+ }
+
+ val correctEnts = Set(("endpoint01", Set("Endpoint")))
+ val allEnts = SquerylEntityModel.allQuery(None, 100).map(simplify)
+ allEnts.size should equal(correctEnts.size)
+ allEnts.toSet should equal(correctEnts)
+
+ val allPoints = ServicesSchema.endpoints.where(t => true === true).toSeq
+ allPoints.size should equal(1)
+ allPoints.head.id should equal(cmd01.id)
+
+ notifier.checkEntities(Set(
+ (Deleted, ("endpoint02", Set("Endpoint"))),
+ (Deleted, ("endpoint03", Set("Endpoint")))))
+ notifier.checkEndpoints(Set(
+ (Deleted, c2),
+ (Deleted, c3)))
+ }
+
+ test("Endpoint put disabled") {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntity("endpoint01", Seq("Endpoint"))
+ val ent02 = createEntity("endpoint02", Seq("Endpoint"))
+ val ent03 = createEntity("endpoint03", Seq("Endpoint"))
+ val cmd01 = createEndpoint(ent01.id, "protocol01", false)
+ val cmd02 = createEndpoint(ent02.id, "protocol02", true)
+ val cmd03 = createEndpoint(ent03.id, "protocol03", false)
+
+ val c1 = ("endpoint01", Set("Endpoint"), "protocol01", true)
+ val c2 = ("endpoint02", Set("Endpoint"), "protocol02", false)
+ val c3 = ("endpoint03", Set("Endpoint"), "protocol03", false)
+ val all = Set(c1, c2, c3)
+
+ val puts = Seq(EndpointDisabledUpdate(ent01.id, true), EndpointDisabledUpdate(ent02.id, false), EndpointDisabledUpdate(ent03.id, false))
+
+ check(all) {
+ SquerylFrontEndModel.putEndpointsDisabled(notifier, puts)
+ }
+
+ notifier.checkEndpoints(Set(
+ (Updated, c1),
+ (Updated, c2)))
+ }
+
+ test("Comm status get (empty)") {
+ val ent01 = createEntAndEndpoint("endpoint01", "protocol01")
+ val ent02 = createEntAndEndpoint("endpoint02", "protocol01")
+
+ check(Set.empty[(String, FrontEndConnectionStatus.Status)]) {
+ SquerylFrontEndModel.getFrontEndConnectionStatuses(List(ent01.id, ent02.id), Nil)
+ }
+ }
+
+ test("Comm status get") {
+ val ent01 = createEntAndEndpoint("endpoint01", "protocol01")
+ val ent02 = createEntAndEndpoint("endpoint02", "protocol01")
+ createConnectionStatus(ent01.id, FrontEndConnectionStatus.Status.COMMS_DOWN, 1)
+ createConnectionStatus(ent02.id, FrontEndConnectionStatus.Status.COMMS_UP, 2)
+
+ val m1 = ("endpoint01", FrontEndConnectionStatus.Status.COMMS_DOWN)
+ val m2 = ("endpoint02", FrontEndConnectionStatus.Status.COMMS_UP)
+ val all = Set(m1, m2)
+
+ check(all) {
+ SquerylFrontEndModel.getFrontEndConnectionStatuses(List(ent01.id, ent02.id), Nil)
+ }
+ }
+
+ test("Comm status put (empty)") {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntAndEndpoint("endpoint01", "protocol01")
+ val ent02 = createEntAndEndpoint("endpoint02", "protocol01")
+
+ val puts = List(
+ (ent01.id, FrontEndConnectionStatus.Status.COMMS_DOWN),
+ (ent02.id, FrontEndConnectionStatus.Status.COMMS_UP))
+
+ val m1 = ("endpoint01", FrontEndConnectionStatus.Status.COMMS_DOWN)
+ val m2 = ("endpoint02", FrontEndConnectionStatus.Status.COMMS_UP)
+ val all = Set(m1, m2)
+
+ check(all) {
+ SquerylFrontEndModel.putFrontEndConnectionStatuses(notifier, puts)
+ }
+
+ notifier.checkConnectionStatus(
+ Set((Updated, m1),
+ (Updated, m2)))
+ }
+
+ test("Comm status put") {
+ val notifier = new TestModelNotifier
+ val ent01 = createEntAndEndpoint("endpoint01", "protocol01")
+ val ent02 = createEntAndEndpoint("endpoint02", "protocol01")
+ createConnectionStatus(ent01.id, FrontEndConnectionStatus.Status.COMMS_DOWN, 1)
+ createConnectionStatus(ent02.id, FrontEndConnectionStatus.Status.COMMS_UP, 2)
+
+ val puts = List(
+ (ent01.id, FrontEndConnectionStatus.Status.COMMS_UP),
+ (ent02.id, FrontEndConnectionStatus.Status.ERROR))
+
+ val m1 = ("endpoint01", FrontEndConnectionStatus.Status.COMMS_UP)
+ val m2 = ("endpoint02", FrontEndConnectionStatus.Status.ERROR)
+ val all = Set(m1, m2)
+
+ check(all) {
+ SquerylFrontEndModel.putFrontEndConnectionStatuses(notifier, puts)
+ }
+
+ notifier.checkConnectionStatus(
+ Set((Updated, m1),
+ (Updated, m2)))
+ }
+}
diff --git a/services/src/test/scala/io/greenbus/services/model/ModelTestHelpers.scala b/services/src/test/scala/io/greenbus/services/model/ModelTestHelpers.scala
new file mode 100644
index 0000000..336e2bc
--- /dev/null
+++ b/services/src/test/scala/io/greenbus/services/model/ModelTestHelpers.scala
@@ -0,0 +1,331 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.model
+
+import io.greenbus.client.service.proto.FrontEnd.FrontEndConnectionStatus
+import io.greenbus.services.core.EntityKeyValueWithEndpoint
+import io.greenbus.services.data._
+import java.util.UUID
+import io.greenbus.client.service.proto.Auth.{ Permission, Agent, PermissionSet }
+import UUIDHelpers._
+import io.greenbus.client.service.proto.Model.{ EntityEdge, Entity }
+import scala.collection.JavaConversions._
+import org.scalatest.matchers.ShouldMatchers
+import io.greenbus.services.framework.{ ModelNotifier, ModelEvent }
+import scala.collection.mutable
+import io.greenbus.client.service.proto.Model._
+import io.greenbus.client.service.proto.Commands.CommandLock
+import io.greenbus.client.service.proto.Events.{ Event, Alarm, EventConfig }
+
+object ModelTestHelpers extends ShouldMatchers {
+
+ def createEntity(name: String, types: Seq[String]): EntityRow = {
+ val ent = ServicesSchema.entities.insert(EntityRow(UUID.randomUUID(), name))
+ types.foreach { typ =>
+ ServicesSchema.entityTypes.insert(EntityTypeRow(ent.id, typ))
+ }
+ ent
+ }
+
+ def createEdge(parentId: UUID, childId: UUID, relation: String, depth: Int): EntityEdgeRow = {
+ ServicesSchema.edges.insert(EntityEdgeRow(0, parentId, childId, relation, depth))
+ }
+
+ def createEntityKeyValue(entityId: UUID, key: String, value: Array[Byte]): EntityKeyStoreRow = {
+ ServicesSchema.entityKeyValues.insert(EntityKeyStoreRow(0, entityId, key, value))
+ }
+
+ def createPoint(entityId: UUID, typ: PointCategory, unit: String): PointRow = {
+ ServicesSchema.points.insert(PointRow(0, entityId, typ.getNumber, unit))
+ }
+
+ def createCommand(entityId: UUID, displayName: String, typ: CommandCategory): CommandRow = {
+ ServicesSchema.commands.insert(CommandRow(0, entityId, displayName, typ.getNumber, None))
+ }
+
+ def createEndpoint(entityId: UUID, protocol: String, disabled: Boolean): EndpointRow = {
+ ServicesSchema.endpoints.insert(EndpointRow(0, entityId, protocol, disabled))
+ }
+
+ def createAgent(name: String, password: String = "password", perms: Set[Long] = Set.empty[Long]): AgentRow = {
+ val a = ServicesSchema.agents.insert(AgentRow(UUID.randomUUID(), name, password))
+ perms.foreach(addPermissionToAgent(_, a.id))
+ a
+ }
+
+ def createEventConfig(typ: String, severity: Int, designation: EventConfig.Designation, startState: Alarm.State, resource: String, builtIn: Boolean = false) = {
+ ServicesSchema.eventConfigs.insert(EventConfigRow(0, typ, severity, designation.getNumber, startState.getNumber, resource, builtIn))
+ }
+
+ def createEvent(eventType: String,
+ alarm: Boolean,
+ time: Long,
+ deviceTime: Option[Long],
+ severity: Int,
+ subsystem: String,
+ userId: String,
+ entityId: Option[UUID],
+ modelGroup: Option[String],
+ args: Array[Byte],
+ rendered: String) = {
+
+ ServicesSchema.events.insert(EventRow(0, eventType, alarm, time, deviceTime, severity, subsystem, userId, entityId, modelGroup, args, rendered))
+ }
+
+ def createAlarm(eventId: Long, state: Alarm.State) = {
+ ServicesSchema.alarms.insert(AlarmRow(0, state.getNumber, eventId))
+ }
+
+ def createConnectionStatus(endpointId: UUID, status: FrontEndConnectionStatus.Status, time: Long) = {
+ ServicesSchema.frontEndCommStatuses.insert(FrontEndCommStatusRow(0, endpointId, status.getNumber, time))
+ }
+
+ def addPermissionToAgent(setId: Long, agentId: UUID) {
+ ServicesSchema.agentSetJoins.insert(AgentPermissionSetJoinRow(setId, agentId))
+ }
+
+ def createPermission(resource: String, action: String): Permission = {
+ Permission.newBuilder()
+ .addResources(resource)
+ .addActions(action)
+ .build()
+ }
+
+ def createPermissionSet(name: String, perms: Seq[Permission] = Seq.empty[Permission]): PermissionSetRow = {
+ val b = PermissionSet.newBuilder
+ .setName(name)
+
+ perms.foreach(b.addPermissions)
+
+ val set = b.build()
+
+ ServicesSchema.permissionSets.insert(PermissionSetRow(0, name, set.toByteArray))
+ }
+
+ def check[A, B](correct: Set[B])(query: => Seq[A])(implicit mapping: A => B) {
+ val simplified = query.map(mapping)
+ if (simplified.toSet != correct) {
+ println("Correct: " + correct.mkString("(\n\t", "\n\t", "\n)"))
+ println("Result: " + simplified.toSet.mkString("(\n\t", "\n\t", "\n)"))
+ }
+ simplified.toSet should equal(correct)
+ simplified.size should equal(correct.size)
+ }
+
+ def checkOrder[A, B](correct: Seq[B])(query: => Seq[A])(implicit mapping: A => B) {
+ val simplified = query.map(mapping).toList
+ simplified.toList should equal(correct)
+ simplified.size should equal(correct.size)
+ }
+
+ implicit def simplify(proto: Entity): (String, Set[String]) = {
+ (proto.getName, proto.getTypesList.toSet)
+ }
+
+ implicit def simplify(proto: EntityKeyValue): (UUID, String, String) = {
+ (proto.getUuid, proto.getKey, proto.getValue.getStringValue)
+ }
+
+ implicit def simplify(v: EntityKeyValueWithEndpoint): (UUID, String, String) = {
+ (v.payload.getUuid, v.payload.getKey, v.payload.getValue.getStringValue)
+ }
+
+ implicit def simplify(proto: EntityEdge): (UUID, UUID, String, Int) = {
+ (proto.getParent, proto.getChild, proto.getRelationship, proto.getDistance)
+ }
+
+ implicit def simplify(proto: Point): (String, Set[String], PointCategory, String) = {
+ (proto.getName, proto.getTypesList.toSet, proto.getPointCategory, proto.getUnit)
+ }
+
+ implicit def simplify(proto: Command): (String, Set[String], String, CommandCategory) = {
+ (proto.getName, proto.getTypesList.toSet, proto.getDisplayName, proto.getCommandCategory)
+ }
+
+ implicit def simplify(proto: Endpoint): (String, Set[String], String, Boolean) = {
+ (proto.getName, proto.getTypesList.toSet, proto.getProtocol, proto.getDisabled)
+ }
+
+ implicit def simplify(proto: Agent): (String, Set[String]) = {
+ (proto.getName, proto.getPermissionSetsList.toSet)
+ }
+
+ implicit def simplify(proto: PermissionSet): (String, Set[(Set[String], Set[String])]) = {
+ (proto.getName, proto.getPermissionsList.map(p => (p.getResourcesList.toSet, p.getActionsList.toSet)).toSet)
+ }
+
+ implicit def simplify(proto: CommandLock): (CommandLock.AccessMode, Set[UUID], UUID, Option[Long]) = {
+ (proto.getAccess, proto.getCommandUuidsList.map(protoUUIDToUuid).toSet, proto.getAgentUuid, if (proto.hasExpireTime) Some(proto.getExpireTime) else None)
+ }
+
+ implicit def simplify(proto: EventConfig): (String, Int, EventConfig.Designation, Alarm.State, String, Boolean) = {
+ (proto.getEventType, proto.getSeverity, proto.getDesignation, proto.getAlarmState, proto.getResource, proto.getBuiltIn)
+ }
+
+ implicit def simplify(proto: Event): (String, Int, String, Boolean, String, String, Option[UUID], Option[String], Option[Long]) = {
+ (proto.getEventType,
+ proto.getSeverity,
+ proto.getAgentName,
+ proto.getAlarm,
+ proto.getSubsystem,
+ proto.getRendered,
+ if (proto.hasEntityUuid) Some(proto.getEntityUuid) else None,
+ if (proto.hasModelGroup) Some(proto.getModelGroup) else None,
+ if (proto.hasDeviceTime) Some(proto.getDeviceTime) else None)
+ }
+
+ implicit def simplifyWithTime(proto: Event): (String, Int, Long, String, Boolean, String, String, Option[UUID], Option[String], Option[Long]) = {
+ (proto.getEventType,
+ proto.getSeverity,
+ proto.getTime,
+ proto.getAgentName,
+ proto.getAlarm,
+ proto.getSubsystem,
+ proto.getRendered,
+ if (proto.hasEntityUuid) Some(proto.getEntityUuid) else None,
+ if (proto.hasModelGroup) Some(proto.getModelGroup) else None,
+ if (proto.hasDeviceTime) Some(proto.getDeviceTime) else None)
+ }
+
+ implicit def simplify(proto: Alarm): (Long, Alarm.State) = {
+ (proto.getEvent.getId.getValue.toLong, proto.getState)
+ }
+
+ implicit def simplifyAlarmFull(proto: Alarm): (Alarm.State, String, Int, String, Boolean, String, String, Option[UUID], Option[Long]) = {
+ val eventProto = proto.getEvent
+ (proto.getState,
+ eventProto.getEventType,
+ eventProto.getSeverity,
+ eventProto.getAgentName,
+ eventProto.getAlarm,
+ eventProto.getSubsystem,
+ eventProto.getRendered,
+ if (eventProto.hasEntityUuid) Some(eventProto.getEntityUuid) else None,
+ if (eventProto.hasDeviceTime) Some(eventProto.getDeviceTime) else None)
+ }
+
+ implicit def simplify(proto: FrontEndConnectionStatus): (String, FrontEndConnectionStatus.Status) = {
+ (proto.getEndpointName, proto.getState)
+ }
+
+ class TestModelNotifier extends ModelNotifier with ShouldMatchers {
+ private val map = mutable.Map.empty[Class[_], mutable.Queue[(ModelEvent, _)]]
+
+ def notify[A](typ: ModelEvent, event: A) {
+ val klass = event.getClass
+ map.get(klass) match {
+ case None =>
+ val q = mutable.Queue.empty[(ModelEvent, _)]
+ q += ((typ, event))
+ map.update(klass, q)
+ case Some(queue) =>
+ queue += ((typ, event))
+ }
+ }
+
+ def getQueue[A](klass: Class[A]): mutable.Queue[(ModelEvent, A)] = {
+ map.get(klass).map(_.asInstanceOf[mutable.Queue[(ModelEvent, A)]]).getOrElse(mutable.Queue.empty[(ModelEvent, A)])
+ }
+
+ /*def check[A, B](klass: Class[A], correct: Set[(ModelEvent, B)])(implicit mapping: A => B) {
+ val results = getQueue(klass).toSeq.map(tup => (tup._1, mapping(tup._2)))
+ setCheck(results, correct)
+ }*/
+
+ def checkEntities(correct: Set[(ModelEvent, (String, Set[String]))]) {
+ val results = getQueue(classOf[Entity]).toSeq.map(tup => (tup._1, simplify(tup._2)))
+ setCheck(results, correct)
+ }
+
+ def checkEntityEdges(correct: Set[(ModelEvent, (UUID, UUID, String, Int))]) {
+ val results = getQueue(classOf[EntityEdge]).toSeq.map(tup => (tup._1, simplify(tup._2)))
+ setCheck(results, correct)
+ }
+
+ def checkEntityKeyValues(correct: Set[(ModelEvent, (UUID, String, String))]): Unit = {
+ val results = getQueue(classOf[EntityKeyValueWithEndpoint]).toSeq.map(tup => (tup._1, simplify(tup._2)))
+ setCheck(results, correct)
+ }
+
+ def checkPoints(correct: Set[(ModelEvent, (String, Set[String], PointCategory, String))]) {
+ val results = getQueue(classOf[Point]).toSeq.map(tup => (tup._1, simplify(tup._2)))
+ setCheck(results, correct)
+ }
+
+ def checkCommands(correct: Set[(ModelEvent, (String, Set[String], String, CommandCategory))]) {
+ val results = getQueue(classOf[Command]).toSeq.map(tup => (tup._1, simplify(tup._2)))
+ setCheck(results, correct)
+ }
+
+ def checkCommandLocks(correct: Set[(ModelEvent, (CommandLock.AccessMode, Set[UUID], UUID, Option[Long]))]) {
+ val results = getQueue(classOf[CommandLock]).toSeq.map(tup => (tup._1, simplify(tup._2)))
+ setCheck(results, correct)
+ }
+
+ def checkEndpoints(correct: Set[(ModelEvent, (String, Set[String], String, Boolean))]) {
+ val results = getQueue(classOf[Endpoint]).toSeq.map(tup => (tup._1, simplify(tup._2)))
+ setCheck(results, correct)
+ }
+
+ def checkAgents(correct: Set[(ModelEvent, (String, Set[String]))]) {
+ val results = getQueue(classOf[Agent]).toSeq.map(tup => (tup._1, simplify(tup._2)))
+ setCheck(results, correct)
+ }
+
+ def checkPermissionSetNames(correct: Set[(ModelEvent, String)]) {
+ val results = getQueue(classOf[PermissionSet]).toSeq.map(tup => (tup._1, tup._2.getName))
+ setCheck(results, correct)
+ }
+ def checkPermissionSets(correct: Set[(ModelEvent, (String, Set[(Set[String], Set[String])]))]) {
+ val results = getQueue(classOf[PermissionSet]).toSeq.map(tup => (tup._1, simplify(tup._2)))
+ setCheck(results, correct)
+ }
+
+ def checkEventConfigs(correct: Set[(ModelEvent, (String, Int, EventConfig.Designation, Alarm.State, String, Boolean))]) {
+ val results = getQueue(classOf[EventConfig]).toSeq.map(tup => (tup._1, simplify(tup._2)))
+ setCheck(results, correct)
+ }
+
+ def checkEvents(correct: Set[(ModelEvent, (String, Int, String, Boolean, String, String, Option[UUID], Option[String], Option[Long]))]) {
+ val results = getQueue(classOf[Event]).toSeq.map(tup => (tup._1, simplify(tup._2)))
+ setCheck(results, correct)
+ }
+
+ def checkAlarms(correct: Set[(ModelEvent, (Alarm.State, String, Int, String, Boolean, String, String, Option[UUID], Option[Long]))]) {
+ val results = getQueue(classOf[Alarm]).toSeq.map(tup => (tup._1, simplifyAlarmFull(tup._2)))
+ setCheck(results, correct)
+ }
+
+ def checkAlarmsSimple(correct: Set[(ModelEvent, (Long, Alarm.State))]) {
+ val results = getQueue(classOf[Alarm]).toSeq.map(tup => (tup._1, simplify(tup._2)))
+ setCheck(results, correct)
+ }
+
+ def checkConnectionStatus(correct: Set[(ModelEvent, (String, FrontEndConnectionStatus.Status))]) {
+ val results = getQueue(classOf[FrontEndConnectionStatus]).toSeq.map(tup => (tup._1, simplify(tup._2)))
+ setCheck(results, correct)
+ }
+
+ private def setCheck[A](result: Seq[A], correct: Set[A]) {
+ result.size should equal(correct.size)
+ result.toSet should equal(correct)
+ }
+ }
+
+}
diff --git a/services/src/test/scala/io/greenbus/services/model/ServiceTestBase.scala b/services/src/test/scala/io/greenbus/services/model/ServiceTestBase.scala
new file mode 100644
index 0000000..d6443d3
--- /dev/null
+++ b/services/src/test/scala/io/greenbus/services/model/ServiceTestBase.scala
@@ -0,0 +1,26 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.services.model
+
+import io.greenbus.sql.test.{ RunTestsInsideTransaction, DatabaseUsingTestBase }
+import io.greenbus.services.data.ServicesSchema
+
+trait ServiceTestBase extends DatabaseUsingTestBase with RunTestsInsideTransaction {
+ def schemas = List(ServicesSchema)
+}
diff --git a/simulator/pom.xml b/simulator/pom.xml
new file mode 100755
index 0000000..e413f02
--- /dev/null
+++ b/simulator/pom.xml
@@ -0,0 +1,82 @@
+
+ 4.0.0
+
+
+ io.greenbus
+ greenbus-scala-base
+ 3.0.0
+ ../scala-base
+
+
+ greenbus-simulator
+ jar
+
+
+
+ AGPLv3
+ http://www.gnu.org/licenses/agpl-3.0.txt
+
+
+
+
+
+
+
+ com.mycila.maven-license-plugin
+ maven-license-plugin
+
+
+
+
+
+
+
+
+
+ typesafe
+ typesafe-releases
+ http://repo.typesafe.com/typesafe/maven-releases
+
+
+
+
+
+ io.greenbus
+ greenbus-client
+ 3.0.0
+
+
+ io.greenbus
+ greenbus-app-framework
+ 3.0.0
+
+
+ io.greenbus
+ greenbus-util
+ 3.0.0
+
+
+ io.greenbus.msg
+ greenbus-msg-qpid
+ 1.0.0
+
+
+ com.typesafe.play
+ play-json_2.10
+ 2.3.2
+
+
+ com.typesafe.akka
+ akka-actor_2.10
+ 2.2.0
+
+
+ com.typesafe.akka
+ akka-slf4j_2.10
+ 2.2.0
+
+
+
+
+
diff --git a/simulator/src/main/scala/io/greenbus/sim/EndpointSimulation.scala b/simulator/src/main/scala/io/greenbus/sim/EndpointSimulation.scala
new file mode 100644
index 0000000..091432d
--- /dev/null
+++ b/simulator/src/main/scala/io/greenbus/sim/EndpointSimulation.scala
@@ -0,0 +1,67 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.sim
+
+import io.greenbus.client.service.proto.Measurements.{ Quality, Measurement }
+import io.greenbus.sim.impl.RandomValue
+import scala.annotation.tailrec
+
+object EndpointSimulation {
+
+ case class MeasRecord(name: String, value: RandomValue) {
+ def measurement: (String, Measurement) = {
+ val meas = Measurement.newBuilder
+ .setQuality(Quality.newBuilder.build)
+
+ value.apply(meas)
+
+ (name, meas.build())
+ }
+ }
+}
+
+class EndpointSimulation(config: Seq[MeasurementSimConfig]) {
+ import EndpointSimulation._
+
+ private var currentValues: List[MeasRecord] = config.map { measConfig =>
+ MeasRecord(measConfig.name, RandomValue(measConfig))
+ }.toList
+
+ def tick(): Seq[(String, Measurement)] = {
+ val (nextValues, toPublish) = updated(currentValues, Nil, Nil)
+ currentValues = nextValues
+ toPublish
+ }
+
+ @tailrec
+ private def updated(current: List[MeasRecord], after: List[MeasRecord], results: List[(String, Measurement)]): (List[MeasRecord], List[(String, Measurement)]) = {
+ current match {
+ case Nil => (after.reverse, results.reverse)
+ case head :: tail => {
+ head.value.next() match {
+ case None => updated(tail, head :: after, results)
+ case Some(next) =>
+ val recordAfter = head.copy(value = next)
+ updated(tail, recordAfter :: after, recordAfter.measurement :: results)
+ }
+ }
+ }
+ }
+
+}
diff --git a/simulator/src/main/scala/io/greenbus/sim/SimulatedEndpoint.scala b/simulator/src/main/scala/io/greenbus/sim/SimulatedEndpoint.scala
new file mode 100644
index 0000000..83d1e75
--- /dev/null
+++ b/simulator/src/main/scala/io/greenbus/sim/SimulatedEndpoint.scala
@@ -0,0 +1,104 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.sim
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.client.service.proto.FrontEnd.FrontEndConnectionStatus
+import akka.actor.{ Actor, Props }
+import play.api.libs.json.Json
+import scala.concurrent.duration._
+import scala.collection.JavaConversions._
+import io.greenbus.client.service.proto.Commands.{ CommandStatus, CommandResult }
+import io.greenbus.client.service.proto.Model.{ EntityKeyValue, ModelUUID, Endpoint }
+import io.greenbus.app.actor.frontend.{ StackStatusUpdated, MeasurementsPublished, ProtocolConfigurer }
+import scala.concurrent.Future
+
+object SimulatedEndpoint extends ProtocolConfigurer[SimulatorEndpointConfig] with Logging {
+
+ def extractConfig(config: EntityKeyValue): Option[SimulatorEndpointConfig] = {
+ if (config.getValue.hasByteArrayValue) {
+ try {
+ val json = Json.parse(config.getValue.getByteArrayValue.toByteArray).as[SimulatorEndpointConfig]
+ Some(json)
+ } catch {
+ case ex: Throwable =>
+ logger.warn("Couldn't unmarshal configuration: " + ex)
+ None
+ }
+ } else {
+ None
+ }
+ }
+
+ def evaluate(endpoint: Endpoint, configFiles: Seq[EntityKeyValue]): Option[SimulatorEndpointConfig] = {
+ configFiles.find(kv => kv.getKey == "protocolConfig").flatMap(extractConfig)
+ }
+
+ def equivalent(latest: SimulatorEndpointConfig, previous: SimulatorEndpointConfig): Boolean = {
+ latest == previous
+ }
+
+ case object Tick
+
+ def props(endpointUuid: ModelUUID, endpointName: String, protocolConfig: SimulatorEndpointConfig, publish: (MeasurementsPublished) => Unit, statusUpdate: (StackStatusUpdated) => Unit): Props = {
+ Props(classOf[SimulatedEndpoint], endpointUuid, endpointName, protocolConfig, publish, statusUpdate)
+ }
+
+}
+
+class SimulatedEndpoint(endpointUuid: ModelUUID, endpointName: String, config: SimulatorEndpointConfig, publish: (MeasurementsPublished) => Unit, statusUpdate: (StackStatusUpdated) => Unit) extends Actor with Logging {
+ import SimulatedEndpoint._
+ import io.greenbus.app.actor.frontend.ActorProtocolManager.CommandIssued
+
+ private val delay = config.delay.map(_.milliseconds).getOrElse(2000.milliseconds)
+ private val simulation = new EndpointSimulation(config.measurements)
+
+ logger.info("Initializing simulated endpoint " + endpointName + ", point count: " + config.measurements.size + ", with period " + delay)
+
+ import context.dispatcher
+
+ statusUpdate(StackStatusUpdated(FrontEndConnectionStatus.Status.COMMS_UP))
+ self ! Tick
+
+ def receive = {
+
+ case Tick => {
+ val measurements = simulation.tick()
+ if (measurements.nonEmpty) {
+ publish(MeasurementsPublished(System.currentTimeMillis(), Seq(), measurements))
+ }
+ scheduleNext()
+ }
+
+ case CommandIssued(cmdName, cmdReq) => {
+ val cmdId = cmdReq.getCommandUuid
+
+ logger.info("Command request for " + cmdId.getValue)
+
+ sender ! Future.successful(CommandResult.newBuilder().setStatus(CommandStatus.NOT_SUPPORTED).build())
+ }
+ }
+
+ private def scheduleNext() {
+ context.system.scheduler.scheduleOnce(
+ delay,
+ self,
+ Tick)
+ }
+}
\ No newline at end of file
diff --git a/simulator/src/main/scala/io/greenbus/sim/Simulator.scala b/simulator/src/main/scala/io/greenbus/sim/Simulator.scala
new file mode 100644
index 0000000..cf2f993
--- /dev/null
+++ b/simulator/src/main/scala/io/greenbus/sim/Simulator.scala
@@ -0,0 +1,53 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.sim
+
+import java.util.UUID
+
+import akka.actor.{ ActorContext, ActorSystem }
+import com.typesafe.config.ConfigFactory
+import io.greenbus.app.actor.{ AmqpConnectionConfig, ProtocolsEndpointStrategy }
+import io.greenbus.app.actor.frontend.{ FrontendRegistrationConfig, FrontendRegistrationConfig$, ActorProtocolManager, FrontendFactory }
+
+object Simulator {
+
+ def main(args: Array[String]) {
+
+ val baseDir = Option(System.getProperty("io.greenbus.config.base")).getOrElse("")
+ val amqpConfigPath = Option(System.getProperty("io.greenbus.config.amqp")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.msg.amqp.cfg")
+ val userConfigPath = Option(System.getProperty("io.greenbus.config.user")).map(baseDir + _).getOrElse(baseDir + "io.greenbus.user.cfg")
+
+ val rootConfig = ConfigFactory.load()
+ val slf4jConfig = ConfigFactory.parseString("""akka { loggers = ["akka.event.slf4j.Slf4jLogger"] }""")
+ val akkaConfig = slf4jConfig.withFallback(rootConfig)
+ val system = ActorSystem("simulator", akkaConfig)
+
+ val endpointStrategy = new ProtocolsEndpointStrategy(Set("simulator", "benchmark"))
+
+ def protocolFactory(context: ActorContext) = new ActorProtocolManager[SimulatorEndpointConfig](context, SimulatedEndpoint.props)
+
+ system.actorOf(FrontendFactory.create(
+ AmqpConnectionConfig.default(amqpConfigPath), userConfigPath, endpointStrategy, protocolFactory, SimulatedEndpoint, Seq("protocolConfig"),
+ connectionRepresentsLock = false,
+ nodeId = UUID.randomUUID().toString,
+ FrontendRegistrationConfig.defaults))
+ }
+
+}
+
diff --git a/simulator/src/main/scala/io/greenbus/sim/SimulatorEndpointConfig.scala b/simulator/src/main/scala/io/greenbus/sim/SimulatorEndpointConfig.scala
new file mode 100644
index 0000000..e44a886
--- /dev/null
+++ b/simulator/src/main/scala/io/greenbus/sim/SimulatorEndpointConfig.scala
@@ -0,0 +1,52 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.sim
+
+object StatusSimConfig {
+ import play.api.libs.json._
+ implicit val writer = Json.writes[StatusSimConfig]
+ implicit val reader = Json.reads[StatusSimConfig]
+}
+case class StatusSimConfig(initialValue: Boolean)
+
+object AnalogSimConfig {
+ import play.api.libs.json._
+ implicit val writer = Json.writes[AnalogSimConfig]
+ implicit val reader = Json.reads[AnalogSimConfig]
+}
+case class AnalogSimConfig(initialValue: Double, min: Double, max: Double, maxDelta: Double)
+
+object MeasurementSimConfig {
+ import play.api.libs.json._
+ implicit val writer = Json.writes[MeasurementSimConfig]
+ implicit val reader = Json.reads[MeasurementSimConfig]
+}
+case class MeasurementSimConfig(
+ name: String,
+ measurementType: String,
+ changeProbability: Option[Double],
+ statusConfig: Option[StatusSimConfig],
+ analogConfig: Option[AnalogSimConfig])
+
+object SimulatorEndpointConfig {
+ import play.api.libs.json._
+ implicit val writer = Json.writes[SimulatorEndpointConfig]
+ implicit val reader = Json.reads[SimulatorEndpointConfig]
+}
+case class SimulatorEndpointConfig(delay: Option[Long], measurements: Seq[MeasurementSimConfig])
diff --git a/simulator/src/main/scala/io/greenbus/sim/impl/RandomValues.scala b/simulator/src/main/scala/io/greenbus/sim/impl/RandomValues.scala
new file mode 100644
index 0000000..40c82ab
--- /dev/null
+++ b/simulator/src/main/scala/io/greenbus/sim/impl/RandomValues.scala
@@ -0,0 +1,76 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the GNU Affero General Public License
+ * Version 3.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/agpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.sim.impl
+
+import io.greenbus.client.service.proto.Measurements
+import java.util.Random
+import io.greenbus.client.service.proto.Measurements.Measurement.Type
+import io.greenbus.sim.MeasurementSimConfig
+
+object RandomValue {
+
+ private val rand = new Random
+
+ def apply(config: MeasurementSimConfig): RandomValue = {
+
+ config.measurementType match {
+ case "BOOL" =>
+ config.statusConfig.map(cfg => BooleanValue(cfg.initialValue, config.changeProbability.getOrElse(0.3))).getOrElse(BooleanValue(false, 0.3))
+ case "INT" =>
+ config.analogConfig.map(cfg => IntValue(cfg.initialValue.toInt, cfg.min.toInt, cfg.max.toInt, cfg.maxDelta.toInt, config.changeProbability.getOrElse(0.3)))
+ .getOrElse(IntValue(0, 0, 100, 2, 0.3))
+ case "DOUBLE" =>
+ config.analogConfig.map(cfg => DoubleValue(cfg.initialValue, cfg.min, cfg.max, cfg.maxDelta, config.changeProbability.getOrElse(0.3)))
+ .getOrElse(DoubleValue(0, -100, 100, 2, 0.3))
+ case x => throw new Exception(s"Can't generate random value for type $x")
+ }
+ }
+
+ case class DoubleValue(value: Double, min: Double, max: Double, maxChange: Double, changeProbability: Double) extends RandomValue {
+ def generate() = this.copy(value = (value + maxChange * 2 * (rand.nextDouble - 0.5)).max(min).min(max))
+ def apply(meas: Measurements.Measurement.Builder) = meas.setDoubleVal(value).setType(Type.DOUBLE)
+ def newChangeProbability(p: Double) = this.copy(changeProbability = p)
+ }
+ case class IntValue(value: Int, min: Int, max: Int, maxChange: Int, changeProbability: Double) extends RandomValue {
+ def generate() = this.copy(value = (value + rand.nextInt(2 * maxChange + 1) - maxChange).max(min).min(max))
+ def apply(meas: Measurements.Measurement.Builder) = meas.setIntVal(value).setType(Type.INT)
+ def newChangeProbability(p: Double) = this.copy(changeProbability = p)
+ }
+ case class BooleanValue(value: Boolean, changeProbability: Double) extends RandomValue {
+ def generate() = this.copy(value = !value, changeProbability)
+ def apply(meas: Measurements.Measurement.Builder) = meas.setBoolVal(value) setType (Type.BOOL)
+ def newChangeProbability(p: Double) = this.copy(changeProbability = p)
+ }
+}
+
+trait RandomValue {
+
+ def changeProbability: Double
+
+ def newChangeProbability(p: Double): RandomValue
+
+ def next(): Option[RandomValue] = {
+ if (RandomValue.rand.nextDouble > changeProbability) None
+ else Some(generate)
+ }
+
+ def generate(): RandomValue
+
+ def apply(meas: Measurements.Measurement.Builder)
+}
\ No newline at end of file
diff --git a/sql/pom.xml b/sql/pom.xml
new file mode 100755
index 0000000..d2ff76b
--- /dev/null
+++ b/sql/pom.xml
@@ -0,0 +1,82 @@
+
+ 4.0.0
+
+
+ io.greenbus
+ greenbus-scala-base
+ 3.0.0
+ ../scala-base
+
+
+ greenbus-sql
+ jar
+
+
+
+ Apache 2
+ http://www.apache.org/licenses/LICENSE-2.0.txt
+ A business-friendly OSS license
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ ${maven-jar-plugin.version}
+
+
+
+ test-jar
+
+
+
+
+
+ com.mycila.maven-license-plugin
+ maven-license-plugin
+
+
+
+
+
+
+
+
+
+ io.greenbus
+ greenbus-util
+ ${greenbus.version}
+
+
+ org.squeryl
+ squeryl_${scala.annotation}
+ ${squeryl.version}
+
+
+ cglib
+ cglib-nodep
+ ${cglib.version}
+ compile
+
+
+ postgresql
+ postgresql
+ ${postgresql.version}
+
+
+ commons-pool
+ commons-pool
+ ${commons-pool.version}
+
+
+ commons-dbcp
+ commons-dbcp
+ ${commons-dbcp.version}
+
+
+
+
+
diff --git a/sql/src/main/scala/io/greenbus/sql/DbConnection.scala b/sql/src/main/scala/io/greenbus/sql/DbConnection.scala
new file mode 100755
index 0000000..b8ffaea
--- /dev/null
+++ b/sql/src/main/scala/io/greenbus/sql/DbConnection.scala
@@ -0,0 +1,49 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.sql
+
+import java.sql.Connection
+import org.squeryl.logging.StatisticsListener
+
+/**
+ * contains the session factory necessary to talk to the database. Handles the setting up of the
+ * session and transaction blocks.
+ *
+ * These functions should be used instead of PrimitiveTypeMode.transaction
+ */
+trait DbConnection {
+
+ /**
+ * will open and close a new transaction around the passed in code
+ */
+ def transaction[A](listener: Option[StatisticsListener])(fun: => A): A
+ def transaction[A](fun: => A): A
+
+ /**
+ * will open a new transaction if one doesn't already exist, otherwise goes into same transaction
+ */
+ def inTransaction[A](fun: => A): A
+
+ /**
+ * get the underlying jdbc Connection for use in low-level tasks like schema setup, cleanup.
+ * Handled in a block because we need to return the connection to the session pool when we are done
+ */
+ def underlyingConnection[A](fun: (Connection) => A): A
+}
+
diff --git a/sql/src/main/scala/io/greenbus/sql/DbConnector.scala b/sql/src/main/scala/io/greenbus/sql/DbConnector.scala
new file mode 100755
index 0000000..bba04ff
--- /dev/null
+++ b/sql/src/main/scala/io/greenbus/sql/DbConnector.scala
@@ -0,0 +1,31 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.sql
+
+object DbConnector {
+
+ def connect(sqlSettings: SqlSettings) = {
+ val provider = new postgres.Connector
+ provider.connect(sqlSettings)
+ }
+}
+
+trait DbConnector {
+ def connect(dbInfo: SqlSettings): DbConnection
+}
diff --git a/sql/src/main/scala/io/greenbus/sql/ExclusiveAccess.scala b/sql/src/main/scala/io/greenbus/sql/ExclusiveAccess.scala
new file mode 100755
index 0000000..d23e665
--- /dev/null
+++ b/sql/src/main/scala/io/greenbus/sql/ExclusiveAccess.scala
@@ -0,0 +1,83 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.sql
+
+import org.squeryl.{ Table, KeyedEntity }
+import org.squeryl.PrimitiveTypeMode._
+
+/**
+ * grants exclusive access to a squeryl object to a single thread or process. It is
+ * hinged on there being some property of the sql object that indicates whether the
+ * action we want to take needs to occur. We try to get exclusive access to the object,
+ * check the precondition, run the user block, then check that we have updated the
+ * object to no longer match the precondition (or else other threads would think
+ * they were the first to get access to the object)
+ */
+object ExclusiveAccess {
+
+ class ExclusiveAccessException(msg: String) extends Exception(msg)
+ class ObjectMissingException(msg: String) extends ExclusiveAccessException(msg)
+ class AcquireConditionNotMetException(msg: String) extends ExclusiveAccessException(msg)
+ class AcquireConditionStillValidException(msg: String) extends ExclusiveAccessException(msg)
+ class InvalidUpdateException(msg: String) extends ExclusiveAccessException(msg)
+
+ def exclusiveAccess[A <: KeyedEntity[Long]](
+ dbConnection: DbConnection,
+ table: Table[A],
+ id: Long,
+ updateFun: A => Any,
+ acquireCondition: A => Boolean)(lockFun: A => A): A = {
+
+ // Wrap/unwrap in list
+ val list = exclusiveAccess(dbConnection, table, List(id), updateFun, acquireCondition) { list =>
+ List(lockFun(list.head))
+ }
+ list.head
+ }
+
+ def exclusiveAccess[A <: KeyedEntity[Long]](dbConnection: DbConnection,
+ table: Table[A],
+ ids: List[Long],
+ updateFun: A => Any,
+ acquireCondition: A => Boolean)(lockFun: List[A] => List[A]): List[A] = {
+
+ dbConnection.transaction {
+ // Select for update
+ val objList = table.where(c => c.id in ids).forUpdate.toList
+
+ // Fail if we have nothing
+ if (objList.size < ids.size) throw new ObjectMissingException("Missing objects: " + ids + " got: " + objList.map { _.id })
+
+ // Precondition on all objects
+ if (objList.exists(!acquireCondition(_))) throw new AcquireConditionNotMetException("Couldn't acquire objects")
+
+ // Get results, do any work inside the lock
+ val results = lockFun(objList)
+
+ // Postcondition on all objects
+ if (results.exists(acquireCondition(_))) throw new AcquireConditionStillValidException("Objects were not updated")
+
+ // Call update for each row
+ results.foreach(updateFun(_))
+
+ results
+ }
+ }
+
+}
diff --git a/sql/src/main/scala/io/greenbus/sql/SqlSettings.scala b/sql/src/main/scala/io/greenbus/sql/SqlSettings.scala
new file mode 100755
index 0000000..54ed1ca
--- /dev/null
+++ b/sql/src/main/scala/io/greenbus/sql/SqlSettings.scala
@@ -0,0 +1,45 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.sql
+
+import com.typesafe.scalalogging.slf4j.Logging
+
+object SqlSettings extends Logging {
+ import io.greenbus.util.PropertyReading._
+
+ def load(file: String): SqlSettings = {
+ apply(loadFile(file))
+ }
+
+ def apply(props: Map[String, String]): SqlSettings = {
+ SqlSettings(
+ get(props, "io.greenbus.sql.url"),
+ get(props, "io.greenbus.sql.username"),
+ get(props, "io.greenbus.sql.password"),
+ getLong(props, "io.greenbus.sql.slowquery"),
+ optionalInt(props, "io.greenbus.sql.maxactive") getOrElse 8)
+ }
+}
+
+case class SqlSettings(url: String, user: String, password: String,
+ slowQueryTimeMilli: Long, poolMaxActive: Int) {
+
+ // custom to hide password
+ override def toString() = user + "@" + url
+}
diff --git a/sql/src/main/scala/io/greenbus/sql/TransactionMetricsListener.scala b/sql/src/main/scala/io/greenbus/sql/TransactionMetricsListener.scala
new file mode 100644
index 0000000..8c6198a
--- /dev/null
+++ b/sql/src/main/scala/io/greenbus/sql/TransactionMetricsListener.scala
@@ -0,0 +1,69 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.sql
+
+import org.squeryl.logging.{ StatementInvocationEvent, StatisticsListener }
+import io.greenbus.jmx.Metrics
+import com.typesafe.scalalogging.slf4j.Logging
+
+class TransactionMetrics(metrics: Metrics) {
+
+ val queryCount = metrics.average("SqlQueryCount")
+ val queryTime = metrics.average("SqlQueryTime")
+ val timePerQuery = metrics.average("SqlTimePerQuery")
+}
+
+class TransactionMetricsListener(requestId: String) extends StatisticsListener with Logging {
+
+ private var queryCount: Int = 0
+ private var queryTime: Int = 0
+
+ def queryExecuted(se: StatementInvocationEvent): Unit = {
+ queryCount += 1
+ val elapsed = (se.end - se.start).toInt
+ queryTime += elapsed
+ logger.trace(s"$requestId - Query ($elapsed) [${se.rowCount}]: " + se.jdbcStatement.take(30).replace('\n', ' ') + " : " + se.definitionOrCallSite)
+ }
+
+ def updateExecuted(se: StatementInvocationEvent): Unit = {
+ val elapsed = (se.end - se.start).toInt
+ logger.trace(s"$requestId - Update ($elapsed) [${se.rowCount}]: " + se.jdbcStatement.take(30).replace('\n', ' '))
+ }
+
+ def insertExecuted(se: StatementInvocationEvent): Unit = {
+ val elapsed = (se.end - se.start).toInt
+ logger.trace(s"$requestId - Insert ($elapsed) [${se.rowCount}]: " + se.jdbcStatement.take(30).replace('\n', ' '))
+ }
+
+ def deleteExecuted(se: StatementInvocationEvent): Unit = {
+ val elapsed = (se.end - se.start).toInt
+ logger.trace(s"$requestId - Delete ($elapsed) [${se.rowCount}]: " + se.jdbcStatement.take(30).replace('\n', ' '))
+ }
+
+ def resultSetIterationEnded(statementInvocationId: String, iterationEndTime: Long, rowCount: Int, iterationCompleted: Boolean): Unit = {
+
+ }
+
+ def report(metrics: TransactionMetrics) {
+ metrics.queryCount(queryCount)
+ metrics.queryTime(queryTime)
+ val timePerQuery = if (queryCount > 0) (queryTime.toDouble / queryCount.toDouble).toInt else 0
+ metrics.timePerQuery(timePerQuery)
+ }
+}
diff --git a/sql/src/main/scala/io/greenbus/sql/impl/SessionDbConnection.scala b/sql/src/main/scala/io/greenbus/sql/impl/SessionDbConnection.scala
new file mode 100644
index 0000000..9b1fe5e
--- /dev/null
+++ b/sql/src/main/scala/io/greenbus/sql/impl/SessionDbConnection.scala
@@ -0,0 +1,71 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.sql.impl
+
+import org.squeryl.{ PrimitiveTypeMode, Session }
+import java.sql.Connection
+import io.greenbus.sql.DbConnection
+import com.typesafe.scalalogging.slf4j.Logging
+import org.squeryl.logging.StatisticsListener
+
+/**
+ * default DbConnection implementation that takes a session factory function and generates
+ * a new session whenever a new transaction is needed
+ */
+class SessionDbConnection(sessionFactory: (Option[StatisticsListener]) => Session) extends DbConnection with Logging {
+
+ def transaction[A](fun: => A): A = {
+ transaction(None)(fun)
+ }
+
+ def transaction[A](listener: Option[StatisticsListener])(fun: => A): A = {
+ val session = sessionFactory(listener)
+ if (logger.underlying.isTraceEnabled) {
+ session.setLogger(s => logger.trace(s))
+ }
+
+ val result = PrimitiveTypeMode.using(session) {
+ PrimitiveTypeMode.transaction(session) {
+ fun
+ }
+ }
+ result
+ }
+
+ def inTransaction[A](fun: => A): A = {
+
+ if (Session.hasCurrentSession) fun
+ else transaction(fun)
+
+ }
+
+ def underlyingConnection[A](fun: (Connection) => A) = {
+ val c = if (Session.hasCurrentSession) Session.currentSession.connection
+ else sessionFactory(None).connection
+
+ try {
+ fun(c)
+ } finally {
+ // return the connection to the pool
+ c.close()
+ }
+
+ }
+
+}
diff --git a/sql/src/main/scala/io/greenbus/sql/impl/SlowQueryTracing.scala b/sql/src/main/scala/io/greenbus/sql/impl/SlowQueryTracing.scala
new file mode 100755
index 0000000..0c177f3
--- /dev/null
+++ b/sql/src/main/scala/io/greenbus/sql/impl/SlowQueryTracing.scala
@@ -0,0 +1,52 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.sql.impl
+
+import org.squeryl.internals.{ StatementWriter, DatabaseAdapter }
+import org.squeryl.Session
+import java.sql.SQLException
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.util.Timing
+
+trait SlowQueryTracing extends DatabaseAdapter with Logging {
+
+ val slowQueryTimeMilli: Long
+
+ override def execFailSafeExecute(sw: StatementWriter, silenceException: SQLException => Boolean): Unit = {
+ val timingFun = monitorSlowQueries(slowQueryTimeMilli, sw) _
+ Timing.time(timingFun) {
+ super.execFailSafeExecute(sw, silenceException)
+ }
+ }
+
+ override def exec[A](s: Session, sw: StatementWriter)(block: Iterable[AnyRef] => A): A = {
+ val timingFun = monitorSlowQueries(slowQueryTimeMilli, sw) _
+ Timing.time[A](timingFun) {
+ super.exec[A](s, sw)(block)
+ }
+ }
+
+ def monitorSlowQueries(maxTimeMilli: Long, sw: StatementWriter)(actualTimeMilli: Long): Unit = {
+ logger.trace("Db time: " + actualTimeMilli + "; " + sw.toString().take(40).replace('\n', ' '))
+ if (actualTimeMilli >= maxTimeMilli) {
+ logger.info("SlowQuery, actual: " + actualTimeMilli + " ms, max allowed: " + maxTimeMilli + "ms, query: " + sw.toString)
+ }
+ }
+}
\ No newline at end of file
diff --git a/sql/src/main/scala/io/greenbus/sql/postgres/Connector.scala b/sql/src/main/scala/io/greenbus/sql/postgres/Connector.scala
new file mode 100755
index 0000000..37a3acc
--- /dev/null
+++ b/sql/src/main/scala/io/greenbus/sql/postgres/Connector.scala
@@ -0,0 +1,53 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.sql.postgres
+
+import org.squeryl.Session
+import org.squeryl.adapters.PostgreSqlAdapter
+
+import com.typesafe.scalalogging.slf4j.Logging
+import io.greenbus.sql.{ DbConnector, SqlSettings }
+import io.greenbus.sql.impl.{ SessionDbConnection, SlowQueryTracing }
+import org.squeryl.logging.StatisticsListener
+
+class Connector extends DbConnector with Logging {
+
+ def connect(sqlSettings: SqlSettings) = {
+
+ val pool = new org.apache.commons.dbcp.BasicDataSource
+ pool.setDriverClassName("org.postgresql.Driver")
+ pool.setUrl(sqlSettings.url)
+ pool.setUsername(sqlSettings.user)
+ pool.setPassword(sqlSettings.password)
+ pool.setMaxActive(sqlSettings.poolMaxActive)
+
+ logger.info("Initializing postgres connection pool: " + sqlSettings)
+
+ def sessionFactory(listener: Option[StatisticsListener]) = {
+ new Session(
+ pool.getConnection(),
+ new PostgreSqlAdapter with SlowQueryTracing {
+ val slowQueryTimeMilli = sqlSettings.slowQueryTimeMilli
+ },
+ listener)
+ }
+
+ new SessionDbConnection(sessionFactory)
+ }
+}
diff --git a/sql/src/test/scala/io/greenbus/sql/test/DatabaseUsingTestBase.scala b/sql/src/test/scala/io/greenbus/sql/test/DatabaseUsingTestBase.scala
new file mode 100755
index 0000000..edfb415
--- /dev/null
+++ b/sql/src/test/scala/io/greenbus/sql/test/DatabaseUsingTestBase.scala
@@ -0,0 +1,80 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.sql.test
+
+import org.scalatest.matchers.ShouldMatchers
+import org.scalatest.{ BeforeAndAfterAll, BeforeAndAfterEach, FunSuite }
+import io.greenbus.util.Timing
+import com.typesafe.scalalogging.slf4j.Logging
+import org.squeryl.Schema
+import io.greenbus.sql.{ SqlSettings, DbConnection, DbConnector }
+
+object ConnectionStorage {
+ private var lastConnection = Option.empty[DbConnection]
+ private var lastOptions = Option.empty[SqlSettings]
+
+ def connect(sqlSettings: SqlSettings): DbConnection = {
+ if (lastOptions != Some(sqlSettings)) {
+ lastConnection = Some(DbConnector.connect(sqlSettings))
+ lastOptions = Some(sqlSettings)
+ }
+ lastConnection.get
+ }
+
+ var dbNeedsReset = true
+}
+
+trait DatabaseUsingTestBaseNoTransaction extends FunSuite with ShouldMatchers with BeforeAndAfterAll with BeforeAndAfterEach with Logging {
+ lazy val dbConnection = ConnectionStorage.connect(SqlSettings.load("io.greenbus.test.cfg"))
+
+ def schemas: Seq[Schema]
+
+ protected def alwaysReset = true
+
+ override def beforeAll() {
+ if (ConnectionStorage.dbNeedsReset || alwaysReset) {
+ val prepareTime = Timing.benchmark {
+ dbConnection.transaction {
+ schemas.foreach { s =>
+ s.drop
+ s.create
+ }
+ }
+ }
+ logger.info("Prepared db in: " + prepareTime)
+ ConnectionStorage.dbNeedsReset = false
+ }
+ }
+
+ /**
+ * we only need to rebuild the database schema when a Non-Transaction-Safe test suite has been run.
+ */
+ protected def resetDbAfterTestSuite: Boolean
+ override def afterAll() {
+ ConnectionStorage.dbNeedsReset = resetDbAfterTestSuite
+ }
+}
+
+abstract class DatabaseUsingTestBase extends DatabaseUsingTestBaseNoTransaction with RunTestsInsideTransaction {
+ protected override def resetDbAfterTestSuite = false
+}
+
+abstract class DatabaseUsingTestNotTransactionSafe extends DatabaseUsingTestBaseNoTransaction {
+ protected override def resetDbAfterTestSuite = true
+}
\ No newline at end of file
diff --git a/sql/src/test/scala/io/greenbus/sql/test/RunTestsInsideTransaction.scala b/sql/src/test/scala/io/greenbus/sql/test/RunTestsInsideTransaction.scala
new file mode 100755
index 0000000..d4bdce3
--- /dev/null
+++ b/sql/src/test/scala/io/greenbus/sql/test/RunTestsInsideTransaction.scala
@@ -0,0 +1,80 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.sql.test
+
+import org.scalatest._
+import io.greenbus.sql.DbConnection
+
+trait RunTestsInsideTransaction extends FunSuite with BeforeAndAfterEach {
+
+ def dbConnection: DbConnection
+
+ class TransactionAbortException extends Exception
+
+ /**
+ * test classes shouldn't use beforeEach if using RunTestsInsideTransaction
+ */
+ final override def beforeEach() {}
+ final override def afterEach() {}
+
+ /**
+ * should be overriden by tests to run setup code inside same transaction
+ * as the test run.
+ */
+ def beforeEachInTransaction() {}
+
+ /**
+ * should be overriden by tests to run teardown code inside same transaction
+ * as the test run.
+ */
+ def afterEachInTransaction() {}
+
+ override def runTest(
+ testName: String,
+ reporter: Reporter,
+ stopper: Stopper,
+ configMap: Map[String, Any],
+ tracker: Tracker): Unit = {
+
+ // transaction will always be rolledback
+ neverCompletingTransaction {
+ beforeEachInTransaction()
+ super.runTest(testName, reporter, stopper, configMap, tracker)
+ afterEachInTransaction()
+ }
+ }
+
+ /**
+ * each test occur from within a transaction, that way when the test completes _all_ changes
+ * made during the test are reverted so each test gets a clean environment to test against
+ */
+ def neverCompletingTransaction[A](func: => A) = {
+ try {
+ dbConnection.transaction {
+ val result = func
+ // we abort the transaction if we get to here, so changes get rolled back
+ throw new TransactionAbortException
+
+ result
+ }
+ } catch {
+ case ex: TransactionAbortException =>
+ }
+ }
+}
\ No newline at end of file
diff --git a/util/pom.xml b/util/pom.xml
new file mode 100755
index 0000000..3e46acd
--- /dev/null
+++ b/util/pom.xml
@@ -0,0 +1,36 @@
+
+ 4.0.0
+
+
+ io.greenbus
+ greenbus-scala-base
+ 3.0.0
+ ../scala-base
+
+
+ greenbus-util
+ jar
+
+
+
+ Apache 2
+ http://www.apache.org/licenses/LICENSE-2.0.txt
+ A business-friendly OSS license
+
+
+
+
+
+
+
+ com.mycila.maven-license-plugin
+ maven-license-plugin
+
+
+
+
+
+
+
+
diff --git a/util/src/main/scala/io/greenbus/jmx/MBeanUtils.scala b/util/src/main/scala/io/greenbus/jmx/MBeanUtils.scala
new file mode 100644
index 0000000..be766e5
--- /dev/null
+++ b/util/src/main/scala/io/greenbus/jmx/MBeanUtils.scala
@@ -0,0 +1,37 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.jmx
+
+import javax.management.ObjectName
+
+object MBeanUtils {
+
+ def objectName(domain: String, name: String): ObjectName = {
+ new ObjectName(domain + ":name=" + name)
+ }
+
+ def objectName(domain: String, tags: List[Tag], name: String): ObjectName = {
+ val tagsText = tags match {
+ case Nil => ""
+ case full => tags.map(t => t.name + "=" + t.value).mkString(",") + ","
+ }
+ val text = domain + ":" + tagsText + "name=" + name
+ new ObjectName(text)
+ }
+}
\ No newline at end of file
diff --git a/util/src/main/scala/io/greenbus/jmx/MetricValue.scala b/util/src/main/scala/io/greenbus/jmx/MetricValue.scala
new file mode 100644
index 0000000..087cd98
--- /dev/null
+++ b/util/src/main/scala/io/greenbus/jmx/MetricValue.scala
@@ -0,0 +1,98 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.jmx
+
+import java.util.concurrent.atomic.AtomicInteger
+
+trait MetricValue {
+ def update(i: Int)
+
+ def reset()
+
+ def value: AnyRef
+}
+
+object MetricValue {
+
+ abstract class IntMetricHolder extends MetricValue {
+
+ protected val current = new AtomicInteger(0)
+
+ def update(i: Int)
+
+ def value = current.get.asInstanceOf[AnyRef]
+
+ def reset() {
+ current.set(0)
+ }
+
+ }
+
+ class CounterMetric extends IntMetricHolder {
+ def update(i: Int) {
+ current.addAndGet(i)
+ }
+ }
+
+ class GaugeMetric extends IntMetricHolder {
+ def update(i: Int) {
+ current.set(i)
+ }
+ }
+
+ /**
+ * keeps a windowed average over the last size updates
+ */
+ class AverageMetric(size: Int) extends MetricValue {
+
+ private var next = 0
+ private val counts = new Array[Int](size)
+ private var sum = 0
+
+ def update(i: Int) {
+ synchronized {
+ sum -= counts.apply(next % size)
+ counts.update(next % size, i)
+ next += 1
+ sum += i
+ }
+ }
+
+ def reset() {
+ synchronized {
+ next = 0
+ sum = 0
+ Range(0, size).foreach(counts.update(_, 0))
+ }
+ }
+
+ def value = {
+ var s = 0
+ synchronized {
+ s = sum
+ }
+ // can look this up unsafely since after startup we dont actually
+ // need this value to be accurate
+ val num = if (next > size) size else next
+ val d = if (num == 0) 0 else s.toDouble / num
+ d.asInstanceOf[AnyRef]
+ }
+ }
+
+}
diff --git a/util/src/main/scala/io/greenbus/jmx/Metrics.scala b/util/src/main/scala/io/greenbus/jmx/Metrics.scala
new file mode 100755
index 0000000..f8f10d0
--- /dev/null
+++ b/util/src/main/scala/io/greenbus/jmx/Metrics.scala
@@ -0,0 +1,174 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.jmx
+
+import io.greenbus.util.Timing
+import scala.collection.mutable
+import management.ManagementFactory
+import javax.management.ObjectName
+
+trait Timer {
+ def apply[A](f: => A): A
+}
+
+trait MetricsContainer {
+ def add(name: String, v: MetricValue)
+ def get(name: String): MetricValue
+ def getAll: List[(String, MetricValue)]
+}
+
+object MetricsContainer {
+
+ def apply(): MetricsContainer = new DefaultMetricsContainer
+
+ class DefaultMetricsContainer extends MetricsContainer {
+ private val map = mutable.Map.empty[String, MetricValue]
+
+ def add(name: String, v: MetricValue) {
+ map += (name -> v)
+ }
+
+ def get(name: String) = {
+ map.get(name) getOrElse { throw new Exception("Failed to get metrics from metrics container " + name) }
+ }
+
+ def getAll: List[(String, MetricValue)] = map.toList
+ }
+}
+
+trait Metrics {
+ def gauge(name: String): (Int) => Unit
+ def counter(name: String): (Int) => Unit
+ def average(name: String): (Int) => Unit
+ def timer(name: String): Timer
+}
+
+object Metrics {
+
+ def apply(container: MetricsContainer): Metrics = new DefaultMetrics(container)
+
+ class DefaultTimer(metric: MetricValue.AverageMetric) extends Timer {
+ def apply[A](f: => A) = {
+ Timing.time(x => metric.update(x.toInt))(f)
+ }
+ }
+
+ class DefaultMetrics(container: MetricsContainer) extends Metrics {
+
+ private def register(name: String, v: MetricValue) = {
+ container.add(name, v)
+ container.get(name).update(_)
+ }
+
+ def gauge(name: String) = {
+ register(name, new MetricValue.GaugeMetric)
+ }
+
+ def counter(name: String) = {
+ register(name, new MetricValue.CounterMetric)
+ }
+
+ def average(name: String) = {
+ register(name, new MetricValue.AverageMetric(30))
+ }
+ def average(name: String, size: Int) = {
+ register(name, new MetricValue.AverageMetric(size))
+ }
+
+ def timer(name: String) = {
+ val metric = new MetricValue.AverageMetric(30)
+ container.add(name, metric)
+ new DefaultTimer(metric)
+ }
+ }
+}
+
+trait MetricsSource {
+ def metrics(name: String): Metrics
+ def metrics(name: String, subTags: Tag): Metrics
+ def metrics(name: String, subTags: List[Tag]): Metrics
+}
+
+trait MetricsManager extends MetricsSource {
+ def register()
+ def unregister()
+}
+
+case class Tag(name: String, value: String)
+
+object MetricsManager {
+
+ val instanceTag = "instance"
+
+ def apply(domain: String): MetricsManager = new DefaultMetricsManager(domain, Nil)
+ def apply(domain: String, instance: String): MetricsManager = new DefaultMetricsManager(domain, List(Tag(instanceTag, instance)))
+
+ case class MetricsInfo(domain: String, tags: List[Tag], name: String, container: MetricsContainer)
+
+ class DefaultMetricsManager(domain: String, tags: List[Tag]) extends MetricsManager {
+
+ private var stash = List.empty[MetricsInfo]
+ private var registered = List.empty[ObjectName]
+
+ private def addMetrics(name: String, tags: List[Tag]): Metrics = {
+ val container = MetricsContainer()
+ val info = MetricsInfo(domain, tags, name, container)
+ stash ::= info
+ Metrics(container)
+ }
+
+ def metrics(name: String, subTag: Tag): Metrics = {
+ addMetrics(name, tags ::: List(subTag))
+ }
+ def metrics(name: String, subTags: List[Tag]): Metrics = {
+ addMetrics(name, tags ::: subTags)
+ }
+
+ def metrics(name: String): Metrics = {
+ addMetrics(name, tags)
+ }
+
+ // TODO: invariants on calling only once and not calling metrics again afterwards
+ def register() {
+
+ val server = ManagementFactory.getPlatformMBeanServer
+
+ registered = stash.map {
+ case MetricsInfo(domain, tags, name, container) => {
+ val objectName = MBeanUtils.objectName(domain, tags, name)
+
+ // Exception will be thrown if we try to register twice,
+ // since we may have modified what attributes the bean has we just replace it
+ if (server.isRegistered(objectName)) {
+ server.unregisterMBean(objectName)
+ }
+
+ server.registerMBean(new MetricsMBean(objectName, container), objectName)
+
+ objectName
+ }
+ }
+ }
+
+ def unregister() {
+ val server = ManagementFactory.getPlatformMBeanServer
+ registered.foreach(name => server.unregisterMBean(name))
+ }
+ }
+}
diff --git a/util/src/main/scala/io/greenbus/jmx/MetricsMBean.scala b/util/src/main/scala/io/greenbus/jmx/MetricsMBean.scala
new file mode 100644
index 0000000..629bcc8
--- /dev/null
+++ b/util/src/main/scala/io/greenbus/jmx/MetricsMBean.scala
@@ -0,0 +1,56 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.jmx
+
+import javax.management._
+
+class MetricsMBean(name: ObjectName, container: MetricsContainer) extends DynamicMBean {
+
+ def getName: ObjectName = name
+
+ def getAttribute(attribute: String): AnyRef = {
+ container.get(attribute).value
+ }
+
+ def getAttributes(attributes: Array[String]): AttributeList = {
+ val results = new AttributeList()
+ attributes.foreach { name =>
+ results.add(new Attribute(name, container.get(name).value))
+ }
+ results
+ }
+
+ def getMBeanInfo: MBeanInfo = {
+ val attributes = container.getAll.map(kv => new MBeanAttributeInfo(kv._1, kv._2.getClass.getSimpleName, "", true, false, false))
+
+ new MBeanInfo(this.getClass.getName, "GreenBus MetricsMBean", attributes.toArray, null, null, null)
+ }
+
+ def setAttributes(attributes: AttributeList): AttributeList = {
+ throw new UnsupportedOperationException("setAttributes is not implemented")
+ }
+
+ def invoke(actionName: String, params: Array[AnyRef], signature: Array[String]): AnyRef = {
+ throw new UnsupportedOperationException("invoke is not implemented")
+ }
+
+ def setAttribute(attribute: Attribute) {
+ throw new UnsupportedOperationException("setAttribute is not implemented")
+ }
+}
diff --git a/util/src/main/scala/io/greenbus/util/Optional.scala b/util/src/main/scala/io/greenbus/util/Optional.scala
new file mode 100644
index 0000000..43a97eb
--- /dev/null
+++ b/util/src/main/scala/io/greenbus/util/Optional.scala
@@ -0,0 +1,36 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.util
+
+object Optional {
+
+ /**
+ * calls the has (or all of the has) functions, taking advantage of short circuiting, to return None if any of the has functions
+ * fail or the result of the get function wrapped in Some.
+ */
+ def optGet[A](has: => Boolean, get: => A): Option[A] = if (has) Some(get) else None
+ def optGet[A](has: => Boolean, has1: => Boolean, get: => A): Option[A] = if (has && has1) Some(get) else None
+ def optGet[A](has: => Boolean, has1: => Boolean, has2: => Boolean, get: => A): Option[A] = if (has && has1 && has2) Some(get) else None
+
+ // if (isTrue) Some(obj) else None ---> isTrue thenGet obj | isTrue ? obj
+ implicit def boolWrapper(b: Boolean) = new OptionalBoolean(b)
+ class OptionalBoolean(b: Boolean) {
+ def thenGet[A](t: => A): Option[A] = if (b) Some(t) else None
+ }
+}
diff --git a/util/src/main/scala/io/greenbus/util/PropertyReading.scala b/util/src/main/scala/io/greenbus/util/PropertyReading.scala
new file mode 100644
index 0000000..b8e65c7
--- /dev/null
+++ b/util/src/main/scala/io/greenbus/util/PropertyReading.scala
@@ -0,0 +1,86 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.util
+
+import java.util.Properties
+import scala.collection.JavaConversions._
+import java.io.FileInputStream
+
+class ConfigurationException(message: String) extends Exception(message)
+
+object PropertyReading {
+
+ def loadFile(filename: String): Map[String, String] = {
+ val properties = new Properties
+ try {
+ properties.load(new FileInputStream(filename))
+ asMap(properties)
+ } catch {
+ case ex: Throwable =>
+ throw new ConfigurationException("Problem loading configuration file: " + ex.getMessage)
+ }
+ }
+
+ private def asMap(properties: Properties): Map[String, String] = {
+ properties.stringPropertyNames().map(name => (name, properties.getProperty(name))).toMap
+ }
+
+ private def missing(name: String) = new ConfigurationException(s"Missing property: '$name'")
+
+ def optional(props: Map[String, String], name: String): Option[String] = {
+ Option(System.getProperty(name)) orElse props.get(name)
+ }
+
+ def get(props: Map[String, String], name: String): String = {
+ optional(props, name) getOrElse (throw missing(name))
+ }
+
+ def optionalLong(props: Map[String, String], name: String): Option[Long] = {
+ checkSignedInt(name) {
+ optional(props, name).map(_.toLong)
+ }
+ }
+
+ def getLong(props: Map[String, String], name: String): Long = {
+ checkSignedInt(name) {
+ get(props, name).toLong
+ }
+ }
+
+ def optionalInt(props: Map[String, String], name: String): Option[Int] = {
+ checkSignedInt(name) {
+ optional(props, name).map(_.toInt)
+ }
+ }
+
+ def getInt(props: Map[String, String], name: String): Int = {
+ checkSignedInt(name) {
+ get(props, name).toInt
+ }
+ }
+
+ private def checkSignedInt[A](name: String)(fun: => A): A = {
+ try {
+ fun
+ } catch {
+ case ex: NumberFormatException =>
+ throw new ConfigurationException(s"Configuration property '$name' must be a signed integer.")
+ }
+ }
+}
diff --git a/util/src/main/scala/io/greenbus/util/Timing.scala b/util/src/main/scala/io/greenbus/util/Timing.scala
new file mode 100644
index 0000000..d1d2c19
--- /dev/null
+++ b/util/src/main/scala/io/greenbus/util/Timing.scala
@@ -0,0 +1,60 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.util
+
+/**
+ * methods for determining elapsed time for a supplied function.
+ */
+object Timing {
+
+ /**
+ * very simple class to make measuring elapsed time easier in benchmark and test code
+ */
+ trait Stopwatch {
+ def elapsed: Long
+ }
+ object Stopwatch {
+ def start: Stopwatch = new Stopwatch {
+ private val startTime = System.nanoTime()
+ def elapsed: Long = convertNanoToMilli(System.nanoTime() - startTime)
+ }
+ }
+
+ /**
+ * Runs a block of code and returns how long it took in milliseconds (not the return value of the block)
+ */
+ def benchmark[A](fun: => A): Long = {
+ val stopwatch = Stopwatch.start
+ fun
+ stopwatch.elapsed
+ }
+
+ /**
+ * Runs a block of code and passes the length of time it took to another function
+ */
+ def time[A](timingFun: Long => Unit)(fun: => A): A = {
+ val stopwatch = Stopwatch.start
+ val ret = fun
+ timingFun(stopwatch.elapsed)
+ ret
+ }
+
+ private def convertNanoToMilli[A](value: Long): Long = scala.math.floor(value / 1000000d).toLong
+
+}
\ No newline at end of file
diff --git a/util/src/main/scala/io/greenbus/util/UserSettings.scala b/util/src/main/scala/io/greenbus/util/UserSettings.scala
new file mode 100755
index 0000000..4c30c60
--- /dev/null
+++ b/util/src/main/scala/io/greenbus/util/UserSettings.scala
@@ -0,0 +1,39 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.util
+
+object UserSettings {
+ import io.greenbus.util.PropertyReading._
+
+ def load(file: String): UserSettings = {
+ apply(loadFile(file))
+ }
+
+ def apply(props: Map[String, String]): UserSettings = {
+ UserSettings(
+ get(props, "io.greenbus.user.username"),
+ get(props, "io.greenbus.user.password"))
+ }
+}
+
+case class UserSettings(user: String, password: String) {
+
+ // custom to hide password
+ override def toString() = user + ",[password]"
+}
diff --git a/util/src/main/scala/io/greenbus/util/XmlHelper.scala b/util/src/main/scala/io/greenbus/util/XmlHelper.scala
new file mode 100644
index 0000000..29699a6
--- /dev/null
+++ b/util/src/main/scala/io/greenbus/util/XmlHelper.scala
@@ -0,0 +1,85 @@
+/**
+ * Copyright 2011-2016 Green Energy Corp.
+ *
+ * Licensed to Green Energy Corp (www.greenenergycorp.com) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. Green Energy
+ * Corp licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.greenbus.util
+
+import javax.xml.bind.Unmarshaller.Listener
+import javax.xml.bind.{ Unmarshaller, JAXBContext, Marshaller }
+import java.io._
+import javax.xml.stream.{ Location, XMLInputFactory }
+
+object XmlHelper {
+
+ def read[A](text: String, klass: Class[A]): A = read(text.getBytes("UTF-8"), klass)
+
+ def read[A](bytes: Array[Byte], klass: Class[A]): A = {
+ val ctx = JAXBContext.newInstance(klass)
+ val um = ctx.createUnmarshaller
+ val is = new ByteArrayInputStream(bytes)
+ um.unmarshal(is).asInstanceOf[A]
+ }
+
+ def read[A](file: File, klass: Class[A]): A = {
+ val ctx = JAXBContext.newInstance(klass)
+ val um = ctx.createUnmarshaller
+ um.unmarshal(file).asInstanceOf[A]
+ }
+
+ def readWithLocation[A](file: File, klass: Class[A]): (A, Map[Any, Location]) = {
+ val ctx = JAXBContext.newInstance(klass)
+
+ val xif = XMLInputFactory.newFactory()
+ val xsr = xif.createXMLStreamReader(new FileInputStream(file))
+
+ val um = ctx.createUnmarshaller
+
+ val mapBuilder = Map.newBuilder[Any, Location]
+
+ val listener = new Listener {
+ override def beforeUnmarshal(target: Any, parent: Any): Unit = {
+ mapBuilder += ((target, xsr.getLocation))
+ }
+ }
+
+ um.setListener(listener)
+ val result = um.unmarshal(xsr).asInstanceOf[A]
+
+ (result, mapBuilder.result())
+ }
+
+ def writeToFile[A](value: A, klass: Class[A], filewriter: FileWriter): Unit = {
+ val ctx = JAXBContext.newInstance(klass)
+ val m = ctx.createMarshaller
+ m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true)
+
+ m.marshal(value, filewriter)
+ }
+
+ def writeToString[A](value: A, klass: Class[A], formatted: Boolean = false): String = {
+ val ctx = JAXBContext.newInstance(klass)
+ val m = ctx.createMarshaller
+ if (formatted) {
+ m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true)
+ }
+ val sw = new StringWriter
+
+ m.marshal(value, sw)
+ sw.toString
+ }
+
+}