Convert ComponentManager into an interface and create a mock This will help to mock out ComponentManager's functionality for unit tests in the future. Change-Id: Ie74c49c6b31b00b0c4d38bf0db715a62a9532bc7 Reviewed-on: https://weave-review.googlesource.com/1785 Reviewed-by: Vitaly Buka <vitalybuka@google.com>
diff --git a/src/component_manager_impl.cc b/src/component_manager_impl.cc new file mode 100644 index 0000000..6950969 --- /dev/null +++ b/src/component_manager_impl.cc
@@ -0,0 +1,558 @@ +// Copyright 2015 The Weave 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 "src/component_manager_impl.h" + +#include <base/strings/stringprintf.h> +#include <base/strings/string_number_conversions.h> +#include <base/strings/string_util.h> + +#include "src/commands/schema_constants.h" +#include "src/json_error_codes.h" +#include "src/string_utils.h" +#include "src/utils.h" + +namespace weave { + +namespace { +// Max of 100 state update events should be enough in the queue. +const size_t kMaxStateChangeQueueSize = 100; +} // namespace + +ComponentManagerImpl::ComponentManagerImpl() {} + +ComponentManagerImpl::ComponentManagerImpl(base::Clock* clock) : clock_{clock} { +} + +ComponentManagerImpl::~ComponentManagerImpl() {} + +bool ComponentManagerImpl::AddComponent(const std::string& path, + const std::string& name, + const std::vector<std::string>& traits, + ErrorPtr* error) { + base::DictionaryValue* root = &components_; + if (!path.empty()) { + root = FindComponentGraftNode(path, error); + if (!root) + return false; + } + if (root->GetWithoutPathExpansion(name, nullptr)) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kInvalidState, + "Component '%s' already exists at path '%s'", + name.c_str(), path.c_str()); + return false; + } + + // Check to make sure the declared traits are already defined. + for (const std::string& trait : traits) { + if (!FindTraitDefinition(trait)) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kInvalidPropValue, + "Trait '%s' is undefined", trait.c_str()); + return false; + } + } + std::unique_ptr<base::DictionaryValue> dict{new base::DictionaryValue}; + std::unique_ptr<base::ListValue> traits_list{new base::ListValue}; + traits_list->AppendStrings(traits); + dict->Set("traits", traits_list.release()); + root->SetWithoutPathExpansion(name, dict.release()); + for (const auto& cb : on_componet_tree_changed_) + cb.Run(); + return true; +} + +bool ComponentManagerImpl::AddComponentArrayItem( + const std::string& path, + const std::string& name, + const std::vector<std::string>& traits, + ErrorPtr* error) { + base::DictionaryValue* root = &components_; + if (!path.empty()) { + root = FindComponentGraftNode(path, error); + if (!root) + return false; + } + base::ListValue* array_value = nullptr; + if (!root->GetListWithoutPathExpansion(name, &array_value)) { + array_value = new base::ListValue; + root->SetWithoutPathExpansion(name, array_value); + } + std::unique_ptr<base::DictionaryValue> dict{new base::DictionaryValue}; + std::unique_ptr<base::ListValue> traits_list{new base::ListValue}; + traits_list->AppendStrings(traits); + dict->Set("traits", traits_list.release()); + array_value->Append(dict.release()); + for (const auto& cb : on_componet_tree_changed_) + cb.Run(); + return true; +} + +void ComponentManagerImpl::AddComponentTreeChangedCallback( + const base::Closure& callback) { + on_componet_tree_changed_.push_back(callback); + callback.Run(); +} + +bool ComponentManagerImpl::LoadTraits(const base::DictionaryValue& dict, + ErrorPtr* error) { + bool modified = false; + bool result = true; + // Check if any of the new traits are already defined. If so, make sure the + // definition is exactly the same, or else this is an error. + for (base::DictionaryValue::Iterator it(dict); !it.IsAtEnd(); it.Advance()) { + if (it.value().GetType() != base::Value::TYPE_DICTIONARY) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kTypeMismatch, + "Trait '%s' must be an object", it.key().c_str()); + result = false; + break; + } + const base::DictionaryValue* existing_def = nullptr; + if (traits_.GetDictionary(it.key(), &existing_def)) { + if (!existing_def->Equals(&it.value())) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kTypeMismatch, + "Trait '%s' cannot be redefined", + it.key().c_str()); + result = false; + break; + } + } else { + traits_.Set(it.key(), it.value().DeepCopy()); + modified = true; + } + } + + if (modified) { + for (const auto& cb : on_trait_changed_) + cb.Run(); + } + return result; +} + +bool ComponentManagerImpl::LoadTraits(const std::string& json, + ErrorPtr* error) { + std::unique_ptr<const base::DictionaryValue> dict = LoadJsonDict(json, error); + if (!dict) + return false; + return LoadTraits(*dict, error); +} + +void ComponentManagerImpl::AddTraitDefChangedCallback( + const base::Closure& callback) { + on_trait_changed_.push_back(callback); + callback.Run(); +} + +void ComponentManagerImpl::AddCommand( + std::unique_ptr<CommandInstance> command_instance) { + command_queue_.Add(std::move(command_instance)); +} + +bool ComponentManagerImpl::AddCommand(const base::DictionaryValue& command, + UserRole role, + std::string* id, + ErrorPtr* error) { + auto command_instance = CommandInstance::FromJson( + &command, Command::Origin::kLocal, nullptr, error); + if (!command_instance) + return false; + + UserRole minimal_role; + if (!GetMinimalRole(command_instance->GetName(), &minimal_role, error)) + return false; + + if (role < minimal_role) { + Error::AddToPrintf( + error, FROM_HERE, errors::commands::kDomain, "access_denied", + "User role '%s' less than minimal: '%s'", EnumToString(role).c_str(), + EnumToString(minimal_role).c_str()); + return false; + } + + std::string component_path = command_instance->GetComponent(); + if (component_path.empty()) { + // Find the component to which to route this command. Get the trait name + // from the command name and find the first component that has this trait. + auto trait_name = SplitAtFirst(command_instance->GetName(), ".", true).first; + component_path = FindComponentWithTrait(trait_name); + if (component_path.empty()) { + Error::AddToPrintf( + error, FROM_HERE, errors::commands::kDomain, "unrouted_command", + "Unable route command '%s' because there is no component supporting" + "trait '%s'", command_instance->GetName().c_str(), + trait_name.c_str()); + return false; + } + command_instance->SetComponent(component_path); + } + + const base::DictionaryValue* component = FindComponent(component_path, error); + if (!component) + return false; + + // Check that the command's trait is supported by the given component. + auto pair = SplitAtFirst(command_instance->GetName(), ".", true); + + bool trait_supported = false; + const base::ListValue* supported_traits = nullptr; + if (component->GetList("traits", &supported_traits)) { + for (const base::Value* value : *supported_traits) { + std::string trait; + CHECK(value->GetAsString(&trait)); + if (trait == pair.first) { + trait_supported = true; + break; + } + } + } + + if (!trait_supported) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + "trait_not_supported", + "Component '%s' doesn't support trait '%s'", + component_path.c_str(), pair.first.c_str()); + return false; + } + + std::string command_id = std::to_string(++next_command_id_); + command_instance->SetID(command_id); + if (id) + *id = command_id; + AddCommand(std::move(command_instance)); + return true; +} + +CommandInstance* ComponentManagerImpl::FindCommand(const std::string& id) { + return command_queue_.Find(id); +} + +void ComponentManagerImpl::AddCommandAddedCallback( + const CommandQueue::CommandCallback& callback) { + command_queue_.AddCommandAddedCallback(callback); +} + +void ComponentManagerImpl::AddCommandRemovedCallback( + const CommandQueue::CommandCallback& callback) { + command_queue_.AddCommandRemovedCallback(callback); +} + +void ComponentManagerImpl::AddCommandHandler( + const std::string& component_path, + const std::string& command_name, + const Device::CommandHandlerCallback& callback) { + CHECK(FindCommandDefinition(command_name)) + << "Command undefined: " << command_name; + command_queue_.AddCommandHandler(component_path, command_name, callback); +} + +const base::DictionaryValue* ComponentManagerImpl::FindComponent( + const std::string& path, ErrorPtr* error) const { + return FindComponentAt(&components_, path, error); +} + +const base::DictionaryValue* ComponentManagerImpl::FindTraitDefinition( + const std::string& name) const { + const base::DictionaryValue* trait = nullptr; + traits_.GetDictionaryWithoutPathExpansion(name, &trait); + return trait; +} + +const base::DictionaryValue* ComponentManagerImpl::FindCommandDefinition( + const std::string& command_name) const { + const base::DictionaryValue* definition = nullptr; + std::vector<std::string> components = Split(command_name, ".", true, false); + // Make sure the |command_name| came in form of trait_name.command_name. + if (components.size() != 2) + return definition; + std::string key = base::StringPrintf("%s.commands.%s", components[0].c_str(), + components[1].c_str()); + traits_.GetDictionary(key, &definition); + return definition; +} + +bool ComponentManagerImpl::GetMinimalRole(const std::string& command_name, + UserRole* minimal_role, + ErrorPtr* error) const { + const base::DictionaryValue* command = FindCommandDefinition(command_name); + if (!command) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kInvalidCommandName, + "Command definition for '%s' not found", + command_name.c_str()); + return false; + } + std::string value; + // The JSON definition has been pre-validated already in LoadCommands, so + // just using CHECKs here. + CHECK(command->GetString(commands::attributes::kCommand_Role, &value)); + CHECK(StringToEnum(value, minimal_role)); + return true; +} + +void ComponentManagerImpl::AddStateChangedCallback( + const base::Closure& callback) { + on_state_changed_.push_back(callback); + callback.Run(); // Force to read current state. +} + +bool ComponentManagerImpl::SetStateProperties(const std::string& component_path, + const base::DictionaryValue& dict, + ErrorPtr* error) { + base::DictionaryValue* component = + FindMutableComponent(component_path, error); + if (!component) + return false; + + base::DictionaryValue* state = nullptr; + if (!component->GetDictionary("state", &state)) { + state = new base::DictionaryValue; + component->Set("state", state); + } + state->MergeDictionary(&dict); + last_state_change_id_++; + auto& queue = state_change_queues_[component_path]; + if (!queue) + queue.reset(new StateChangeQueue{kMaxStateChangeQueueSize}); + base::Time timestamp = clock_ ? clock_->Now() : base::Time::Now(); + queue->NotifyPropertiesUpdated(timestamp, dict); + for (const auto& cb : on_state_changed_) + cb.Run(); + return true; +} + +bool ComponentManagerImpl::SetStatePropertiesFromJson( + const std::string& component_path, + const std::string& json, + ErrorPtr* error) { + std::unique_ptr<const base::DictionaryValue> dict = LoadJsonDict(json, error); + return dict && SetStateProperties(component_path, *dict, error); +} + +const base::Value* ComponentManagerImpl::GetStateProperty( + const std::string& component_path, + const std::string& name, + ErrorPtr* error) const { + const base::DictionaryValue* component = FindComponent(component_path, error); + if (!component) + return false; + auto pair = SplitAtFirst(name, ".", true); + if (pair.first.empty()) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kPropertyMissing, + "Empty state package in '%s'", name.c_str()); + return false; + } + if (pair.second.empty()) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kPropertyMissing, + "State property name not specified in '%s'", + name.c_str()); + return false; + } + std::string key = base::StringPrintf("state.%s", name.c_str()); + const base::Value* value = nullptr; + if (!component->Get(key, &value)) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kPropertyMissing, + "State property '%s' not found in component '%s'", + name.c_str(), component_path.c_str()); + } + return value; +} + +bool ComponentManagerImpl::SetStateProperty(const std::string& component_path, + const std::string& name, + const base::Value& value, + ErrorPtr* error) { + base::DictionaryValue dict; + auto pair = SplitAtFirst(name, ".", true); + if (pair.first.empty()) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kPropertyMissing, + "Empty state package in '%s'", name.c_str()); + return false; + } + if (pair.second.empty()) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kPropertyMissing, + "State property name not specified in '%s'", + name.c_str()); + return false; + } + dict.Set(name, value.DeepCopy()); + return SetStateProperties(component_path, dict, error); +} + +ComponentManager::StateSnapshot +ComponentManagerImpl::GetAndClearRecordedStateChanges() { + StateSnapshot snapshot; + snapshot.update_id = GetLastStateChangeId(); + for (auto& pair : state_change_queues_) { + auto changes = pair.second->GetAndClearRecordedStateChanges(); + auto component = pair.first; + auto conv = [component](weave::StateChange& change) { + return ComponentStateChange{change.timestamp, component, + std::move(change.changed_properties)}; + }; + std::transform(changes.begin(), changes.end(), + std::back_inserter(snapshot.state_changes), conv); + } + + // Sort events by the timestamp. + auto pred = [](const ComponentStateChange& lhs, + const ComponentStateChange& rhs) { + return lhs.timestamp < rhs.timestamp; + }; + std::sort(snapshot.state_changes.begin(), snapshot.state_changes.end(), pred); + state_change_queues_.clear(); + return snapshot; +} + +void ComponentManagerImpl::NotifyStateUpdatedOnServer(UpdateID id) { + on_server_state_updated_.Notify(id); +} + +ComponentManager::Token ComponentManagerImpl::AddServerStateUpdatedCallback( + const base::Callback<void(UpdateID)>& callback) { + if (state_change_queues_.empty()) + callback.Run(GetLastStateChangeId()); + return Token{on_server_state_updated_.Add(callback).release()}; +} + +std::string ComponentManagerImpl::FindComponentWithTrait( + const std::string& trait) const { + for (base::DictionaryValue::Iterator it(components_); !it.IsAtEnd(); + it.Advance()) { + const base::ListValue* supported_traits = nullptr; + const base::DictionaryValue* component = nullptr; + CHECK(it.value().GetAsDictionary(&component)); + if (component->GetList("traits", &supported_traits)) { + for (const base::Value* value : *supported_traits) { + std::string supported_trait; + CHECK(value->GetAsString(&supported_trait)); + if (trait == supported_trait) + return it.key(); + } + } + } + return std::string{}; +} + +base::DictionaryValue* ComponentManagerImpl::FindComponentGraftNode( + const std::string& path, ErrorPtr* error) { + base::DictionaryValue* root = nullptr; + base::DictionaryValue* component = FindMutableComponent(path, error); + if (component && !component->GetDictionary("components", &root)) { + root = new base::DictionaryValue; + component->Set("components", root); + } + return root; +} + +base::DictionaryValue* ComponentManagerImpl::FindMutableComponent( + const std::string& path, + ErrorPtr* error) { + return const_cast<base::DictionaryValue*>( + FindComponentAt(&components_, path, error)); +} + +const base::DictionaryValue* ComponentManagerImpl::FindComponentAt( + const base::DictionaryValue* root, + const std::string& path, + ErrorPtr* error) { + auto parts = Split(path, ".", true, false); + std::string root_path; + for (size_t i = 0; i < parts.size(); i++) { + auto element = SplitAtFirst(parts[i], "[", true); + int array_index = -1; + if (element.first.empty()) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kPropertyMissing, + "Empty path element at '%s'", root_path.c_str()); + return nullptr; + } + if (!element.second.empty()) { + if (element.second.back() != ']') { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kPropertyMissing, + "Invalid array element syntax '%s'", + parts[i].c_str()); + return nullptr; + } + element.second.pop_back(); + std::string index_str; + base::TrimWhitespaceASCII(element.second, base::TrimPositions::TRIM_ALL, + &index_str); + if (!base::StringToInt(index_str, &array_index) || array_index < 0) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kInvalidPropValue, + "Invalid array index '%s'", element.second.c_str()); + return nullptr; + } + } + + if (!root_path.empty()) { + // We have processed at least one item in the path before, so now |root| + // points to the actual parent component. We need the root to point to + // the 'components' element containing child sub-components instead. + if (!root->GetDictionary("components", &root)) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kPropertyMissing, + "Component '%s' does not exist at '%s'", + element.first.c_str(), root_path.c_str()); + return nullptr; + } + } + + const base::Value* value = nullptr; + if (!root->GetWithoutPathExpansion(element.first, &value)) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kPropertyMissing, + "Component '%s' does not exist at '%s'", + element.first.c_str(), root_path.c_str()); + return nullptr; + } + + if (value->GetType() == base::Value::TYPE_LIST && array_index < 0) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kTypeMismatch, + "Element '%s.%s' is an array", + root_path.c_str(), element.first.c_str()); + return nullptr; + } + if (value->GetType() == base::Value::TYPE_DICTIONARY && array_index >= 0) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kTypeMismatch, + "Element '%s.%s' is not an array", + root_path.c_str(), element.first.c_str()); + return nullptr; + } + + if (value->GetType() == base::Value::TYPE_DICTIONARY) { + CHECK(value->GetAsDictionary(&root)); + } else { + const base::ListValue* component_array = nullptr; + CHECK(value->GetAsList(&component_array)); + const base::Value* component_value = nullptr; + if (!component_array->Get(array_index, &component_value) || + !component_value->GetAsDictionary(&root)) { + Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain, + errors::commands::kPropertyMissing, + "Element '%s.%s' does not contain item #%d", + root_path.c_str(), element.first.c_str(), + array_index); + return nullptr; + } + } + if (!root_path.empty()) + root_path += '.'; + root_path += parts[i]; + } + return root; +} + +} // namespace weave