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/buffet.gyp b/buffet/buffet.gyp
index ca2ddc7..db041dd 100644
--- a/buffet/buffet.gyp
+++ b/buffet/buffet.gyp
@@ -40,6 +40,9 @@
         'device_registration_info.cc',
         'manager.cc',
         'storage_impls.cc',
+        'states/error_codes.cc',
+        'states/state_manager.cc',
+        'states/state_package.cc',
         'utils.cc',
       ],
     },
@@ -105,6 +108,8 @@
             'commands/schema_utils_unittest.cc',
             'commands/unittest_utils.cc',
             'device_registration_info_unittest.cc',
+            'states/state_manager_unittest.cc',
+            'states/state_package_unittest.cc',
           ],
         },
       ],
diff --git a/buffet/buffet_client.cc b/buffet/buffet_client.cc
index 1b2396b..63ba2cc 100644
--- a/buffet/buffet_client.cc
+++ b/buffet/buffet_client.cc
@@ -10,11 +10,12 @@
 #include <base/command_line.h>
 #include <base/logging.h>
 #include <base/memory/ref_counted.h>
-#include <base/memory/scoped_ptr.h>
 #include <base/values.h>
 #include <chromeos/any.h>
 #include <chromeos/data_encoding.h>
 #include <chromeos/dbus/data_serialization.h>
+#include <chromeos/dbus/dbus_method_invoker.h>
+#include <chromeos/errors/error.h>
 #include <dbus/bus.h>
 #include <dbus/message.h>
 #include <dbus/object_proxy.h>
@@ -25,8 +26,13 @@
 
 using namespace buffet::dbus_constants;  // NOLINT(build/namespaces)
 
+using chromeos::dbus_utils::CallMethodAndBlock;
+using chromeos::dbus_utils::CallMethodAndBlockWithTimeout;
+using chromeos::dbus_utils::Dictionary;
+using chromeos::dbus_utils::ExtractMethodCallResults;
+using chromeos::ErrorPtr;
+
 namespace {
-static const int default_timeout_ms = 1000;
 
 void usage() {
   std::cerr << "Possible commands:" << std::endl;
@@ -40,7 +46,8 @@
   std::cerr << "  " << kManagerAddCommand
                     << " '{\"name\":\"command_name\",\"parameters\":{}}'"
                     << std::endl;
-  std::cerr << "  " << kManagerUpdateStateMethod << std::endl;
+  std::cerr << "  " << kManagerUpdateStateMethod
+                    << " prop_name prop_value" << std::endl;
   std::cerr << "  " << dbus::kObjectManagerGetManagedObjects << std::endl;
 }
 
@@ -64,21 +71,19 @@
     if (!args.empty())
       message = args.front();
 
-    dbus::MethodCall method_call(kManagerInterface, kManagerTestMethod);
-    dbus::MessageWriter writer(&method_call);
-    writer.AppendString(message);
-    scoped_ptr<dbus::Response> response(
-        manager_proxy_->CallMethodAndBlock(&method_call, default_timeout_ms));
-    if (!response) {
-      std::cout << "Failed to receive a response." << std::endl;
+    ErrorPtr error;
+    auto response = CallMethodAndBlock(
+        manager_proxy_,
+        kManagerInterface, kManagerTestMethod, &error,
+        message);
+    std::string response_message;
+    if (!response ||
+        !ExtractMethodCallResults(response.get(), &error, &response_message)) {
+      std::cout << "Failed to receive a response:"
+                << error->GetMessage() << std::endl;
       return EX_UNAVAILABLE;
     }
-    dbus::MessageReader reader(response.get());
-    std::string response_message;
-    if (!reader.PopString(&response_message)) {
-      std::cout << "No valid response." << std::endl;
-      return EX_SOFTWARE;
-    }
+
     std::cout << "Received a response: " << response_message << std::endl;
     return EX_OK;
   }
@@ -90,21 +95,17 @@
       usage();
       return EX_USAGE;
     }
-    dbus::MethodCall method_call(
-        kManagerInterface, kManagerCheckDeviceRegistered);
 
