Add property watches

Property watches cache DBus property values given an externally
supplied index of property names and paths, in an externally
supplied storage location.

Change-Id: I155081da88c3ab0e4f6a13b012fc9719203b1888
Signed-off-by: Brad Bishop <bradleyb@fuzziesquirrel.com>
diff --git a/src/Makefile.am b/src/Makefile.am
index 0dfe63b..3b4d9cf 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -8,7 +8,8 @@
 phosphor_dbus_monitor_SOURCES = \
 	functor.cpp \
 	main.cpp \
-	monitor.cpp
+	monitor.cpp \
+	propertywatch.cpp
 phosphor_dbus_monitor_LDADD = \
 	$(SDBUSPLUS_LIBS) \
 	$(PHOSPHOR_LOGGING_LIBS)
diff --git a/src/data_types.hpp b/src/data_types.hpp
index 653b7c3..af0ef68 100644
--- a/src/data_types.hpp
+++ b/src/data_types.hpp
@@ -69,6 +69,17 @@
                           std::string,
                           sdbusplus::message::variant<T >>;
 
+/** @brief Lookup index for properties . */
+// *INDENT-OFF*
+using PropertyIndex = TupleRefMap <
+        TupleOfRefs<
+            const std::string,
+            const std::string,
+            any_ns::any>,
+        const std::string,
+        const std::string,
+        const std::string >;
+// *INDENT-ON*
 } // namespace monitoring
 } // namespace dbus
 } // namespace phosphor
diff --git a/src/example/example.yaml b/src/example/example.yaml
index 3d60396..4e216cd 100644
--- a/src/example/example.yaml
+++ b/src/example/example.yaml
@@ -44,3 +44,12 @@
     - interface: xyz.openbmc_project.Sensor.Value
       meta: property
       property: ValueB
+
+- name: example property watch
+  description: >
+    'A property watch instructs PDM to maintain a cache of the state
+    of the specified properties on the specified DBus objects.'
+  class: watch
+  watch: property
+  paths: example path group
+  properties: example property group
diff --git a/src/pdmgen.py b/src/pdmgen.py
index 61546eb..070c118 100755
--- a/src/pdmgen.py
+++ b/src/pdmgen.py
@@ -208,6 +208,29 @@
         super(Property, self).setup(objs)
 
 
+class Instance(ConfigEntry):
+    '''Property/Path association.'''
+
+    def __init__(self, *a, **kw):
+        super(Instance, self).__init__(**kw)
+
+    def setup(self, objs):
+        '''Resolve elements to indicies.'''
+
+        self.interface = get_index(
+            objs, 'interface', self.name['property']['interface'])
+        self.prop = get_index(
+            objs, 'propertyname', self.name['property']['property'])
+        self.propmeta = get_index(
+            objs, 'meta', self.name['property']['meta'])
+        self.path = get_index(
+            objs, 'pathname', self.name['path']['path'])
+        self.pathmeta = get_index(
+            objs, 'meta', self.name['path']['meta'])
+
+        super(Instance, self).setup(objs)
+
+
 class Group(ConfigEntry):
     '''Pop the members keyword for groups.'''
 
@@ -294,6 +317,102 @@
         super(GroupOfProperties, self).setup(objs)
 
 
