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/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