-    scoped_ptr<dbus::Response> response(
-        manager_proxy_->CallMethodAndBlock(&method_call, default_timeout_ms));
-    if (!response) {
-      std::cout << "Failed to receive a response." << std::endl;
-      return EX_UNAVAILABLE;
-    }
-
-    dbus::MessageReader reader(response.get());
+    ErrorPtr error;
+    auto response = CallMethodAndBlock(
+        manager_proxy_,
+        kManagerInterface, kManagerCheckDeviceRegistered, &error);
     std::string device_id;
-    if (!reader.PopString(&device_id)) {
-      std::cout << "No device ID in response." << std::endl;
-      return EX_SOFTWARE;
+    if (!response ||
+        !ExtractMethodCallResults(response.get(), &error, &device_id)) {
+      std::cout << "Failed to receive a response:"
+                << error->GetMessage() << std::endl;
+      return EX_UNAVAILABLE;
     }
 
     std::cout << "Device ID: "
@@ -120,21 +121,16 @@
       usage();
       return EX_USAGE;
     }
-    dbus::MethodCall method_call(
-        kManagerInterface, kManagerGetDeviceInfo);
 
-    scoped_ptr<dbus::Response> response(
-        manager_proxy_->CallMethodAndBlock(&method_call, default_timeout_ms));
-    if (!response) {
-      std::cout << "Failed to receive a response." << std::endl;
-      return EX_UNAVAILABLE;
-    }
-
-    dbus::MessageReader reader(response.get());
+    ErrorPtr error;
+    auto response = CallMethodAndBlock(
+        manager_proxy_, kManagerInterface, kManagerGetDeviceInfo, &error);
     std::string device_info;
-    if (!reader.PopString(&device_info)) {
-      std::cout << "No device info in response." << std::endl;
-      return EX_SOFTWARE;
+    if (!response ||
+        !ExtractMethodCallResults(response.get(), &error, &device_info)) {
+      std::cout << "Failed to receive a response:"
+                << error->GetMessage() << std::endl;
+      return EX_UNAVAILABLE;
     }
 
     std::cout << "Device Info: "
@@ -150,8 +146,8 @@
       usage();
       return EX_USAGE;
     }
-    chromeos::dbus_utils::Dictionary params;
 
+    Dictionary params;
     if (!args.empty()) {
       auto key_values = chromeos::data_encoding::WebParamsDecode(args.front());
       for (const auto& pair : key_values) {
@@ -159,25 +155,19 @@
       }
     }
 
-    dbus::MethodCall method_call(
-        kManagerInterface, kManagerStartRegisterDevice);
-    dbus::MessageWriter writer(&method_call);
-    CHECK(chromeos::dbus_utils::AppendValueToWriter(&writer, params))
-        << "Failed to send the parameters over D-Bus";
-
+    ErrorPtr error;
     static const int timeout_ms = 3000;
-    scoped_ptr<dbus::Response> response(
-        manager_proxy_->CallMethodAndBlock(&method_call, timeout_ms));
-    if (!response) {
-      std::cout << "Failed to receive a response." << std::endl;
-      return EX_UNAVAILABLE;
-    }
-
-    dbus::MessageReader reader(response.get());
+    auto response = CallMethodAndBlockWithTimeout(
+        timeout_ms,
+        manager_proxy_,
+        kManagerInterface, kManagerStartRegisterDevice, &error,
+        params);
     std::string info;
-    if (!reader.PopString(&info)) {
-      std::cout << "No valid response." << std::endl;
-      return EX_SOFTWARE;
+    if (!response ||
+        !ExtractMethodCallResults(response.get(), &error, &info)) {
+      std::cout << "Failed to receive a response:"
+                << error->GetMessage() << std::endl;
+      return EX_UNAVAILABLE;
     }
 
     std::cout << "Registration started: " << info << std::endl;
@@ -191,25 +181,22 @@
       usage();
       return EX_USAGE;
     }
-    dbus::MethodCall method_call(
-        kManagerInterface, kManagerFinishRegisterDevice);
-    dbus::MessageWriter writer(&method_call);
+
+    ErrorPtr error;
     std::string user_auth_code;
     if (!args.empty()) { user_auth_code = args.front(); }
-    writer.AppendString(user_auth_code);
     static const int timeout_ms = 10000;