+class GroupOfInstances(ImplicitGroup):
+    '''A group of property instances.'''
+
+    def __init__(self, *a, **kw):
+        super(GroupOfInstances, self).__init__(**kw)
+
+    def setup(self, objs):
+        '''Resolve group members.'''
+
+        def map_member(x):
+            path = get_index(objs, 'pathname', x['path']['path'])
+            pathmeta = get_index(objs, 'meta', x['path']['meta'])
+            interface = get_index(
+                objs, 'interface', x['property']['interface'])
+            prop = get_index(objs, 'propertyname', x['property']['property'])
+            propmeta = get_index(objs, 'meta', x['property']['meta'])
+            instance = get_index(objs, 'instance', x)
+
+            return (path, pathmeta, interface, prop, propmeta, instance)
+
+        self.members = map(
+            map_member,
+            self.members)
+
+        super(GroupOfInstances, self).setup(objs)
+
+
+class HasPropertyIndex(ConfigEntry):
+    '''Handle config file directives that require an index to be
+    constructed.'''
+
+    def __init__(self, *a, **kw):
+        self.paths = kw.pop('paths')
+        self.properties = kw.pop('properties')
+        super(HasPropertyIndex, self).__init__(**kw)
+
+    def factory(self, objs):
+        '''Create a group of instances for this index.'''
+
+        members = []
+        path_group = get_index(
+            objs, 'pathgroup', self.paths, config=self.configfile)
+        property_group = get_index(
+            objs, 'propertygroup', self.properties, config=self.configfile)
+
+        for path in objs['pathgroup'][path_group].members:
+            for prop in objs['propertygroup'][property_group].members:
+                member = {
+                    'path': path,
+                    'property': prop,
+                }
+                members.append(member)
+
+        args = {
+            'members': members,
+            'class': 'instancegroup',
+            'instancegroup': 'instance',
+            'name': '{0} {1}'.format(self.paths, self.properties)
+        }
+
+        group = GroupOfInstances(configfile=self.configfile, **args)
+        add_unique(group, objs, config=self.configfile)
+        group.factory(objs)
+
+        super(HasPropertyIndex, self).factory(objs)
+
+    def setup(self, objs):
+        '''Resolve path, property, and instance groups.'''
+
+        self.instances = get_index(
+            objs,
+            'instancegroup',
+            '{0} {1}'.format(self.paths, self.properties),
+            config=self.configfile)
+        self.paths = get_index(
+            objs,
+            'pathgroup',
+            self.paths,
+            config=self.configfile)
+        self.properties = get_index(
+            objs,
+            'propertygroup',
+            self.properties,
+            config=self.configfile)
+        self.datatype = objs['propertygroup'][self.properties].datatype
+
+        super(HasPropertyIndex, self).setup(objs)
+
+
+class PropertyWatch(HasPropertyIndex):
+    '''Handle the property watch config file directive.'''
+
+    def __init__(self, *a, **kw):
+        super(PropertyWatch, self).__init__(**kw)
+
+
 class Everything(Renderer):
     '''Parse/render entry point.'''
 
@@ -315,6 +434,12 @@
             'property': {
                 'element': Property,
             },
+            'watch': {
+                'property': PropertyWatch,
+            },
+            'instance': {
+                'element': Instance,
+            },
         }
 
         if cls not in class_map:
@@ -401,6 +526,9 @@
         self.properties = kw.pop('property', [])
         self.propertynames = kw.pop('propertyname', [])
         self.propertygroups = kw.pop('propertygroup', [])
+        self.instances = kw.pop('instance', [])
+        self.instancegroups = kw.pop('instancegroup', [])
+        self.watches = kw.pop('watch', [])
 
         super(Everything, self).__init__(**kw)
 
@@ -419,6 +547,9 @@
                     pathmeta=self.pathmeta,
                     pathgroups=self.pathgroups,
                     propertygroups=self.propertygroups,
+                    instances=self.instances,
+                    watches=self.watches,
+                    instancegroups=self.instancegroups,
                     indent=Indent()))
 
 if __name__ == '__main__':
