Add propertyIs filter

The existing property match filter tests a property
in the PropertiesChanged signal payload.

Add a match filter that tests any arbitrary property on
any object.

Change-Id: I1c238c03a3ccbf45f7b338693a4342fbd4f670c2
Signed-off-by: Brad Bishop <bradleyb@fuzziesquirrel.com>
diff --git a/Makefile.am b/Makefile.am
index 36edbca..16d35b9 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -14,6 +14,7 @@
 libmanager_la_SOURCES = \
 	xyz.openbmc_project.Inventory.Manager.cpp \
 	generated.cpp \
+	events.cpp \
 	manager.cpp
 libmanager_la_LIBADD = libextra.la
 
diff --git a/README.md b/README.md
index 5f479fc..c7c64a1 100644
--- a/README.md
+++ b/README.md
@@ -47,15 +47,38 @@
 
 * propertyChangedTo - Only match events when the specified property has
 the specified value.
+* propertyIs - Only match events when the specified property has
+the specified value.
 
 ----
 **propertyChangedTo**
 
+The property under test is obtained from an sdbus message
+generated from an org.freedesktop.DBus.Properties.PropertiesChanged
+signal payload.
+
 Supported arguments for the propertyChangedTo filter are:
 * interface - The interface hosting the property to be checked.
 * property - The property to check.
 * value - The value to check.
 
+----
+**propertyIs**
+
+The property under test is obtained by invoking
+org.freedesktop.Properties.Get on the specified interface.
+
+Supported arguments for the propertyIs filter are:
+* path - The object hosting the property to be checked.
+* interface - The interface hosting the property to be checked.
+* property - The property to check.
+* value - The value to check.
+* service - An optional DBus service name.
+
+The service argument is optional.  If provided that service will
+be called explicitly.  If omitted, the service will be obtained
+with an xyz.openbmc_project.ObjectMapper lookup.
+
 ---
 **actions**
 