-    scoped_ptr<dbus::Response> response(
-        manager_proxy_->CallMethodAndBlock(&method_call, timeout_ms));
-    if (!response) {
-      std::cout << "Failed to receive a response." << std::endl;
-      return EX_UNAVAILABLE;
-    }
-
-    dbus::MessageReader reader(response.get());
+    auto response = CallMethodAndBlockWithTimeout(
+        timeout_ms,
+        manager_proxy_,
+        kManagerInterface, kManagerFinishRegisterDevice, &error,
+        user_auth_code);
     std::string device_id;
-    if (!reader.PopString(&device_id)) {
-      std::cout << "No device ID in response." << std::endl;
-      return EX_SOFTWARE;
+    if (!response ||
+        !ExtractMethodCallResults(response.get(), &error, &device_id)) {
+      std::cout << "Failed to receive a response:"
+                << error->GetMessage() << std::endl;
+      return EX_UNAVAILABLE;
     }
 
     std::cout << "Device ID is "
@@ -219,20 +206,22 @@
   }
 
   int CallManagerUpdateState(const CommandLine::StringVector& args) {
-    if (args.size() != 1) {
+    if (args.size() != 2) {
       std::cerr << "Invalid number of arguments for "
                 << "Manager." << kManagerUpdateStateMethod << std::endl;
       usage();
       return EX_USAGE;
     }
-    dbus::MethodCall method_call(
-        kManagerInterface, kManagerUpdateStateMethod);
-    dbus::MessageWriter writer(&method_call);
-    writer.AppendString(args.front());
-    scoped_ptr<dbus::Response> response(
-        manager_proxy_->CallMethodAndBlock(&method_call, default_timeout_ms));
-    if (!response) {
-      std::cout << "Failed to receive a response." << std::endl;
+
+    ErrorPtr error;
+    Dictionary property_set{{args.front(), args.back()}};
+    auto response = CallMethodAndBlock(
+        manager_proxy_,
+        kManagerInterface, kManagerUpdateStateMethod, &error,
+        property_set);
+    if (!response || !ExtractMethodCallResults(response.get(), &error)) {
+      std::cout << "Failed to receive a response:"
+                << error->GetMessage() << std::endl;
       return EX_UNAVAILABLE;
     }
     return EX_OK;
@@ -245,14 +234,15 @@
       usage();
       return EX_USAGE;
     }
-    dbus::MethodCall method_call(
-        kManagerInterface, kManagerAddCommand);
-    dbus::MessageWriter writer(&method_call);
-    writer.AppendString(args.front());
-    scoped_ptr<dbus::Response> response(
-        manager_proxy_->CallMethodAndBlock(&method_call, default_timeout_ms));
-    if (!response) {
-      std::cout << "Failed to receive a response." << std::endl;
+
+    ErrorPtr error;
+    auto response = CallMethodAndBlock(
+        manager_proxy_,
+        kManagerInterface, kManagerAddCommand, &error,
+        args.front());
+    if (!response || !ExtractMethodCallResults(response.get(), &error)) {
+      std::cout << "Failed to receive a response:"
+                << error->GetMessage() << std::endl;
       return EX_UNAVAILABLE;
     }
     return EX_OK;
@@ -265,12 +255,15 @@
       usage();
       return EX_USAGE;
     }
-    dbus::MethodCall method_call(
-        dbus::kObjectManagerInterface, dbus::kObjectManagerGetManagedObjects);
-    scoped_ptr<dbus::Response> response(
-        root_proxy_->CallMethodAndBlock(&method_call, default_timeout_ms));
+
+    ErrorPtr error;
+    auto response = CallMethodAndBlock(
+        manager_proxy_,
+        dbus::kObjectManagerInterface, dbus::kObjectManagerGetManagedObjects,
+        &error);
     if (!response) {
-      std::cout << "Failed to receive a response." << std::endl;
+      std::cout << "Failed to receive a response:"
+                << error->GetMessage() << std::endl;
       return EX_UNAVAILABLE;
     }
     std::cout << response->ToString() << std::endl;
diff --git a/buffet/device_registration_info.cc b/buffet/device_registration_info.cc
index 2f2ddac..d4b22a2 100644
--- a/buffet/device_registration_info.cc
+++ b/buffet/device_registration_info.cc
@@ -19,6 +19,7 @@
 #include "buffet/commands/command_definition.h"
 #include "buffet/commands/command_manager.h"
 #include "buffet/device_registration_storage_keys.h"
+#include "buffet/states/state_manager.h"
 #include "buffet/storage_impls.h"
 #include "buffet/utils.h"
 
@@ -141,21 +142,26 @@
 namespace buffet {
 
 DeviceRegistrationInfo::DeviceRegistrationInfo(
-    const std::shared_ptr<CommandManager>& command_manager)
-    : transport_(chromeos::http::Transport::CreateDefault()),
-      // TODO(avakulenko): Figure out security implications of storing
-      // this data unencrypted.
-      storage_(new FileStorage(base::FilePath(kDeviceInfoFilePath))),
-      command_manager_(command_manager) {
+    const std::shared_ptr<CommandManager>& command_manager,
+    const std::shared_ptr<const StateManager>& state_manager)
+    : DeviceRegistrationInfo(
+        command_manager,
+        state_manager,
+        chromeos::http::Transport::CreateDefault(),
+        // TODO(avakulenko): Figure out security implications of storing
+        // this data unencrypted.
+        std::make_shared<FileStorage>(base::FilePath{kDeviceInfoFilePath})) {
 }
 
 DeviceRegistrationInfo::DeviceRegistrationInfo(
     const std::shared_ptr<CommandManager>& command_manager,
+    const std::shared_ptr<const StateManager>& state_manager,
     const std::shared_ptr<chromeos::http::Transport>& transport,
     const std::shared_ptr<StorageInterface>& storage)
-    : transport_(transport),
-      storage_(storage),
-      command_manager_(command_manager) {
+    : transport_{transport},
+      storage_{storage},
+      command_manager_{command_manager},
+      state_manager_{state_manager} {
 }
 
 std::pair<std::string, std::string>
@@ -367,6 +373,11 @@
   if (!commands)
     return std::string();
 
+  std::unique_ptr<base::DictionaryValue> state =
+      state_manager_->GetStateValuesAsJson(error);
+  if (!state)
+    return std::string();
+
   base::DictionaryValue req_json;
   req_json.SetString("oauthClientId", client_id_);
   req_json.SetString("deviceDraft.deviceKind", device_kind_);
@@ -374,6 +385,7 @@
   req_json.SetString("deviceDraft.displayName", display_name_);
   req_json.SetString("deviceDraft.channel.supportedType", "xmpp");
   req_json.Set("deviceDraft.commandDefs", commands.release());
+  req_json.Set("deviceDraft.state", state.release());
 
   std::string url = GetServiceURL("registrationTickets", {{"key", api_key_}});
   auto resp_json = chromeos::http::ParseJsonResponse(
diff --git a/buffet/device_registration_info.h b/buffet/device_registration_info.h
index 8cef223..90e306f 100644
--- a/buffet/device_registration_info.h
+++ b/buffet/device_registration_info.h
@@ -25,6 +25,7 @@
 namespace buffet {
 
 class CommandManager;
+class StateManager;
 
 extern const char kErrorDomainOAuth2[];
 extern const char kErrorDomainGCD[];
@@ -36,12 +37,14 @@
   // This is a helper class for unit testing.
   class TestHelper;
   // This constructor uses CURL HTTP transport.
-  explicit DeviceRegistrationInfo(
-      const std::shared_ptr<CommandManager>& command_manager);
+  DeviceRegistrationInfo(
+      const std::shared_ptr<CommandManager>& command_manager,
+      const std::shared_ptr<const StateManager>& state_manager);
   // This constructor allows to pass in a custom HTTP transport
   // (mainly for testing).
   DeviceRegistrationInfo(
       const std::shared_ptr<CommandManager>& command_manager,
+      const std::shared_ptr<const StateManager>& state_manager,
       const std::shared_ptr<chromeos::http::Transport>& transport,
       const std::shared_ptr<StorageInterface>& storage);
 
@@ -143,6 +146,8 @@
   std::shared_ptr<StorageInterface> storage_;
   // Global command manager.
   std::shared_ptr<CommandManager> command_manager_;
+  // Device state manager.
+  std::shared_ptr<const StateManager> state_manager_;
 
   friend class TestHelper;
   DISALLOW_COPY_AND_ASSIGN(DeviceRegistrationInfo);
diff --git a/buffet/device_registration_info_unittest.cc b/buffet/device_registration_info_unittest.cc
index 097c0a6..50eb01a 100644
--- a/buffet/device_registration_info_unittest.cc
+++ b/buffet/device_registration_info_unittest.cc
@@ -14,6 +14,7 @@
 #include "buffet/commands/unittest_utils.h"
 #include "buffet/device_registration_info.h"
 #include "buffet/device_registration_storage_keys.h"
+#include "buffet/states/state_manager.h"
 #include "buffet/storage_impls.h"
 
 using chromeos::http::request_header::kAuthorization;
@@ -165,8 +166,10 @@
     storage_->Save(&data_);
     transport_ = std::make_shared<chromeos::http::fake::Transport>();
     command_manager_ = std::make_shared<CommandManager>();
+    state_manager_ = std::make_shared<StateManager>();
     dev_reg_ = std::unique_ptr<DeviceRegistrationInfo>(
-        new DeviceRegistrationInfo(command_manager_, transport_, storage_));
+        new DeviceRegistrationInfo(command_manager_, state_manager_,
+                                   transport_, storage_));
   }
 
   base::DictionaryValue data_;
@@ -174,6 +177,7 @@
   std::shared_ptr<chromeos::http::fake::Transport> transport_;
   std::unique_ptr<DeviceRegistrationInfo> dev_reg_;
   std::shared_ptr<CommandManager> command_manager_;
+  std::shared_ptr<StateManager> state_manager_;
 };
 
 ////////////////////////////////////////////////////////////////////////////////
diff --git a/buffet/etc/buffet/base_state.defaults.json b/buffet/etc/buffet/base_state.defaults.json
new file mode 100644
index 0000000..baeae37
--- /dev/null
+++ b/buffet/etc/buffet/base_state.defaults.json
@@ -0,0 +1,12 @@
+{
+  "base": {
+    "serialNumber": "",
+    "manufacturer": "",
+    "model": "",
+    "firmwareVersion": "",
+    "supportUrl": "",
+    "updateUrl": "",
+    "localDiscoveryEnabled": false,
+    "isProximityTokenRequired": false
+  }
+}
diff --git a/buffet/etc/buffet/base_state.schema.json b/buffet/etc/buffet/base_state.schema.json
new file mode 100644
index 0000000..df74bba
--- /dev/null
+++ b/buffet/etc/buffet/base_state.schema.json
@@ -0,0 +1,12 @@
+{
+  "base": {
+    "serialNumber": "string",
+    "manufacturer": "string",
+    "model": "string",
+    "firmwareVersion": "string",
+    "supportUrl": "string",
+    "updateUrl": "string",
+    "localDiscoveryEnabled": "boolean",
+    "isProximityTokenRequired": "boolean"
+  }
+}
diff --git a/buffet/manager.cc b/buffet/manager.cc
index 5af35a5..03a5443 100644
--- a/buffet/manager.cc
+++ b/buffet/manager.cc
@@ -21,6 +21,7 @@
 #include "buffet/commands/command_instance.h"
 #include "buffet/commands/command_manager.h"
 #include "buffet/libbuffet/dbus_constants.h"
+#include "buffet/states/state_manager.h"
 
 using chromeos::dbus_utils::AsyncEventSequencer;
 using chromeos::dbus_utils::ExportedObjectManager;
@@ -56,16 +57,14 @@
   itf->AddMethodHandler(dbus_constants::kManagerTestMethod,
                         base::Unretained(this),
                         &Manager::HandleTestMethod);
-  itf->AddProperty("State", &state_);
-  // TODO(wiley): Initialize all properties appropriately before claiming
-  //              the properties interface.
-  state_.SetValue("{}");
   dbus_object_.RegisterAsync(cb);
   command_manager_ =
       std::make_shared<CommandManager>(dbus_object_.GetObjectManager());
   command_manager_->Startup();
+  state_manager_ = std::make_shared<StateManager>();
+  state_manager_->Startup();
   device_info_ = std::unique_ptr<DeviceRegistrationInfo>(
-      new DeviceRegistrationInfo(command_manager_));
+      new DeviceRegistrationInfo(command_manager_, state_manager_));
   device_info_->Load();
 }
 
@@ -118,9 +117,9 @@
 }
 
 void Manager::HandleUpdateState(
-    chromeos::ErrorPtr* error, const std::string& json_state_fragment) {
-  // TODO(wiley): Merge json state blobs intelligently.
-  state_.SetValue(json_state_fragment);
+    chromeos::ErrorPtr* error,
+    const chromeos::dbus_utils::Dictionary& property_set) {
+  state_manager_->UpdateProperties(property_set, error);
 }
 
 void Manager::HandleAddCommand(
diff --git a/buffet/manager.h b/buffet/manager.h
index be399d5..66be522 100644
--- a/buffet/manager.h
+++ b/buffet/manager.h
@@ -12,6 +12,7 @@
 #include <base/macros.h>
 #include <base/memory/weak_ptr.h>
 #include <base/values.h>
+#include <chromeos/dbus/data_serialization.h>
 #include <chromeos/dbus/dbus_object.h>
 #include <chromeos/dbus/exported_property_set.h>
 #include <chromeos/errors/error.h>
@@ -27,6 +28,7 @@
 namespace buffet {
 
 class CommandManager;
+class StateManager;
 
 // The Manager is responsible for global state of Buffet.  It exposes
 // interfaces which affect the entire device such as device registration and
@@ -40,9 +42,6 @@
       const chromeos::dbus_utils::AsyncEventSequencer::CompletionAction& cb);
 
  private:
-  // DBus properties:
-  chromeos::dbus_utils::ExportedProperty<std::string> state_;
-
   // DBus methods:
   // Handles calls to org.chromium.Buffet.Manager.CheckDeviceRegistered().
   std::string HandleCheckDeviceRegistered(chromeos::ErrorPtr* error);
@@ -57,7 +56,7 @@
                                          const std::string& user_auth_code);
   // Handles calls to org.chromium.Buffet.Manager.UpdateState().
   void HandleUpdateState(chromeos::ErrorPtr* error,
-                         const std::string& json_state_fragment);
+                         const chromeos::dbus_utils::Dictionary& property_set);
   // Handles calls to org.chromium.Buffet.Manager.AddCommand().
   void HandleAddCommand(chromeos::ErrorPtr* error,
                         const std::string& json_command);