diff --git a/src/propertywatch.cpp b/src/propertywatch.cpp
new file mode 100644
index 0000000..defde78
--- /dev/null
+++ b/src/propertywatch.cpp
@@ -0,0 +1,49 @@
+/**
+ * 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 "propertywatchimpl.hpp"
+
+namespace phosphor
+{
+namespace dbus
+{
+namespace monitoring
+{
+
+/** @brief convert index.
+ *
+ *  An optimal start implementation requires objects organized in the
+ *  same structure as the mapper response.  The convert method reorganizes
+ *  the flat structure of the index to match.
+ *
+ *  @param[in] index - The index to be converted.
+ */
+MappedPropertyIndex convert(const PropertyIndex& index)
+{
+    MappedPropertyIndex m;
+
+    for (const auto& i : index)
+    {
+        const auto& path = std::get<0>(i.first);
+        const auto& interface = std::get<1>(i.first);
+        const auto& property = std::get<2>(i.first);
+        m[path][interface].push_back(property);
+    }
+
+    return m;
+}
+} // namespace monitoring
+} // namespace dbus
+} // namespace phosphor
diff --git a/src/propertywatch.hpp b/src/propertywatch.hpp
new file mode 100644
index 0000000..59db599
--- /dev/null
+++ b/src/propertywatch.hpp
@@ -0,0 +1,167 @@
+/**
+ * @file propertywatch.hpp
+ * @brief PropertyWatch class declarations.
+ *
+ * In general class users should include propertywatchimpl.hpp instead to avoid
+ * link failures.
+ */
+#pragma once
+
+#include "data_types.hpp"
+#include "watch.hpp"
+
+namespace phosphor
+{
+namespace dbus
+{
+namespace monitoring
+{
+
+/** @class PropertyWatch
+ *  @brief Type agnostic, factored out logic for property watches.
+ *
+ *  A property watch maintains the state of one or more DBus properties
+ *  as specified by the supplied index.
+ */
+template <typename DBusInterfaceType>
+class PropertyWatch : public Watch
+{
+    public:
+        PropertyWatch() = delete;
+        PropertyWatch(const PropertyWatch&) = delete;
+        PropertyWatch(PropertyWatch&&) = default;
+        PropertyWatch& operator=(const PropertyWatch&) = delete;
+        PropertyWatch& operator=(PropertyWatch&&) = default;
+        virtual ~PropertyWatch() = default;
+        explicit PropertyWatch(const PropertyIndex& watchIndex)
+            : Watch(), index(watchIndex), alreadyRan(false) {}
+
+        /** @brief Start the watch.
+         *
+         *  Watch start interface implementation for PropertyWatch.
+         */
+        void start() override;
+
+        /** @brief Update properties.
+         *
+         *  Subclasses to query the properties specified by the index
+         *  and update the cache.
+         *
+         *  @param[in] busName - The busname hosting the interface to query.
+         *  @param[in] path - The path of the interface to query.
+         *  @param[in] interface - The interface to query.
+         */
+        virtual void updateProperties(
+            const std::string& busName,
+            const std::string& path,
+            const std::string& interface) = 0;
+
+        /** @brief Dbus signal callback for PropertiesChanged.
+         *
+         *  Subclasses to update the cache.
+         *
+         *  @param[in] message - The org.freedesktop.DBus.PropertiesChanged
+         *               message.
+         *  @param[in] path - The path associated with the message.
+         *  @param[in] interface - The interface associated with the message.
+         */
+        virtual void propertiesChanged(
+            sdbusplus::message::message&,
+            const std::string& path,
+            const std::string& interface) = 0;
+
+        /** @brief Dbus signal callback for InterfacesAdded.
+         *
+         *  Subclasses to update the cache.
+         *
+         *  @param[in] msg - The org.freedesktop.DBus.PropertiesChanged
+         *               message.
+         */
+        virtual void interfacesAdded(sdbusplus::message::message& msg) = 0;
+
+    protected:
+
+        /** @brief Property names and their associated storage. */
+        const PropertyIndex& index;
+
+    private:
+
+        /** @brief The start method should only be invoked once. */
+        bool alreadyRan;
+};
+
+/** @class PropertyWatchOfType
+ *  @brief Type specific logic for PropertyWatch.
+ *
+ *  @tparam DBusInterfaceType - DBus access delegate.
+ *  @tparam T - The type of the properties being watched.
+ */
+template <typename T, typename DBusInterfaceType>
+class PropertyWatchOfType : public PropertyWatch<DBusInterfaceType>
+{
+    public:
+        PropertyWatchOfType() = default;
+        PropertyWatchOfType(const PropertyWatchOfType&) = delete;
+        PropertyWatchOfType(PropertyWatchOfType&&) = default;
+        PropertyWatchOfType& operator=(const PropertyWatchOfType&) = delete;
+        PropertyWatchOfType& operator=(PropertyWatchOfType&&) = default;
+        ~PropertyWatchOfType() = default;
+        explicit PropertyWatchOfType(
+            const PropertyIndex& watchIndex) :
+            PropertyWatch<DBusInterfaceType>(watchIndex) {}
+
+        /** @brief PropertyMatch implementation for PropertyWatchOfType.
+         *
+         *  @param[in] busName - The busname hosting the interface to query.
+         *  @param[in] path - The path of the interface to query.
+         *  @param[in] interface - The interface to query.
+         */
+        void updateProperties(
+            const std::string& busName,
+            const std::string& path,
+            const std::string& interface) override;
+
+        /** @brief PropertyMatch implementation for PropertyWatchOfType.
+         *
+         *  @param[in] msg - The org.freedesktop.DBus.PropertiesChanged
+         *               message.
+         *  @param[in] path - The path associated with the message.
+         *  @param[in] interface - The interface associated with the message.
+         */
+        void propertiesChanged(
+            sdbusplus::message::message& msg,
+            const std::string& path,
+            const std::string& interface) override;
+
+        /** @brief DBus agnostic implementation of interfacesAdded.
+         *
+         *  @param[in] path - The path of the properties that changed.
+         *  @param[in] interface - The interface of the properties that
+         *                  changed.
+         *  @param[in] properites - The properties that changed.
+         */
+        void propertiesChanged(
+            const std::string& path,
+            const std::string& interface,
+            const PropertiesChanged<T>& properties);
+
+        /** @brief PropertyMatch implementation for PropertyWatchOfType.
+         *
+         *  @param[in] msg - The org.freedesktop.DBus.PropertiesChanged
+         *               message.
+         */
+        void interfacesAdded(sdbusplus::message::message& msg) override;
+
+        /** @brief DBus agnostic implementation of interfacesAdded.
+         *
+         *  @param[in] path - The path of the added interfaces.
+         *  @param[in] interfaces - The added interfaces.
+         */
+        void interfacesAdded(
+            const std::string& path,
+            const InterfacesAdded<T>& interfaces);
+};
+
+} // namespace monitoring
+} // namespace dbus
+} // namespace phosphor
diff --git a/src/propertywatchimpl.hpp b/src/propertywatchimpl.hpp
new file mode 100644
index 0000000..46799ed
--- /dev/null
+++ b/src/propertywatchimpl.hpp
@@ -0,0 +1,183 @@
+#pragma once
+
+#include <sdbusplus/message.hpp>
+#include <sdbusplus/bus/match.hpp>
+#include <vector>
+#include "data_types.hpp"
+#include "propertywatch.hpp"
+
+namespace phosphor
+{
+namespace dbus
+{
+namespace monitoring
+{
+
+static constexpr auto MAPPER_BUSNAME = "xyz.openbmc_project.ObjectMapper";
+static constexpr auto MAPPER_PATH = "/xyz/openbmc_project/object_mapper";
+static constexpr auto MAPPER_INTERFACE =
+    "xyz.openbmc_project.ObjectMapper";
+
+using MappedPropertyIndex =
+    RefKeyMap<const std::string,
+    RefKeyMap<const std::string,
+    RefVector<const std::string>>>;
+
+MappedPropertyIndex convert(const PropertyIndex& index);
+
+template <typename DBusInterfaceType>
+void PropertyWatch<DBusInterfaceType>::start()
+{
+    if (alreadyRan)
+    {
+        return;
+    }
+    else
+    {
+        alreadyRan = true;
+    }
+
+    // The index has a flat layout which is not optimal here.  Nest
+    // properties in a map of interface names in a map of object paths.
+    auto mapped = convert(index);
+
+    for (const auto& m : mapped)
+    {
+        const auto& path = m.first.get();
+        const auto& interfaces = m.second;
+
+        // Watch for new interfaces on this path.
+        DBusInterfaceType::addMatch(
+            sdbusplus::bus::match::rules::interfacesAdded(path),
+            [this](auto & msg)
+        // *INDENT-OFF*
+            {
+                this->interfacesAdded(msg);
+            });
+        // *INDENT-ON*
+
+        // Do a query to populate the cache.  Start with a mapper query.
+        // The specific services are queried below.
+        const std::vector<std::string> queryInterfaces; // all interfaces
+        auto mapperResp =
+            DBusInterfaceType::template callMethodAndRead<GetObject>(
+                MAPPER_BUSNAME,
+                MAPPER_PATH,
+                MAPPER_INTERFACE,
+                "GetObject",
+                path,
+                queryInterfaces);
+
+        for (const auto& i : interfaces)
+        {
+            const auto& interface = i.first.get();
+
+            // Watch for property changes on this interface.
+            DBusInterfaceType::addMatch(
+                sdbusplus::bus::match::rules::propertiesChanged(
+                        path, interface),
+                [this](auto & msg)
+                // *INDENT-OFF*
+                {
+                    std::string interface;
+                    msg.read(interface);
+                    auto path = msg.get_path();
+                    this->propertiesChanged(msg, path, interface);
+                });
+                // *INDENT-ON*
+
+            // The mapper response is a busname:[interfaces] map.  Look for
+            // each interface in the index and if found, query the service and
+            // populate the cache entries for the interface.
+            for (const auto& mr : mapperResp)
+            {
+                const auto& busName = mr.first;
+                const auto& mapperInterfaces = mr.second;
+                if (mapperInterfaces.end() == std::find(
+                        mapperInterfaces.begin(),
+                        mapperInterfaces.end(),
+                        interface))
+                {
+                    // This interface isn't being watched.
+                    continue;
+                }
+
+                // Delegate type specific property updates to subclasses.
+                updateProperties(busName, path, interface);
+            }
+        }
+    }
+}
+
+template <typename T, typename DBusInterfaceType>
+void PropertyWatchOfType<T, DBusInterfaceType>::updateProperties(
+    const std::string& busName,
+    const std::string& path,
+    const std::string& interface)
+{
+    auto properties =
+        DBusInterfaceType::template callMethodAndRead<PropertiesChanged<T>>(
+            busName.c_str(),
+            path.c_str(),
+            "org.freedesktop.DBus.Properties",
+            "GetAll",
+            interface);
+    propertiesChanged(path, interface, properties);
+}
+
+template <typename T, typename DBusInterfaceType>
+void PropertyWatchOfType<T, DBusInterfaceType>::propertiesChanged(
+    const std::string& path,
+    const std::string& interface,
+    const PropertiesChanged<T>& properties)
+{
+    // Update the cache for any watched properties.
+    for (const auto& p : properties)
+    {
+        auto key = std::make_tuple(path, interface, p.first);
+        auto item = this->index.find(key);
+        if (item == this->index.end())
+        {
+            // This property isn't being watched.
+            continue;
+        }
+
+        std::get<2>(item->second).get() = p.second.template get<T>();
+    }
+}
+
+template <typename T, typename DBusInterfaceType>
+void PropertyWatchOfType<T, DBusInterfaceType>::propertiesChanged(
+    sdbusplus::message::message& msg,
+    const std::string& path,
+    const std::string& interface)
+{
+    PropertiesChanged<T> properties;
+    msg.read(properties);
+    propertiesChanged(path, interface, properties);
+}
+
+template <typename T, typename DBusInterfaceType>
+void PropertyWatchOfType<T, DBusInterfaceType>::interfacesAdded(
+    const std::string& path,
+    const InterfacesAdded<T>& interfaces)
+{
+    for (const auto& i : interfaces)
+    {
+        propertiesChanged(path, i.first, i.second);
+    }
+}
+
+template <typename T, typename DBusInterfaceType>
+void PropertyWatchOfType<T, DBusInterfaceType>::interfacesAdded(
+    sdbusplus::message::message& msg)
+{
+    sdbusplus::message::object_path path;
+    InterfacesAdded<T> interfaces;
+    msg.read(path, interfaces);
+    interfacesAdded(path, interfaces);
+}
+
+} // namespace monitoring
+} // namespace dbus
+} // namespace phosphor
diff --git a/src/templates/generated.mako.hpp b/src/templates/generated.mako.hpp
index 7c6e527..0911e3a 100644
--- a/src/templates/generated.mako.hpp
+++ b/src/templates/generated.mako.hpp
@@ -4,6 +4,9 @@
 
 #include <array>
 #include <string>