diff --git a/events.cpp b/events.cpp
new file mode 100644
index 0000000..41a6e5f
--- /dev/null
+++ b/events.cpp
@@ -0,0 +1,99 @@
+/**
+ * Copyright © 2017 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "events.hpp"
+#include <sdbusplus/bus.hpp>
+
+namespace phosphor
+{
+namespace inventory
+{
+namespace manager
+{
+namespace filters
+{
+namespace details
+{
+namespace property_condition
+{
+
+bool PropertyConditionBase::operator()(
+    sdbusplus::bus::bus& bus,
+    sdbusplus::message::message&,
+    Manager&) const
+{
+    std::string host;
+
+    if (_service)
+    {
+        host.assign(_service);
+    }
+    else
+    {
+        auto mapperCall = bus.new_method_call(
+                              "xyz.openbmc_project.ObjectMapper",
+                              "/xyz/openbmc_project/ObjectMapper",
+                              "xyz.openbmc_project.ObjectMapper",
+                              "GetObject");
+        mapperCall.append(_path);
+        mapperCall.append(std::vector<std::string>({_iface}));
+
+        auto mapperResponseMsg = bus.call(mapperCall);
+        if (mapperResponseMsg.is_method_error())
+        {
+            return false;
+        }
+
+        std::map<std::string, std::vector<std::string>> mapperResponse;
+        mapperResponseMsg.read(mapperResponse);
+
+        if (mapperResponse.begin() == mapperResponse.end())
+        {
+            return false;
+        }
+
+        host = mapperResponse.begin()->first;
+
+        if (host == bus.get_unique_name())
+        {
+            // TODO I can't call myself here.
+            return false;
+        }
+    }
+    auto hostCall = bus.new_method_call(
+                        host.c_str(),
+                        _path.c_str(),
+                        "org.freedesktop.DBus.Properties",
+                        "Get");
+    hostCall.append(_iface);
+    hostCall.append(_property);
+
+    auto hostResponseMsg = bus.call(hostCall);
+    if (hostResponseMsg.is_method_error())
+    {
+        return false;
+    }
+
+    return eval(hostResponseMsg);
+}
+
+} // namespace property_condition
+} // namespace details
+} // namespace filters
+} // namespace manager
+} // namespace inventory
+} // namespace phosphor
+
+// vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
diff --git a/events.hpp b/events.hpp
index 80fcdae..45122a0 100644
--- a/events.hpp
+++ b/events.hpp
@@ -154,6 +154,120 @@
         U _condition;
 };
 
+/** @struct PropertyConditionBase
+ *  @brief Match filter functor that tests a property value.
+ *
+ *  Base class for PropertyCondition - factored out code that
+ *  doesn't need to be templated.
+ */
+struct PropertyConditionBase
+{
+        PropertyConditionBase() = delete;
+        virtual ~PropertyConditionBase() = default;
+        PropertyConditionBase(const PropertyConditionBase&) = delete;
+        PropertyConditionBase& operator=(const PropertyConditionBase&) = delete;
+        PropertyConditionBase(PropertyConditionBase&&) = default;
+        PropertyConditionBase& operator=(PropertyConditionBase&&) = default;
+
+        /** @brief Constructor
+         *
+         *  The service argument can be nullptr.  If something
+         *  else is provided the function will call the the
+         *  service directly.  If omitted, the function will
+         *  look up the service in the ObjectMapper.
+         *
+         *  @param path - The path of the object containing
+         *     the property to be tested.
+         *  @param iface - The interface hosting the property
+         *     to be tested.
+         *  @param property - The property to be tested.
+         *  @param service - The DBus service hosting the object.
+         */
+        PropertyConditionBase(
+            const char* path,
+            const char* iface,
+            const char* property,
+            const char* service) :
+            _path(path),
+            _iface(iface),
+            _property(property),
+            _service(service) {}
+
+        /** @brief Forward comparison to type specific implementation. */
+        virtual bool eval(sdbusplus::message::message&) const = 0;
+
+        /** @brief Test a property value.
+         *
+         * Make a DBus call and test the value of any property.
+         */
+        bool operator()(
+            sdbusplus::bus::bus&,
+            sdbusplus::message::message&,
+            Manager&) const;
+
+    private:
+        std::string _path;
+        std::string _iface;
+        std::string _property;
+        const char* _service;
+};
+
+/** @struct PropertyCondition
+ *  @brief Match filter functor that tests a property value.
+ *
+ *  @tparam T - The type of the property being tested.
+ *  @tparam U - The type of the condition checking functor.
+ */
+template <typename T, typename U>
+struct PropertyCondition final : public PropertyConditionBase
+{
+        PropertyCondition() = delete;
+        ~PropertyCondition() = default;
+        PropertyCondition(const PropertyCondition&) = delete;
+        PropertyCondition& operator=(const PropertyCondition&) = delete;
+        PropertyCondition(PropertyCondition&&) = default;
+        PropertyCondition& operator=(PropertyCondition&&) = default;
+
+        /** @brief Constructor
+         *
+         *  The service argument can be nullptr.  If something
+         *  else is provided the function will call the the
+         *  service directly.  If omitted, the function will
+         *  look up the service in the ObjectMapper.
+         *
+         *  @param path - The path of the object containing
+         *     the property to be tested.
+         *  @param iface - The interface hosting the property
+         *     to be tested.
+         *  @param property - The property to be tested.
+         *  @param condition - The test to run on the property.
+         *  @param service - The DBus service hosting the object.
+         */
+        PropertyCondition(
+            const char* path,
+            const char* iface,
+            const char* property,
+            U&& condition,
+            const char* service) :
+            PropertyConditionBase(path, iface, property, service),
+            _condition(std::forward<decltype(condition)>(condition)) {}
+
+        /** @brief Test a property value.
+         *
+         * Make a DBus call and test the value of any property.
+         */
+        bool eval(sdbusplus::message::message& msg) const override
+        {
+            sdbusplus::message::variant<T> value;
+            msg.read(value);
+            return _condition(
+                       std::forward<T>(value.template get<T>()));
+        }
+
+    private:
+        U _condition;
+};
+
 } // namespace property_condition
 } // namespace details
 
@@ -173,6 +287,24 @@
                iface, property, std::move(condition));
 }
 
+/** @brief Implicit type deduction for constructing PropertyCondition.  */
+template <typename T>
+auto propertyIs(
+    const char* path,
+    const char* iface,
+    const char* property,
+    T&& val,
+    const char* service = nullptr)
+{
+    auto condition = [val = std::forward<T>(val)](T && arg)
+    {
+        return arg == val;
+    };
+    using U = decltype(condition);
+    return details::property_condition::PropertyCondition<T, U>(
+               path, iface, property, std::move(condition), service);
+}
+
 } // namespace filters
 } // namespace manager
 } // namespace inventory
diff --git a/example/events.d/match2.yaml b/example/events.d/match2.yaml
index 047462d..a4e98f0 100644
--- a/example/events.d/match2.yaml
+++ b/example/events.d/match2.yaml
@@ -49,4 +49,36 @@
             paths:
               - /deleteme3
 