@@ -68,6 +67,7 @@
   chromeos::dbus_utils::DBusObject dbus_object_;
 
   std::shared_ptr<CommandManager> command_manager_;
+  std::shared_ptr<StateManager> state_manager_;
   std::unique_ptr<DeviceRegistrationInfo> device_info_;
 
   DISALLOW_COPY_AND_ASSIGN(Manager);
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
diff --git a/buffet/utils.cc b/buffet/utils.cc
index 71e9a56..1a39984 100644
--- a/buffet/utils.cc
+++ b/buffet/utils.cc
@@ -15,6 +15,8 @@
 
 const char kErrorDomainBuffet[] = "buffet";
 const char kFileReadError[] = "file_read_error";
+const char kInvalidCategoryError[] = "invalid_category";
+const char kInvalidPackageError[] = "invalid_package";
 
 std::unique_ptr<const base::DictionaryValue> LoadJsonDict(
     const base::FilePath& json_file_path, chromeos::ErrorPtr* error) {
diff --git a/buffet/utils.h b/buffet/utils.h
index e71560c..a776e0c 100644
--- a/buffet/utils.h
+++ b/buffet/utils.h
@@ -13,8 +13,18 @@
 
 namespace buffet {
 
+// Buffet-wide errors.
+// TODO(avakulenko): This should be consolidated into errors::<domain> namespace
+// See crbug.com/417274
 extern const char kErrorDomainBuffet[];
 extern const char kFileReadError[];
+extern const char kInvalidCategoryError[];
+extern const char kInvalidPackageError[];
+
+// kDefaultCategory represents a default state property category for standard
+// properties from "base" package which are provided by buffet and not any of
+// the daemons running on the device.
+const char kDefaultCategory[] = "";
 
 // Helper function to load a JSON file that is expected to be
 // an object/dictionary. In case of error, returns empty unique ptr and fills