blob: 3d331f9c08e7a8334ecf7d6120da17d7569f9a51 [file] [log] [blame]
// Copyright 2017 The Fuchsia 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 "peridot/bin/module_resolver/local_module_resolver.h"
#include <algorithm>
#include <fuchsia/modular/cpp/fidl.h>
#include <lib/async/cpp/task.h>
#include <lib/async/default.h>
#include <lib/context/cpp/context_helper.h>
#include <lib/entity/cpp/json.h>
#include <lib/fxl/strings/split_string.h>
#include "peridot/lib/fidl/clone.h"
namespace modular {
namespace {
// << operator for LocalModuleResolver::EntryId.
std::ostream& operator<<(std::ostream& o,
const std::pair<std::string, std::string>& id) {
return o << id.first << ":" << id.second;
}
} // namespace
LocalModuleResolver::LocalModuleResolver()
: query_handler_binding_(this),
already_checking_if_sources_are_ready_(false),
weak_factory_(this) {}
LocalModuleResolver::~LocalModuleResolver() = default;
void LocalModuleResolver::AddSource(
std::string name, std::unique_ptr<ModuleManifestSource> repo) {
FXL_CHECK(bindings_.size() == 0);
auto ptr = repo.get();
sources_.emplace(name, std::move(repo));
ptr->Watch(
async_get_default_dispatcher(), [this, name]() { OnSourceIdle(name); },
[this, name](std::string id, fuchsia::modular::ModuleManifest entry) {
OnNewManifestEntry(name, std::move(id), std::move(entry));
},
[this, name](std::string id) {
OnRemoveManifestEntry(name, std::move(id));
});
}
void LocalModuleResolver::Connect(
fidl::InterfaceRequest<fuchsia::modular::ModuleResolver> request) {
if (!AllSourcesAreReady()) {
PeriodicCheckIfSourcesAreReady();
pending_bindings_.push_back(std::move(request));
} else {
bindings_.AddBinding(this, std::move(request));
}
}
void LocalModuleResolver::BindQueryHandler(
fidl::InterfaceRequest<fuchsia::modular::QueryHandler> request) {
query_handler_binding_.Bind(std::move(request));
}
class LocalModuleResolver::FindModulesCall
: public Operation<fuchsia::modular::FindModulesResponse> {
public:
FindModulesCall(LocalModuleResolver* local_module_resolver,
fuchsia::modular::FindModulesQuery query,
ResultCall result_call)
: Operation("LocalModuleResolver::FindModulesCall",
std::move(result_call)),
local_module_resolver_(local_module_resolver),
query_(std::move(query)) {}
// Finds all modules that match |query_|.
//
// The specified action is used to filter potential modules, and
// the associated parameters are required to match in both name and type. If
// |query_.module_handler| is specified, then the search for the action and
// parameters are restricted to the specified handler.
void Run() override {
FlowToken flow{this, &response_};
auto action_it =
local_module_resolver_->action_to_entries_.find(query_.action);
if (action_it == local_module_resolver_->action_to_entries_.end()) {
response_ = CreateEmptyResponse();
return;
}
candidates_ = action_it->second;
// For each parameter in the FindModulesQuery, try to find Modules that
// provide the types in the parameter as constraints.
if (query_.parameter_constraints.is_null() ||
query_.parameter_constraints->size() == 0) {
Finally(flow);
return;
}
for (const auto& parameter_entry : *query_.parameter_constraints) {
ProcessParameterTypes(parameter_entry.param_name,
parameter_entry.param_types);
}
Finally(flow);
}
private:
// |parameter_name| and |types| come from the FindModulesQuery.
void ProcessParameterTypes(const std::string& parameter_name,
const fidl::VectorPtr<fidl::StringPtr>& types) {
std::set<EntryId> parameter_type_entries;
for (const auto& type : *types) {
std::set<EntryId> found_entries =
GetEntriesMatchingParameterByTypeAndName(type, parameter_name);
parameter_type_entries.insert(found_entries.begin(), found_entries.end());
}
std::set<EntryId> new_result_entries;
// All parameters in the query must be handled by the candidates. For each
// parameter that is processed, filter out any existing results that can't
// also handle the new parameter type.
std::set_intersection(
candidates_.begin(), candidates_.end(), parameter_type_entries.begin(),
parameter_type_entries.end(),
std::inserter(new_result_entries, new_result_entries.begin()));
candidates_.swap(new_result_entries);
}
// Returns the EntryIds of all entries with a parameter that matches the
// provided name and type.
std::set<EntryId> GetEntriesMatchingParameterByTypeAndName(
const std::string& parameter_type, const std::string& parameter_name) {
std::set<EntryId> found_entries;
auto found_entries_it =
local_module_resolver_->parameter_type_and_name_to_entries_.find(
std::make_pair(parameter_type, parameter_name));
if (found_entries_it !=
local_module_resolver_->parameter_type_and_name_to_entries_.end()) {
found_entries.insert(found_entries_it->second.begin(),
found_entries_it->second.end());
}
return found_entries;
}
// At this point |candidates_| contains all the modules that could
// potentially match the query. The purpose of this method is to create
// those matches and populate |response_|.
void Finally(FlowToken flow) {
response_ = CreateEmptyResponse();
if (candidates_.empty()) {
return;
}
for (auto id : candidates_) {
auto entry_it = local_module_resolver_->entries_.find(id);
FXL_CHECK(entry_it != local_module_resolver_->entries_.end()) << id;
const auto& entry = entry_it->second;
fuchsia::modular::FindModulesResult result;
result.module_id = entry.binary;
result.manifest = fuchsia::modular::ModuleManifest::New();
fidl::Clone(entry, result.manifest.get());
response_.results.push_back(std::move(result));
}
}
fuchsia::modular::FindModulesResponse CreateEmptyResponse() {
fuchsia::modular::FindModulesResponse response;
response.results.resize(0);
return response;
}
fuchsia::modular::FindModulesResponse response_;
LocalModuleResolver* const local_module_resolver_;
fuchsia::modular::FindModulesQuery query_;
std::set<EntryId> candidates_;
FXL_DISALLOW_COPY_AND_ASSIGN(FindModulesCall);
};
class LocalModuleResolver::FindModulesByTypesCall
: public Operation<fuchsia::modular::FindModulesByTypesResponse> {
public:
FindModulesByTypesCall(LocalModuleResolver* const local_module_resolver,
fuchsia::modular::FindModulesByTypesQuery query,
ResultCall result_call)
: Operation("LocalModuleResolver::FindModulesByTypesCall",
std::move(result_call)),
local_module_resolver_(local_module_resolver),
query_(std::move(query)) {}
void Run() override {
FlowToken flow{this, &response_};
response_ = CreateEmptyResponse();
std::set<EntryId> candidates;
for (auto& constraint : *query_.parameter_constraints) {
std::set<EntryId> param_type_entries;
parameter_types_cache_[constraint.constraint_name] =
constraint.param_types.Clone();
for (auto& type : *constraint.param_types) {
auto found_entries = GetEntriesMatchingParameterByType(type);
candidates.insert(found_entries.begin(), found_entries.end());
}
}
for (auto& candidate : candidates) {
auto results = MatchQueryParametersToEntryParametersByType(
local_module_resolver_->entries_[candidate]);
using iter = decltype(results->begin());
response_.results->insert(response_.results->end(),
std::move_iterator<iter>(results->begin()),
std::move_iterator<iter>(results->end()));
}
}
private:
fuchsia::modular::FindModulesByTypesResponse CreateEmptyResponse() {
fuchsia::modular::FindModulesByTypesResponse r;
r.results.resize(0);
return r;
}
// Returns the set of all modules that have a parameter whose type is
// |parameter_type|.
std::set<LocalModuleResolver::EntryId> GetEntriesMatchingParameterByType(
const std::string& parameter_type) {
std::set<EntryId> found_entries;
auto found_entries_it =
local_module_resolver_->parameter_type_to_entries_.find(parameter_type);
if (found_entries_it !=
local_module_resolver_->parameter_type_to_entries_.end()) {
found_entries.insert(found_entries_it->second.begin(),
found_entries_it->second.end());
}
return found_entries;
}
// Creates FindModulesResults for each available mapping from
// parameters in |query_| to the corresponding parameters in each candidate
// entry.
//
// In order for a query to match an entry, it must contain enough parameters
// to populate each of the entry parameters.
// TODO(MI4-866): Handle entries with optional parameters.
fidl::VectorPtr<fuchsia::modular::FindModulesByTypesResult>
MatchQueryParametersToEntryParametersByType(
const fuchsia::modular::ModuleManifest& entry) {
fidl::VectorPtr<fuchsia::modular::FindModulesByTypesResult> modules;
modules.resize(0);
if (query_.parameter_constraints->size() <
entry.parameter_constraints->size()) {
return modules;
}
// Map each parameter in |entry| to the query parameter names that could
// be used to populate the |entry| parameter.
std::map<std::string, std::vector<std::string>>
entry_parameters_to_query_constraints =
MapEntryParametersToCompatibleQueryParameters(entry);
// Compute each possible map from |query_| parameter to the |entry|
// parameter that it should populate.
std::vector<std::map<std::string, std::string>> parameter_mappings =
ComputeResultsFromEntryParameterToQueryParameterMapping(
entry_parameters_to_query_constraints);
// For each of the possible mappings, create a resolver result.
for (const auto& parameter_mapping : parameter_mappings) {
fuchsia::modular::FindModulesByTypesResult result;
// TODO(vardhan): This score is a place holder. Compute a simple score for
// results.
result.score = 1.0f;
result.module_id = entry.binary;
result.action = entry.action;
for (auto& kv : parameter_mapping) {
fuchsia::modular::FindModulesByTypesParameterMapping entry;
entry.query_constraint_name = kv.first;
entry.result_param_name = kv.second;
result.parameter_mappings.push_back(std::move(entry));
}
result.manifest = fuchsia::modular::ModuleManifest::New();
fidl::Clone(entry, result.manifest.get());
modules.push_back(std::move(result));
}
return modules;
}
// Returns a map where the keys are the |entry|'s parameters, and the values
// are all the |query_| parameters that are type-compatible with that
// |entry| parameter.
std::map<std::string, std::vector<std::string>>
MapEntryParametersToCompatibleQueryParameters(
const fuchsia::modular::ModuleManifest& entry) {
std::map<std::string, std::vector<std::string>>
entry_parameter_to_query_constraints;
for (const auto& entry_parameter : *entry.parameter_constraints) {
std::vector<std::string> matching_query_constraints;
for (const auto& query_constraint : *query_.parameter_constraints) {
const auto& this_query_constraint_cache =
parameter_types_cache_[query_constraint.constraint_name];
if (std::find(this_query_constraint_cache->begin(),
this_query_constraint_cache->end(),
entry_parameter.type) !=
this_query_constraint_cache->end()) {
matching_query_constraints.push_back(
query_constraint.constraint_name);
}
}
entry_parameter_to_query_constraints[entry_parameter.name] =
matching_query_constraints;
}
return entry_parameter_to_query_constraints;
}
// Returns a collection of valid mappings where the key is the query
// parameter, and the value is the entry parameter to be populated with the
// query parameters contents.
//
// |remaining_entry_parameters| are all the entry parameters that are yet to
// be matched. |used_query_constraints| are all the query parameters that
// have already been used in the current solution.
std::vector<std::map<std::string, std::string>>
ComputeResultsFromEntryParameterToQueryParameterMapping(
const std::map<std::string, std::vector<std::string>>&
remaining_entry_parameters,
const std::set<std::string>& used_query_constraints = {}) {
std::vector<std::map<std::string, std::string>> result;
if (remaining_entry_parameters.empty()) {
return result;
}
auto first_entry_parameter_it = remaining_entry_parameters.begin();
const std::string& first_entry_parameter_name =
first_entry_parameter_it->first;
const std::vector<std::string> query_constraints_for_first_entry =
first_entry_parameter_it->second;
// If there is only one remaining entry parameter, create one result
// mapping for each viable query parameter.
if (remaining_entry_parameters.size() == 1) {
for (const auto& query_constraint_name :
query_constraints_for_first_entry) {
// Don't create solutions where the query parameter has already been
// used.
if (used_query_constraints.find(query_constraint_name) !=
used_query_constraints.end()) {
continue;
}
std::map<std::string, std::string> result_map;
result_map[query_constraint_name] = first_entry_parameter_name;
result.push_back(result_map);
}
return result;
}
for (const auto& query_constraint_name : first_entry_parameter_it->second) {
// If the query parameter has already been used, it cannot be matched
// again, and thus the loop continues.
if (used_query_constraints.find(query_constraint_name) !=
used_query_constraints.end()) {
continue;
}
// The current query parameter that will be used by the first entry
// parameter must be added to the used set before computing the solution
// to the smaller problem.
std::set<std::string> new_used_query_constraints = used_query_constraints;
new_used_query_constraints.insert(query_constraint_name);
// Recurse for the remaining parameters.
std::vector<std::map<std::string, std::string>> solution_for_remainder =
ComputeResultsFromEntryParameterToQueryParameterMapping(
{std::next(remaining_entry_parameters.begin()),
remaining_entry_parameters.end()},
new_used_query_constraints);
// Expand each solution to the smaller problem by inserting the current
// query parameter -> entry parameter into the solution.
for (const auto& existing_solution : solution_for_remainder) {
std::map<std::string, std::string> updated_solution = existing_solution;
updated_solution[query_constraint_name] = first_entry_parameter_name;
result.push_back(updated_solution);
}
}
return result;
}
LocalModuleResolver* const local_module_resolver_;
fuchsia::modular::FindModulesByTypesQuery const query_;
fuchsia::modular::FindModulesByTypesResponse response_;
// A cache of the parameter types for each parameter name in |query_|.
std::map<std::string, fidl::VectorPtr<fidl::StringPtr>>
parameter_types_cache_;
FXL_DISALLOW_COPY_AND_ASSIGN(FindModulesByTypesCall);
};
void LocalModuleResolver::FindModules(fuchsia::modular::FindModulesQuery query,
FindModulesCallback callback) {
FXL_DCHECK(!query.action.is_null());
operations_.Add(new FindModulesCall(this, std::move(query), callback));
}
void LocalModuleResolver::FindModulesByTypes(
fuchsia::modular::FindModulesByTypesQuery query,
FindModulesByTypesCallback callback) {
operations_.Add(new FindModulesByTypesCall(this, std::move(query), callback));
}
void LocalModuleResolver::GetModuleManifest(
fidl::StringPtr module_id, GetModuleManifestCallback callback) {
FXL_DCHECK(!module_id.is_null());
for (auto& entry : entries_) {
if (entry.first.second == module_id.get()) {
callback(CloneOptional(entry.second));
return;
}
}
callback(nullptr);
}
namespace {
bool StringStartsWith(const std::string& str, const std::string& prefix) {
return str.compare(0, prefix.length(), prefix) == 0;
}
} // namespace
void LocalModuleResolver::OnQuery(fuchsia::modular::UserInput query,
OnQueryCallback done) {
// TODO(thatguy): This implementation is bare-bones. Don't judge.
// Before adding new member variables to support OnQuery() (and tying the
// LocalModuleResolver internals up with what's needed for this method),
// please split the index-building & querying portion of LocalModuleResolver
// out into its own class. Then, make a new class to handle OnQuery() and
// share the same index instance here and there.
auto proposals = fidl::VectorPtr<fuchsia::modular::Proposal>::New(0);
if (query.text->empty()) {
fuchsia::modular::QueryResponse response;
response.proposals = std::move(proposals);
done(std::move(response));
return;
}
for (const auto& id_entry : entries_) {
const auto& entry = id_entry.second;
// Simply prefix match on the last element of the action.
// actions have a convention of being namespaced like java classes:
// com.google.subdomain.action
std::string action = entry.action;
auto parts =
fxl::SplitString(action, ".", fxl::kKeepWhitespace, fxl::kSplitWantAll);
const auto& last_part = parts.back();
if (StringStartsWith(entry.action, query.text) ||
StringStartsWith(last_part.ToString(), query.text)) {
fuchsia::modular::Proposal proposal;
proposal.id = entry.binary;
fuchsia::modular::CreateStory create_story;
create_story.intent.handler = entry.binary;
fuchsia::modular::Action action;
action.set_create_story(std::move(create_story));
proposal.on_selected.push_back(std::move(action));
proposal.display.headline =
std::string("Go go gadget ") + last_part.ToString();
proposal.display.subheadline = entry.binary;
proposal.display.color = 0xffffffff;
proposal.display.annoyance = fuchsia::modular::AnnoyanceType::NONE;
proposal.confidence = 1.0; // Yeah, super confident.
proposals.push_back(std::move(proposal));
}
}
if (proposals->size() > 10) {
proposals.resize(10);
}
fuchsia::modular::QueryResponse response;
response.proposals = std::move(proposals);
done(std::move(response));
}
void LocalModuleResolver::OnSourceIdle(const std::string& source_name) {
auto res = ready_sources_.insert(source_name);
if (!res.second) {
// It's OK for us to get an idle notification twice from a repo. This
// happens, for instance, if there's a network problem and we have to
// re-establish it.
return;
}
if (AllSourcesAreReady()) {
// They are all ready. Bind any pending Connect() calls.
for (auto& request : pending_bindings_) {
bindings_.AddBinding(this, std::move(request));
}
pending_bindings_.clear();
}
}
void LocalModuleResolver::OnNewManifestEntry(
const std::string& source_name, std::string id_in,
fuchsia::modular::ModuleManifest new_entry) {
FXL_LOG(INFO) << "New Module manifest " << id_in
<< ": action = " << new_entry.action
<< ", binary = " << new_entry.binary;
// Add this new entry info to our local index.
if (entries_.count(EntryId(source_name, id_in)) > 0) {
// Remove this existing entry first, then add it back in.
OnRemoveManifestEntry(source_name, id_in);
}
auto ret =
entries_.emplace(EntryId(source_name, id_in), std::move(new_entry));
FXL_CHECK(ret.second);
const auto& id = ret.first->first;
const auto& entry = ret.first->second;
action_to_entries_[entry.action].insert(id);
for (const auto& constraint : *entry.parameter_constraints) {
parameter_type_and_name_to_entries_[std::make_pair(constraint.type,
constraint.name)]
.insert(id);
parameter_type_to_entries_[constraint.type].insert(id);
}
}
void LocalModuleResolver::OnRemoveManifestEntry(const std::string& source_name,
std::string id_in) {
EntryId id{source_name, id_in};
auto it = entries_.find(id);
if (it == entries_.end()) {
FXL_LOG(WARNING) << "Asked to remove non-existent manifest entry: " << id;
return;
}
const auto& entry = it->second;
action_to_entries_[entry.action].erase(id);
for (const auto& constraint : *entry.parameter_constraints) {
parameter_type_and_name_to_entries_[std::make_pair(constraint.type,
constraint.name)]
.erase(id);
parameter_type_to_entries_[constraint.type].erase(id);
}
entries_.erase(id);
}
void LocalModuleResolver::PeriodicCheckIfSourcesAreReady() {
if (!AllSourcesAreReady()) {
for (const auto& it : sources_) {
if (ready_sources_.count(it.first) == 0) {
FXL_LOG(WARNING) << "Still waiting on source: " << it.first;
}
}
if (already_checking_if_sources_are_ready_)
return;
already_checking_if_sources_are_ready_ = true;
async::PostDelayedTask(
async_get_default_dispatcher(),
[weak_this = weak_factory_.GetWeakPtr()]() {
if (weak_this) {
weak_this->already_checking_if_sources_are_ready_ = false;
weak_this->PeriodicCheckIfSourcesAreReady();
}
},
zx::sec(10));
}
}
} // namespace modular