+    - name: Example Match
+      description: >
+          Destroys the /deleteme3 object when the value of
+          ExampleProperty3 on /testing/trigger4 is 99
+          and the value of ExampleProperty2 on /testing/trigger4
+          changes to "123".
+      type: match
+      signatures:
+          - type: signal
+            path: /testing/trigger4
+            interface: org.freedesktop.DBus.Properties
+            member: PropertiesChanged
+      filters:
+          - name: propertyChangedTo
+            interface: xyz.openbmc_project.Example.Iface2
+            property: ExampleProperty2
+            value:
+              value: 123
+              type: string
+          - name: propertyIs
+            path: /testing/trigger4
+            interface: xyz.openbmc_project.Example.Iface2
+            property: ExampleProperty3
+            service: phosphor.inventory.test.example
+            value:
+              value: 99
+              type: int64
+      actions:
+          - name: destroyObjects
+            paths:
+              - /deleteme3
+
 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
diff --git a/test/test.cpp b/test/test.cpp
index bd75dbb..e4b6a45 100644
--- a/test/test.cpp
+++ b/test/test.cpp
@@ -38,6 +38,9 @@
                       "/trigger2"s);
 const auto trigger3 = sdbusplus::message::object_path(EXAMPLE_ROOT +
                       "/trigger3"s);
+const auto trigger4 = sdbusplus::message::object_path(EXAMPLE_ROOT +
+                      "/trigger4"s);
+
 
 const sdbusplus::message::object_path relDeleteMeOne{"/deleteme1"};
 const sdbusplus::message::object_path relDeleteMeTwo{"/deleteme2"};
@@ -73,6 +76,8 @@
         ExampleIface1, ExampleIface2 > t2(bus, trigger2.str.c_str());
         sdbusplus::server::object::object <
         ExampleIface1, ExampleIface2 > t3(bus, trigger3.str.c_str());
+        sdbusplus::server::object::object <
+        ExampleIface1, ExampleIface2 > t4(bus, trigger4.str.c_str());
 
         while (!shutdown)
         {
@@ -233,6 +238,79 @@
         assert(!moreSignals);
     }
 
+    // Validate the propertyIs filter.
+    {
+        // Create an object to be deleted.
+        {
+            auto m = notify();
+            m.append(relDeleteMeThree);
+            m.append(obj);
+            b.call(m);
+        }
+
+        // Validate that the action does not run if the property doesn't match.
+        {
+            SignalQueue queue(
+                "path='" + root + "',member='InterfacesRemoved'");
+            auto m = set(trigger4.str);
+            m.append("xyz.openbmc_project.Example.Iface2");
+            m.append("ExampleProperty2");
+            m.append(sdbusplus::message::variant<std::string>("123"));
+            b.call(m);
+            auto sig{queue.pop()};
+            assert(!sig);
+        }
+
+        // Validate that the action does run if the property matches.
+        {
+            // Set ExampleProperty2 to something else to the 123 filter
+            // matches.
+            SignalQueue queue(
+                "path='" + root + "',member='InterfacesRemoved'");
+            auto m = set(trigger4.str);
+            m.append("xyz.openbmc_project.Example.Iface2");
+            m.append("ExampleProperty2");
+            m.append(sdbusplus::message::variant<std::string>("xyz"));
+            b.call(m);
+            auto sig{queue.pop()};
+            assert(!sig);
+        }
+        {
+            // Set ExampleProperty3 to 99.
+            SignalQueue queue(
+                "path='" + root + "',member='InterfacesRemoved'");
+            auto m = set(trigger4.str);
+            m.append("xyz.openbmc_project.Example.Iface2");
+            m.append("ExampleProperty3");
+            m.append(sdbusplus::message::variant<int64_t>(99));
+            b.call(m);
+            auto sig{queue.pop()};
+            assert(!sig);
+        }
+        {
+            SignalQueue queue(
+                "path='" + root + "',member='InterfacesRemoved'");
+            auto m = set(trigger4.str);
+            m.append("xyz.openbmc_project.Example.Iface2");
+            m.append("ExampleProperty2");
+            m.append(sdbusplus::message::variant<std::string>("123"));
+            b.call(m);
+
+            sdbusplus::message::object_path sigpath;
+            std::vector<std::string> interfaces;
+            {
+                std::vector<std::string> interfaces;
+                auto sig{queue.pop()};
+                assert(sig);
+                sig.read(sigpath);
+                assert(sigpath == deleteMeThree);
+                sig.read(interfaces);
+                std::sort(interfaces.begin(), interfaces.end());
+                assert(hasInterfaces(interfaces, obj));
+            }
+        }
+    }
+
     // Make sure DBus signals are handled.
     {
         // Create some objects to be deleted by an action.