buffet: Add device state manager
Added StateManager class to buffet and all the internals to
load vendor-provided state definition fragments, apply state
property defaults, expose the state property values over D-Bus
to be updated by daemons (using Buffet.UpdateState method) and
sent the current device state to GCD server as part of device
draft provided during device registration.
BUG=chromium:415364
TEST=FEATURES=test emerge-link buffet
Change-Id: I78e470c98d906064dfbe925614613ee6a91ff3cf
Reviewed-on: https://chromium-review.googlesource.com/218743
Tested-by: Alex Vakulenko <avakulenko@chromium.org>
Reviewed-by: Christopher Wiley <wiley@chromium.org>
Commit-Queue: Alex Vakulenko <avakulenko@chromium.org>
diff --git a/buffet/states/error_codes.cc b/buffet/states/error_codes.cc
new file mode 100644
index 0000000..3faea7e
--- /dev/null
+++ b/buffet/states/error_codes.cc
@@ -0,0 +1,20 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "buffet/states/error_codes.h"
+
+namespace buffet {
+namespace errors {
+namespace state {
+
+const char kDomain[] = "buffet_state";
+
+const char kPackageNameMissing[] = "package_name_missing";
+const char kPropertyNameMissing[] = "property_name_missing";
+const char kPropertyNotDefined[] = "property_not_defined";
+const char kPropertyRedefinition[] = "property_redefinition";
+
+} // namespace state
+} // namespace errors
+} // namespace buffet
diff --git a/buffet/states/error_codes.h b/buffet/states/error_codes.h
new file mode 100644
index 0000000..2298741
--- /dev/null
+++ b/buffet/states/error_codes.h
@@ -0,0 +1,25 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef BUFFET_STATES_ERROR_CODES_H_
+#define BUFFET_STATES_ERROR_CODES_H_
+
+namespace buffet {
+namespace errors {
+namespace state {
+
+// Error domain for state definitions.
+extern const char kDomain[];
+
+// State-specific error codes.
+extern const char kPackageNameMissing[];
+extern const char kPropertyNameMissing[];
+extern const char kPropertyNotDefined[];
+extern const char kPropertyRedefinition[];
+
+} // namespace state
+} // namespace errors
+} // namespace buffet
+
+#endif // BUFFET_STATES_ERROR_CODES_H_
diff --git a/buffet/states/state_manager.cc b/buffet/states/state_manager.cc
new file mode 100644
index 0000000..afda934
--- /dev/null
+++ b/buffet/states/state_manager.cc
@@ -0,0 +1,254 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "buffet/states/state_manager.h"
+
+#include <base/files/file_enumerator.h>
+#include <base/files/file_path.h>
+#include <base/logging.h>
+#include <base/values.h>
+#include <chromeos/errors/error_codes.h>
+#include <chromeos/strings/string_utils.h>
+
+#include "buffet/states/error_codes.h"
+#include "buffet/utils.h"
+
+namespace buffet {
+
+void StateManager::Startup() {
+ LOG(INFO) << "Initializing StateManager.";
+
+ // Load standard device state definition.
+ base::FilePath base_state_file("/etc/buffet/base_state.schema.json");
+ LOG(INFO) << "Loading standard state definition from "
+ << base_state_file.value();
+ CHECK(LoadBaseStateDefinition(base_state_file, nullptr))
+ << "Failed to load the standard state definition file.";
+
+ // Load component-specific device state definitions.
+ base::FilePath device_state_dir("/etc/buffet/states");
+ base::FileEnumerator enumerator(device_state_dir, false,
+ base::FileEnumerator::FILES,
+ FILE_PATH_LITERAL("*.schema.json"));
+ base::FilePath json_file_path = enumerator.Next();
+ while (!json_file_path.empty()) {
+ LOG(INFO) << "Loading state definition from " << json_file_path.value();
+ CHECK(LoadStateDefinition(json_file_path, nullptr))
+ << "Failed to load the state definition file.";
+ json_file_path = enumerator.Next();
+ }
+
+ // Load standard device state defaults.
+ base::FilePath base_state_defaults("/etc/buffet/base_state.defaults.json");
+ LOG(INFO) << "Loading base state defaults from "
+ << base_state_defaults.value();
+ CHECK(LoadStateDefaults(base_state_defaults, nullptr))
+ << "Failed to load the base state defaults.";
+
+ // Load component-specific device state defaults.
+ base::FileEnumerator enumerator2(device_state_dir, false,
+ base::FileEnumerator::FILES,
+ FILE_PATH_LITERAL("*.defaults.json"));
+ json_file_path = enumerator2.Next();
+ while (!json_file_path.empty()) {
+ LOG(INFO) << "Loading state defaults from " << json_file_path.value();
+ CHECK(LoadStateDefaults(json_file_path, nullptr))
+ << "Failed to load the state defaults.";
+ json_file_path = enumerator2.Next();
+ }
+}
+
+std::unique_ptr<base::DictionaryValue> StateManager::GetStateValuesAsJson(
+ chromeos::ErrorPtr* error) const {
+ std::unique_ptr<base::DictionaryValue> dict{new base::DictionaryValue};
+ for (const auto& pair : packages_) {
+ auto pkg_value = pair.second->GetValuesAsJson(error);
+ if (!pkg_value) {
+ dict.reset();
+ break;
+ }
+ dict->SetWithoutPathExpansion(pair.first, pkg_value.release());
+ }
+ return dict;
+}
+
+bool StateManager::SetPropertyValue(const std::string& full_property_name,
+ const chromeos::Any& value,
+ chromeos::ErrorPtr* error) {
+ std::string package_name;
+ std::string property_name;
+ bool split = chromeos::string_utils::SplitAtFirst(
+ full_property_name, '.', &package_name, &property_name);
+ if (full_property_name.empty() || (split && property_name.empty())) {
+ chromeos::Error::AddTo(error, errors::state::kDomain,
+ errors::state::kPropertyNameMissing,
+ "Property name is missing");
+ return false;
+ }
+ if (!split || package_name.empty()) {
+ chromeos::Error::AddTo(error, errors::state::kDomain,
+ errors::state::kPackageNameMissing,
+ "Package name is missing in the property name");
+ return false;
+ }
+ StatePackage* package = FindPackage(package_name);
+ if (package == nullptr) {
+ chromeos::Error::AddToPrintf(error, errors::state::kDomain,
+ errors::state::kPropertyNotDefined,
+ "Unknown state property package '%s'",
+ package_name.c_str());
+ return false;
+ }
+ return package->SetPropertyValue(property_name, value, error);
+}
+
+bool StateManager::UpdateProperties(
+ const chromeos::dbus_utils::Dictionary& property_set,
+ chromeos::ErrorPtr* error) {
+ for (const auto& pair : property_set) {
+ if (!SetPropertyValue(pair.first, pair.second, error))
+ return false;
+ }
+ return true;
+}
+
+bool StateManager::LoadStateDefinition(const base::DictionaryValue& json,
+ const std::string& category,
+ chromeos::ErrorPtr* error) {
+ base::DictionaryValue::Iterator iter(json);
+ while (!iter.IsAtEnd()) {
+ std::string package_name = iter.key();
+ if (package_name.empty()) {
+ chromeos::Error::AddTo(error, kErrorDomainBuffet, kInvalidPackageError,
+ "State package name is empty");
+ return false;
+ }
+ const base::DictionaryValue* package_dict = nullptr;
+ if (!iter.value().GetAsDictionary(&package_dict)) {
+ chromeos::Error::AddToPrintf(error, chromeos::errors::json::kDomain,
+ chromeos::errors::json::kObjectExpected,
+ "State package '%s' must be an object",
+ package_name.c_str());
+ return false;
+ }
+ StatePackage* package = FindOrCreatePackage(package_name);
+ CHECK(package) << "Unable to create state package " << package_name;
+ if (!package->AddSchemaFromJson(package_dict, error))
+ return false;
+ iter.Advance();
+ }
+ if (category != kDefaultCategory)
+ categories_.insert(category);
+
+ return true;
+}
+
+bool StateManager::LoadStateDefinition(const base::FilePath& json_file_path,
+ chromeos::ErrorPtr* error) {
+ std::unique_ptr<const base::DictionaryValue> json =
+ LoadJsonDict(json_file_path, error);
+ if (!json)
+ return false;
+ std::string category = json_file_path.BaseName().RemoveExtension().value();
+ if (category == kDefaultCategory) {
+ chromeos::Error::AddToPrintf(error, kErrorDomainBuffet,
+ kInvalidCategoryError,
+ "Invalid state category specified in '%s'",
+ json_file_path.value().c_str());
+ return false;
+ }
+
+ if (!LoadStateDefinition(*json, category, error)) {
+ chromeos::Error::AddToPrintf(error, kErrorDomainBuffet,
+ kFileReadError,
+ "Failed to load file '%s'",
+ json_file_path.value().c_str());
+ return false;
+ }
+ return true;
+}
+
+bool StateManager::LoadBaseStateDefinition(const base::FilePath& json_file_path,
+ chromeos::ErrorPtr* error) {
+ std::unique_ptr<const base::DictionaryValue> json =
+ LoadJsonDict(json_file_path, error);
+ if (!json)
+ return false;
+ if (!LoadStateDefinition(*json, kDefaultCategory, error)) {
+ chromeos::Error::AddToPrintf(error, kErrorDomainBuffet,
+ kFileReadError,
+ "Failed to load file '%s'",
+ json_file_path.value().c_str());
+ return false;
+ }
+ return true;
+}
+
+bool StateManager::LoadStateDefaults(const base::DictionaryValue& json,
+ chromeos::ErrorPtr* error) {
+ base::DictionaryValue::Iterator iter(json);
+ while (!iter.IsAtEnd()) {
+ std::string package_name = iter.key();
+ if (package_name.empty()) {
+ chromeos::Error::AddTo(error, kErrorDomainBuffet, kInvalidPackageError,
+ "State package name is empty");
+ return false;
+ }
+ const base::DictionaryValue* package_dict = nullptr;
+ if (!iter.value().GetAsDictionary(&package_dict)) {
+ chromeos::Error::AddToPrintf(error, chromeos::errors::json::kDomain,
+ chromeos::errors::json::kObjectExpected,
+ "State package '%s' must be an object",
+ package_name.c_str());
+ return false;
+ }
+ StatePackage* package = FindPackage(package_name);
+ if (package == nullptr) {
+ chromeos::Error::AddToPrintf(
+ error, chromeos::errors::json::kDomain,
+ chromeos::errors::json::kObjectExpected,
+ "Providing values for undefined state package '%s'",
+ package_name.c_str());
+ return false;
+ }
+ if (!package->AddValuesFromJson(package_dict, error))
+ return false;
+ iter.Advance();
+ }
+ return true;
+}
+
+bool StateManager::LoadStateDefaults(const base::FilePath& json_file_path,
+ chromeos::ErrorPtr* error) {
+ std::unique_ptr<const base::DictionaryValue> json =
+ LoadJsonDict(json_file_path, error);
+ if (!json)
+ return false;
+ if (!LoadStateDefaults(*json, error)) {
+ chromeos::Error::AddToPrintf(error, kErrorDomainBuffet,
+ kFileReadError,
+ "Failed to load file '%s'",
+ json_file_path.value().c_str());
+ return false;
+ }
+ return true;
+}
+
+StatePackage* StateManager::FindPackage(const std::string& package_name) {
+ auto it = packages_.find(package_name);
+ return (it != packages_.end()) ? it->second.get() : nullptr;
+}
+
+StatePackage* StateManager::FindOrCreatePackage(
+ const std::string& package_name) {
+ StatePackage* package = FindPackage(package_name);
+ if (package == nullptr) {
+ std::unique_ptr<StatePackage> new_package{new StatePackage(package_name)};
+ package = packages_.emplace(package_name,
+ std::move(new_package)).first->second.get();
+ }
+ return package;
+}
+
+} // namespace buffet
diff --git a/buffet/states/state_manager.h b/buffet/states/state_manager.h
new file mode 100644
index 0000000..19212f0
--- /dev/null
+++ b/buffet/states/state_manager.h
@@ -0,0 +1,101 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef BUFFET_STATES_STATE_MANAGER_H_
+#define BUFFET_STATES_STATE_MANAGER_H_
+
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+
+#include <base/macros.h>
+#include <chromeos/dbus/data_serialization.h>
+#include <chromeos/errors/error.h>
+
+#include "buffet/states/state_package.h"
+
+namespace base {
+class DictionaryValue;
+class FilePath;
+} // namespace base
+
+namespace buffet {
+
+// StateManager is the class that aggregates the device state fragments
+// provided by device daemons and makes the aggregate device state available
+// to the GCD cloud server and local clients.
+class StateManager final {
+ public:
+ StateManager() = default;
+
+ // Initializes the state manager and load device state fragments.
+ // Called by Buffet daemon at startup.
+ void Startup();
+
+ // Returns aggregated state properties across all registered packages as
+ // a JSON object that can be used to send the device state to the GCD server.
+ std::unique_ptr<base::DictionaryValue> GetStateValuesAsJson(
+ chromeos::ErrorPtr* error) const;
+
+ // Updates a single property value. |full_property_name| must be the full
+ // name of the property to update in format "package.property".
+ bool SetPropertyValue(const std::string& full_property_name,
+ const chromeos::Any& value,
+ chromeos::ErrorPtr* error);
+
+ // Updates a number of state properties in one shot.
+ // |property_set| is a (full_property_name)-to-(property_value) map.
+ bool UpdateProperties(const chromeos::dbus_utils::Dictionary& property_set,
+ chromeos::ErrorPtr* error);
+
+ // Returns all the categories the state properties are registered from.
+ // As with GCD command handling, the category normally represent a device
+ // service (daemon) that is responsible for a set of properties.
+ const std::set<std::string>& GetCategories() const {
+ return categories_;
+ }
+
+ private:
+ // Loads a device state fragment from a JSON object. |category| represents
+ // a device daemon providing the state fragment or empty string for the
+ // base state fragment.
+ bool LoadStateDefinition(const base::DictionaryValue& json,
+ const std::string& category,
+ chromeos::ErrorPtr* error);
+
+ // Loads a device state fragment JSON file. The file name (without extension)
+ // is used as the state fragment category.
+ bool LoadStateDefinition(const base::FilePath& json_file_path,
+ chromeos::ErrorPtr* error);
+
+ // Loads the base device state fragment JSON file. This state fragment
+ // defines the standard state properties from the 'base' package as defined
+ // by GCD specification.
+ bool LoadBaseStateDefinition(const base::FilePath& json_file_path,
+ chromeos::ErrorPtr* error);
+
+ // Loads state default values from JSON object.
+ bool LoadStateDefaults(const base::DictionaryValue& json,
+ chromeos::ErrorPtr* error);
+
+ // Loads state default values from JSON file.
+ bool LoadStateDefaults(const base::FilePath& json_file_path,
+ chromeos::ErrorPtr* error);
+
+ // Finds a package by its name. Returns nullptr if not found.
+ StatePackage* FindPackage(const std::string& package_name);
+ // Finds a package by its name. If none exists, one will be created.
+ StatePackage* FindOrCreatePackage(const std::string& package_name);
+
+ std::map<std::string, std::unique_ptr<StatePackage>> packages_;
+ std::set<std::string> categories_;
+
+ friend class StateManagerTest;
+ DISALLOW_COPY_AND_ASSIGN(StateManager);
+};
+
+} // namespace buffet
+
+#endif // BUFFET_STATES_STATE_MANAGER_H_
diff --git a/buffet/states/state_manager_unittest.cc b/buffet/states/state_manager_unittest.cc
new file mode 100644
index 0000000..1ce130d
--- /dev/null
+++ b/buffet/states/state_manager_unittest.cc
@@ -0,0 +1,130 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+
+#include <base/values.h>
+#include <gtest/gtest.h>
+
+#include "buffet/commands/schema_constants.h"
+#include "buffet/commands/unittest_utils.h"
+#include "buffet/states/error_codes.h"
+#include "buffet/states/state_manager.h"
+
+using buffet::unittests::CreateDictionaryValue;
+using buffet::unittests::ValueToString;
+
+namespace buffet {
+
+namespace {
+std::unique_ptr<base::DictionaryValue> GetTestSchema() {
+ return CreateDictionaryValue(R"({
+ 'base': {
+ 'manufacturer':'string',
+ 'serialNumber':'string'
+ },
+ 'terminator': {
+ 'target':'string'
+ }
+ })");
+}
+
+std::unique_ptr<base::DictionaryValue> GetTestValues() {
+ return CreateDictionaryValue(R"({
+ 'base': {
+ 'manufacturer':'Skynet',
+ 'serialNumber':'T1000'
+ }
+ })");
+}
+} // anonymous namespace
+
+class StateManagerTest : public ::testing::Test {
+ public:
+ void SetUp() override {
+ mgr_.reset(new StateManager);
+ LoadStateDefinition(GetTestSchema().get(), "default", nullptr);
+ ASSERT_TRUE(mgr_->LoadStateDefaults(*GetTestValues().get(), nullptr));
+ }
+ void TearDown() override {
+ mgr_.reset();
+ }
+
+ void LoadStateDefinition(const base::DictionaryValue* json,
+ const std::string& category,
+ chromeos::ErrorPtr* error) {
+ ASSERT_TRUE(mgr_->LoadStateDefinition(*json, category, error));
+ }
+
+ std::unique_ptr<StateManager> mgr_;
+};
+
+TEST(StateManager, Empty) {
+ StateManager manager;
+ EXPECT_TRUE(manager.GetCategories().empty());
+}
+
+TEST_F(StateManagerTest, Initialized) {
+ EXPECT_EQ(std::set<std::string>{"default"}, mgr_->GetCategories());
+ EXPECT_EQ("{'base':{'manufacturer':'Skynet','serialNumber':'T1000'},"
+ "'terminator':{'target':''}}",
+ ValueToString(mgr_->GetStateValuesAsJson(nullptr).get()));
+}
+
+TEST_F(StateManagerTest, LoadStateDefinition) {
+ auto dict = CreateDictionaryValue(R"({
+ 'power': {
+ 'battery_level':'integer'
+ }
+ })");
+ LoadStateDefinition(dict.get(), "powerd", nullptr);
+ EXPECT_EQ((std::set<std::string>{"default", "powerd"}),
+ mgr_->GetCategories());
+ EXPECT_EQ("{'base':{'manufacturer':'Skynet','serialNumber':'T1000'},"
+ "'power':{'battery_level':0},"
+ "'terminator':{'target':''}}",
+ ValueToString(mgr_->GetStateValuesAsJson(nullptr).get()));
+}
+
+TEST_F(StateManagerTest, SetPropertyValue) {
+ ASSERT_TRUE(mgr_->SetPropertyValue("terminator.target",
+ std::string{"John Connor"}, nullptr));
+ EXPECT_EQ("{'base':{'manufacturer':'Skynet','serialNumber':'T1000'},"
+ "'terminator':{'target':'John Connor'}}",
+ ValueToString(mgr_->GetStateValuesAsJson(nullptr).get()));
+}
+
+TEST_F(StateManagerTest, SetPropertyValue_Error_NoName) {
+ chromeos::ErrorPtr error;
+ ASSERT_FALSE(mgr_->SetPropertyValue("", int{0}, &error));
+ EXPECT_EQ(errors::state::kDomain, error->GetDomain());
+ EXPECT_EQ(errors::state::kPropertyNameMissing, error->GetCode());
+ EXPECT_EQ("Property name is missing", error->GetMessage());
+}
+
+TEST_F(StateManagerTest, SetPropertyValue_Error_NoPackage) {
+ chromeos::ErrorPtr error;
+ ASSERT_FALSE(mgr_->SetPropertyValue("target", int{0}, &error));
+ EXPECT_EQ(errors::state::kDomain, error->GetDomain());
+ EXPECT_EQ(errors::state::kPackageNameMissing, error->GetCode());
+ EXPECT_EQ("Package name is missing in the property name",
+ error->GetMessage());
+}
+
+TEST_F(StateManagerTest, SetPropertyValue_Error_UnknownPackage) {
+ chromeos::ErrorPtr error;
+ ASSERT_FALSE(mgr_->SetPropertyValue("power.level", int{0}, &error));
+ EXPECT_EQ(errors::state::kDomain, error->GetDomain());
+ EXPECT_EQ(errors::state::kPropertyNotDefined, error->GetCode());
+ EXPECT_EQ("Unknown state property package 'power'", error->GetMessage());
+}
+
+TEST_F(StateManagerTest, SetPropertyValue_Error_UnknownProperty) {
+ chromeos::ErrorPtr error;
+ ASSERT_FALSE(mgr_->SetPropertyValue("base.level", int{0}, &error));
+ EXPECT_EQ(errors::state::kDomain, error->GetDomain());
+ EXPECT_EQ(errors::state::kPropertyNotDefined, error->GetCode());
+ EXPECT_EQ("State property 'base.level' is not defined", error->GetMessage());
+}
+
+} // namespace buffet
diff --git a/buffet/states/state_package.cc b/buffet/states/state_package.cc
new file mode 100644
index 0000000..ce71c50
--- /dev/null
+++ b/buffet/states/state_package.cc
@@ -0,0 +1,116 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "buffet/states/state_package.h"
+
+#include <base/logging.h>
+#include <base/values.h>
+#include <chromeos/dbus/data_serialization.h>
+
+#include "buffet/commands/prop_types.h"
+#include "buffet/commands/prop_values.h"
+#include "buffet/commands/schema_utils.h"
+#include "buffet/states/error_codes.h"
+
+namespace buffet {
+
+StatePackage::StatePackage(const std::string& name) : name_(name) {
+}
+
+bool StatePackage::AddSchemaFromJson(const base::DictionaryValue* json,
+ chromeos::ErrorPtr* error) {
+ ObjectSchema schema;
+ if (!schema.FromJson(json, nullptr, error))
+ return false;
+
+ // Scan first to make sure we have no property redefinitions.
+ for (const auto& pair : schema.GetProps()) {
+ if (types_.GetProp(pair.first)) {
+ chromeos::Error::AddToPrintf(error, errors::state::kDomain,
+ errors::state::kPropertyRedefinition,
+ "State property '%s.%s' is already defined",
+ name_.c_str(), pair.first.c_str());
+ return false;
+ }
+ }
+
+ // Now move all the properties to |types_| object.
+ for (const auto& pair : schema.GetProps()) {
+ types_.AddProp(pair.first, pair.second);
+ // Create default value for this state property.
+ values_.insert(std::make_pair(pair.first, pair.second->CreateValue()));
+ }
+
+ return true;
+}
+
+bool StatePackage::AddValuesFromJson(const base::DictionaryValue* json,
+ chromeos::ErrorPtr* error) {
+ base::DictionaryValue::Iterator iter(*json);
+ while (!iter.IsAtEnd()) {
+ std::string property_name = iter.key();
+ auto it = values_.find(property_name);
+ if (it == values_.end()) {
+ chromeos::Error::AddToPrintf(error, errors::state::kDomain,
+ errors::state::kPropertyNotDefined,
+ "State property '%s.%s' is not defined",
+ name_.c_str(), property_name.c_str());
+ return false;
+ }
+ auto new_value = it->second->GetPropType()->CreateValue();
+ if (!new_value->FromJson(&iter.value(), error))
+ return false;
+ it->second = new_value;
+ iter.Advance();
+ }
+ return true;
+}
+
+std::unique_ptr<base::DictionaryValue> StatePackage::GetValuesAsJson(
+ chromeos::ErrorPtr* error) const {
+ std::unique_ptr<base::DictionaryValue> dict(new base::DictionaryValue);
+ for (const auto& pair : values_) {
+ auto value = pair.second->ToJson(error);
+ if (!value) {
+ dict.reset();
+ break;
+ }
+ dict->SetWithoutPathExpansion(pair.first, value.release());
+ }
+ return dict;
+}
+
+chromeos::Any StatePackage::GetPropertyValue(const std::string& property_name,
+ chromeos::ErrorPtr* error) const {
+ auto it = values_.find(property_name);
+ if (it == values_.end()) {
+ chromeos::Error::AddToPrintf(error, errors::state::kDomain,
+ errors::state::kPropertyNotDefined,
+ "State property '%s.%s' is not defined",
+ name_.c_str(), property_name.c_str());
+ return chromeos::Any();
+ }
+ return PropValueToDBusVariant(it->second.get());
+}
+
+bool StatePackage::SetPropertyValue(const std::string& property_name,
+ const chromeos::Any& value,
+ chromeos::ErrorPtr* error) {
+ auto it = values_.find(property_name);
+ if (it == values_.end()) {
+ chromeos::Error::AddToPrintf(error, errors::state::kDomain,
+ errors::state::kPropertyNotDefined,
+ "State property '%s.%s' is not defined",
+ name_.c_str(), property_name.c_str());
+ return false;
+ }
+ auto new_value = PropValueFromDBusVariant(it->second->GetPropType(),
+ value, error);
+ if (!new_value)
+ return false;
+ it->second = new_value;
+ return true;
+}
+
+} // namespace buffet
diff --git a/buffet/states/state_package.h b/buffet/states/state_package.h
new file mode 100644
index 0000000..b2e1383
--- /dev/null
+++ b/buffet/states/state_package.h
@@ -0,0 +1,82 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef BUFFET_STATES_STATE_PACKAGE_H_
+#define BUFFET_STATES_STATE_PACKAGE_H_
+
+#include <map>
+#include <memory>
+#include <string>
+
+#include <base/macros.h>
+#include <chromeos/any.h>
+#include <chromeos/errors/error.h>
+
+#include "buffet/commands/object_schema.h"
+#include "buffet/commands/prop_values.h"
+
+namespace base {
+class DictionaryValue;
+} // namespace base
+
+namespace buffet {
+
+// A package is a set of related state properties. GCD specification defines
+// a number of standard state properties in "base" package such as
+// "base.manufacturer", "base.model", "base.firmwareVersion" and so on.
+class StatePackage final {
+ public:
+ explicit StatePackage(const std::string& name);
+
+ // Loads state property definitions from a JSON object and adds them
+ // to the current package.
+ bool AddSchemaFromJson(const base::DictionaryValue* json,
+ chromeos::ErrorPtr* error);
+ // Loads a set of state property values from a JSON object and assigns them
+ // to existing properties. A property must be defined prior to loading its
+ // value. We use this when we load default values during buffet startup.
+ bool AddValuesFromJson(const base::DictionaryValue* json,
+ chromeos::ErrorPtr* error);
+
+ // Returns a set of state properties and their values as a JSON object.
+ // After being aggregated across multiple packages, this becomes the device
+ // state object passed to the GCD server or a local client in the format
+ // described by GCD specification, e.g.:
+ // {
+ // "base": {
+ // "manufacturer":"...",
+ // "model":"..."
+ // },
+ // "printer": {
+ // "message": "Printer low on cyan ink"
+ // }
+ // }
+ std::unique_ptr<base::DictionaryValue> GetValuesAsJson(
+ chromeos::ErrorPtr* error) const;
+
+ // Gets the value for a specific state property. |property_name| must not
+ // include the package name as part of the property name.
+ chromeos::Any GetPropertyValue(const std::string& property_name,
+ chromeos::ErrorPtr* error) const;
+ // Sets the value for a specific state property. |property_name| must not
+ // include the package name as part of the property name.
+ bool SetPropertyValue(const std::string& property_name,
+ const chromeos::Any& value,
+ chromeos::ErrorPtr* error);
+
+ // Returns the name of the this package.
+ const std::string& GetName() const { return name_; }
+
+ private:
+ std::string name_;
+ ObjectSchema types_;
+ native_types::Object values_;
+
+ friend class StatePackageTestHelper;
+ DISALLOW_COPY_AND_ASSIGN(StatePackage);
+};
+
+} // namespace buffet
+
+#endif // BUFFET_STATES_STATE_PACKAGE_H_
diff --git a/buffet/states/state_package_unittest.cc b/buffet/states/state_package_unittest.cc
new file mode 100644
index 0000000..a9caf09
--- /dev/null
+++ b/buffet/states/state_package_unittest.cc
@@ -0,0 +1,312 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <memory>
+#include <string>
+
+#include <base/values.h>
+#include <chromeos/dbus/data_serialization.h>
+#include <gtest/gtest.h>
+
+#include "buffet/commands/schema_constants.h"
+#include "buffet/commands/unittest_utils.h"
+#include "buffet/states/error_codes.h"
+#include "buffet/states/state_package.h"
+
+using buffet::unittests::CreateDictionaryValue;
+using buffet::unittests::ValueToString;
+
+namespace buffet {
+
+class StatePackageTestHelper {
+ public:
+ // Returns the state property definitions (types/constraints/etc).
+ static const ObjectSchema& GetTypes(const StatePackage& package) {
+ return package.types_;
+ }
+ // Returns the all state property values in this package.
+ static const native_types::Object& GetValues(const StatePackage& package) {
+ return package.values_;
+ }
+};
+
+namespace {
+std::unique_ptr<base::DictionaryValue> GetTestSchema() {
+ return CreateDictionaryValue(R"({
+ 'light': 'boolean',
+ 'color': 'string',
+ 'direction':{'properties':{'azimuth':'number','altitude':{'maximum':90.0}}},
+ 'iso': [50, 100, 200, 400]
+ })");
+}
+
+std::unique_ptr<base::DictionaryValue> GetTestValues() {
+ return CreateDictionaryValue(R"({
+ 'light': true,
+ 'color': 'white',
+ 'direction': {'azimuth':57.2957795, 'altitude':89.9},
+ 'iso': 200
+ })");
+}
+
+inline const ObjectSchema& GetTypes(const StatePackage& package) {
+ return StatePackageTestHelper::GetTypes(package);
+}
+// Returns the all state property values in this package.
+inline const native_types::Object& GetValues(const StatePackage& package) {
+ return StatePackageTestHelper::GetValues(package);
+}
+
+} // anonymous namespace
+
+class StatePackageTest : public ::testing::Test {
+ public:
+ void SetUp() override {
+ package_.reset(new StatePackage("test"));
+ ASSERT_TRUE(package_->AddSchemaFromJson(GetTestSchema().get(), nullptr));
+ ASSERT_TRUE(package_->AddValuesFromJson(GetTestValues().get(), nullptr));
+ }
+ void TearDown() override {
+ package_.reset();
+ }
+ std::unique_ptr<StatePackage> package_;
+};
+
+TEST(StatePackage, Empty) {
+ StatePackage package("test");
+ EXPECT_EQ("test", package.GetName());
+ EXPECT_TRUE(GetTypes(package).GetProps().empty());
+ EXPECT_TRUE(GetValues(package).empty());
+}
+
+TEST(StatePackage, AddSchemaFromJson_OnEmpty) {
+ StatePackage package("test");
+ ASSERT_TRUE(package.AddSchemaFromJson(GetTestSchema().get(), nullptr));
+ EXPECT_EQ(4, GetTypes(package).GetProps().size());
+ EXPECT_EQ(4, GetValues(package).size());
+ EXPECT_EQ("{'color':{'type':'string'},"
+ "'direction':{'properties':{"
+ "'altitude':{'maximum':90.0,'type':'number'},"
+ "'azimuth':{'type':'number'}},"
+ "'type':'object'},"
+ "'iso':{'enum':[50,100,200,400],'type':'integer'},"
+ "'light':{'type':'boolean'}}",
+ ValueToString(GetTypes(package).ToJson(true, nullptr).get()));
+ EXPECT_EQ("{'color':'','direction':{},'iso':0,'light':false}",
+ ValueToString(package.GetValuesAsJson(nullptr).get()));
+}
+
+TEST(StatePackage, AddValuesFromJson_OnEmpty) {
+ StatePackage package("test");
+ ASSERT_TRUE(package.AddSchemaFromJson(GetTestSchema().get(), nullptr));
+ ASSERT_TRUE(package.AddValuesFromJson(GetTestValues().get(), nullptr));
+ EXPECT_EQ(4, GetValues(package).size());
+ EXPECT_EQ("{'color':'white',"
+ "'direction':{'altitude':89.9,'azimuth':57.2957795},"
+ "'iso':200,"
+ "'light':true}",
+ ValueToString(package.GetValuesAsJson(nullptr).get()));
+}
+
+TEST_F(StatePackageTest, AddSchemaFromJson_AddMore) {
+ auto dict = CreateDictionaryValue("{'brightness':['low', 'medium', 'high']}");
+ ASSERT_TRUE(package_->AddSchemaFromJson(dict.get(), nullptr));
+ EXPECT_EQ(5, GetTypes(*package_).GetProps().size());
+ EXPECT_EQ(5, GetValues(*package_).size());
+ EXPECT_EQ("{'brightness':{'enum':['low','medium','high'],'type':'string'},"
+ "'color':{'type':'string'},"
+ "'direction':{'properties':{"
+ "'altitude':{'maximum':90.0,'type':'number'},"
+ "'azimuth':{'type':'number'}},"
+ "'type':'object'},"
+ "'iso':{'enum':[50,100,200,400],'type':'integer'},"
+ "'light':{'type':'boolean'}}",
+ ValueToString(GetTypes(*package_).ToJson(true, nullptr).get()));
+ EXPECT_EQ("{'brightness':'',"
+ "'color':'white',"
+ "'direction':{'altitude':89.9,'azimuth':57.2957795},"
+ "'iso':200,"
+ "'light':true}",
+ ValueToString(package_->GetValuesAsJson(nullptr).get()));
+}
+
+TEST_F(StatePackageTest, AddValuesFromJson_AddMore) {
+ auto dict = CreateDictionaryValue("{'brightness':['low', 'medium', 'high']}");
+ ASSERT_TRUE(package_->AddSchemaFromJson(dict.get(), nullptr));
+ dict = CreateDictionaryValue("{'brightness':'medium'}");
+ ASSERT_TRUE(package_->AddValuesFromJson(dict.get(), nullptr));
+ EXPECT_EQ(5, GetValues(*package_).size());
+ EXPECT_EQ("{'brightness':'medium',"
+ "'color':'white',"
+ "'direction':{'altitude':89.9,'azimuth':57.2957795},"
+ "'iso':200,"
+ "'light':true}",
+ ValueToString(package_->GetValuesAsJson(nullptr).get()));
+}
+
+TEST_F(StatePackageTest, AddSchemaFromJson_Error_Redefined) {
+ auto dict = CreateDictionaryValue("{'color':['white', 'blue', 'red']}");
+ chromeos::ErrorPtr error;
+ EXPECT_FALSE(package_->AddSchemaFromJson(dict.get(), &error));
+ EXPECT_EQ(errors::state::kDomain, error->GetDomain());
+ EXPECT_EQ(errors::state::kPropertyRedefinition, error->GetCode());
+ EXPECT_EQ("State property 'test.color' is already defined",
+ error->GetMessage());
+}
+
+TEST_F(StatePackageTest, AddValuesFromJson_Error_Undefined) {
+ auto dict = CreateDictionaryValue("{'brightness':'medium'}");
+ chromeos::ErrorPtr error;
+ EXPECT_FALSE(package_->AddValuesFromJson(dict.get(), &error));
+ EXPECT_EQ(errors::state::kDomain, error->GetDomain());
+ EXPECT_EQ(errors::state::kPropertyNotDefined, error->GetCode());
+ EXPECT_EQ("State property 'test.brightness' is not defined",
+ error->GetMessage());
+}
+
+TEST_F(StatePackageTest, GetPropertyValue) {
+ chromeos::Any value = package_->GetPropertyValue("color", nullptr);
+ EXPECT_EQ("white", value.TryGet<std::string>());
+
+ value = package_->GetPropertyValue("light", nullptr);
+ EXPECT_TRUE(value.TryGet<bool>());
+
+ value = package_->GetPropertyValue("iso", nullptr);
+ EXPECT_EQ(200, value.TryGet<int>());
+
+ value = package_->GetPropertyValue("direction", nullptr);
+ auto direction = value.TryGet<chromeos::dbus_utils::Dictionary>();
+ ASSERT_FALSE(direction.empty());
+ EXPECT_DOUBLE_EQ(89.9, direction["altitude"].TryGet<double>());
+ EXPECT_DOUBLE_EQ(57.2957795, direction["azimuth"].TryGet<double>());
+}
+
+TEST_F(StatePackageTest, GetPropertyValue_Unknown) {
+ chromeos::ErrorPtr error;
+ chromeos::Any value = package_->GetPropertyValue("volume", &error);
+ EXPECT_TRUE(value.IsEmpty());
+ EXPECT_EQ(errors::state::kDomain, error->GetDomain());
+ EXPECT_EQ(errors::state::kPropertyNotDefined, error->GetCode());
+ EXPECT_EQ("State property 'test.volume' is not defined",
+ error->GetMessage());
+}
+
+TEST_F(StatePackageTest, SetPropertyValue_Simple) {
+ EXPECT_TRUE(package_->SetPropertyValue("color", std::string{"blue"},
+ nullptr));
+ chromeos::Any value = package_->GetPropertyValue("color", nullptr);
+ EXPECT_EQ("blue", value.TryGet<std::string>());
+
+ EXPECT_TRUE(package_->SetPropertyValue("light", bool{false}, nullptr));
+ value = package_->GetPropertyValue("light", nullptr);
+ EXPECT_FALSE(value.TryGet<bool>());
+
+ EXPECT_TRUE(package_->SetPropertyValue("iso", int{400}, nullptr));
+ value = package_->GetPropertyValue("iso", nullptr);
+ EXPECT_EQ(400, value.TryGet<int>());
+}
+
+TEST_F(StatePackageTest, SetPropertyValue_Object) {
+ chromeos::dbus_utils::Dictionary direction{
+ {"altitude", double{45.0}},
+ {"azimuth", double{15.0}},
+ };
+ EXPECT_TRUE(package_->SetPropertyValue("direction", direction, nullptr));
+ EXPECT_EQ("{'color':'white',"
+ "'direction':{'altitude':45.0,'azimuth':15.0},"
+ "'iso':200,"
+ "'light':true}",
+ ValueToString(package_->GetValuesAsJson(nullptr).get()));
+}
+
+TEST_F(StatePackageTest, SetPropertyValue_Error_TypeMismatch) {
+ chromeos::ErrorPtr error;
+ ASSERT_FALSE(package_->SetPropertyValue("color", int{12}, &error));
+ EXPECT_EQ(errors::commands::kDomain, error->GetDomain());
+ EXPECT_EQ(errors::commands::kTypeMismatch, error->GetCode());
+ EXPECT_EQ("Unable to convert value to type 'string'", error->GetMessage());
+ error.reset();
+
+ ASSERT_FALSE(package_->SetPropertyValue("iso", bool{false}, &error));
+ EXPECT_EQ(errors::commands::kDomain, error->GetDomain());
+ EXPECT_EQ(errors::commands::kTypeMismatch, error->GetCode());
+ EXPECT_EQ("Unable to convert value to type 'integer'", error->GetMessage());
+}
+
+TEST_F(StatePackageTest, SetPropertyValue_Error_OutOfRange) {
+ chromeos::ErrorPtr error;
+ ASSERT_FALSE(package_->SetPropertyValue("iso", int{150}, &error));
+ EXPECT_EQ(errors::commands::kDomain, error->GetDomain());
+ EXPECT_EQ(errors::commands::kOutOfRange, error->GetCode());
+ EXPECT_EQ("Value 150 is invalid. Expected one of [50,100,200,400]",
+ error->GetMessage());
+}
+
+TEST_F(StatePackageTest, SetPropertyValue_Error_Object_TypeMismatch) {
+ chromeos::ErrorPtr error;
+ chromeos::dbus_utils::Dictionary direction{
+ {"altitude", double{45.0}},
+ {"azimuth", int{15}},
+ };
+ ASSERT_FALSE(package_->SetPropertyValue("direction", direction, &error));
+ EXPECT_EQ(errors::commands::kDomain, error->GetDomain());
+ EXPECT_EQ(errors::commands::kInvalidPropValue, error->GetCode());
+ EXPECT_EQ("Invalid value for property 'azimuth'", error->GetMessage());
+ const chromeos::Error* inner = error->GetInnerError();
+ EXPECT_EQ(errors::commands::kDomain, inner->GetDomain());
+ EXPECT_EQ(errors::commands::kTypeMismatch, inner->GetCode());
+ EXPECT_EQ("Unable to convert value to type 'number'", inner->GetMessage());
+}
+
+TEST_F(StatePackageTest, SetPropertyValue_Error_Object_OutOfRange) {
+ chromeos::ErrorPtr error;
+ chromeos::dbus_utils::Dictionary direction{
+ {"altitude", double{100.0}},
+ {"azimuth", double{290.0}},
+ };
+ ASSERT_FALSE(package_->SetPropertyValue("direction", direction, &error));
+ EXPECT_EQ(errors::commands::kDomain, error->GetDomain());
+ EXPECT_EQ(errors::commands::kInvalidPropValue, error->GetCode());
+ EXPECT_EQ("Invalid value for property 'altitude'", error->GetMessage());
+ const chromeos::Error* inner = error->GetInnerError();
+ EXPECT_EQ(errors::commands::kDomain, inner->GetDomain());
+ EXPECT_EQ(errors::commands::kOutOfRange, inner->GetCode());
+ EXPECT_EQ("Value 100 is out of range. It must not be greater than 90",
+ inner->GetMessage());
+}
+
+TEST_F(StatePackageTest, SetPropertyValue_Error_Object_UnknownProperty) {
+ chromeos::ErrorPtr error;
+ chromeos::dbus_utils::Dictionary direction{
+ {"altitude", double{10.0}},
+ {"azimuth", double{20.0}},
+ {"spin", double{30.0}},
+ };
+ ASSERT_FALSE(package_->SetPropertyValue("direction", direction, &error));
+ EXPECT_EQ(errors::commands::kDomain, error->GetDomain());
+ EXPECT_EQ(errors::commands::kUnknownProperty, error->GetCode());
+ EXPECT_EQ("Unrecognized property 'spin'", error->GetMessage());
+}
+
+TEST_F(StatePackageTest, SetPropertyValue_Error_Object_MissingProperty) {
+ chromeos::ErrorPtr error;
+ chromeos::dbus_utils::Dictionary direction{
+ {"altitude", double{10.0}},
+ };
+ ASSERT_FALSE(package_->SetPropertyValue("direction", direction, &error));
+ EXPECT_EQ(errors::commands::kDomain, error->GetDomain());
+ EXPECT_EQ(errors::commands::kPropertyMissing, error->GetCode());
+ EXPECT_EQ("Required parameter missing: azimuth", error->GetMessage());
+}
+
+TEST_F(StatePackageTest, SetPropertyValue_Error_Unknown) {
+ chromeos::ErrorPtr error;
+ ASSERT_FALSE(package_->SetPropertyValue("volume", int{100}, &error));
+ EXPECT_EQ(errors::state::kDomain, error->GetDomain());
+ EXPECT_EQ(errors::state::kPropertyNotDefined, error->GetCode());
+ EXPECT_EQ("State property 'test.volume' is not defined",
+ error->GetMessage());
+}
+
+} // namespace buffet