+#include "data_types.hpp"
+#include "propertywatchimpl.hpp"
+#include "sdbusplus.hpp"
 
 using namespace std::string_literals;
 
@@ -77,6 +80,69 @@
         return properties;
     }
 };
+
+struct ConfigPropertyStorage
+{
+    using Storage = std::array<any_ns::any, ${len(instances)}>;
+
+    static auto& get()
+    {
+        static Storage storage;
+        return storage;
+    }
+};
+
+struct ConfigPropertyIndicies
+{
+    using PropertyIndicies = std::array<PropertyIndex, ${len(instancegroups)}>;
+
+    static auto& get()
+    {
+        static const PropertyIndicies propertyIndicies =
+        {
+            {
+% for g in instancegroups:
+                {
+    % for i in g.members:
+                    {
+                        PropertyIndex::key_type
+                        {
+                            ConfigPaths::get()[${i[0]}],
+                            ConfigInterfaces::get()[${i[2]}],
+                            ConfigProperties::get()[${i[3]}]
+                        },
+                        PropertyIndex::mapped_type
+                        {
+                            ConfigMeta::get()[${i[1]}],
+                            ConfigMeta::get()[${i[4]}],
+                            ConfigPropertyStorage::get()[${i[5]}]
+                        },
+                    },
+    % endfor
+                },
+% endfor
+            }
+        };
+        return propertyIndicies;
+    }
+};
+
+struct ConfigPropertyWatches
+{
+    using PropertyWatches = std::array<std::unique_ptr<Watch>, ${len(watches)}>;
+
+    static auto& get()
+    {
+        static const PropertyWatches propertyWatches =
+        {
+% for w in watches:
+            std::make_unique<PropertyWatchOfType<${w.datatype}, SDBusPlus>>(
+                ConfigPropertyIndicies::get()[${w.instances}]),
+% endfor
+        };
+        return propertyWatches;
+    }
+};
 } // namespace monitoring
 } // namespace dbus
 } // namespace phosphor
diff --git a/src/watch.hpp b/src/watch.hpp
new file mode 100644
index 0000000..1ba671d
--- /dev/null
+++ b/src/watch.hpp
@@ -0,0 +1,36 @@
+#pragma once
+
+namespace phosphor
+{
+namespace dbus
+{
+namespace monitoring
+{
+
+/** @class Watch
+ *  @brief Watch interface.
+ *
+ *  The start method is invoked by main() on all watches of any type
+ *  at application startup, to allow watches to perform custom setup
+ *  or initialization.  Typical implementations might register dbus
+ *  callbacks or perform queries.
+ *
+ *  Watches of any type can be started.
+ */
+class Watch
+{
+    public:
+        Watch() = default;
+        Watch(const Watch&) = default;
+        Watch(Watch&&) = default;
+        Watch& operator=(const Watch&) = default;
+        Watch& operator=(Watch&&) = default;
+        virtual ~Watch() = default;
+
+        /** @brief Start the watch. */
+        virtual void start() = 0;
+};
+
+} // namespace monitoring
+} // namespace dbus
+} // namespace phosphor