aboutsummaryrefslogtreecommitdiff
path: root/mod
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2024-08-06 22:03:31 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2024-08-07 19:19:22 +0300
commit443088f6093d3420212be0e1af3b9e802dca9362 (patch)
treeb1ec3b0c62ee0b8d66b0cbf21e21d68ae0d4f806 /mod
parent7db53790ca2d2c004bfd00b503eca59a8d084870 (diff)
Add support for advanced package search
Diffstat (limited to 'mod')
-rw-r--r--mod/mod-advanced-search.cxx342
-rw-r--r--mod/mod-advanced-search.hxx41
-rw-r--r--mod/mod-builds.cxx69
-rw-r--r--mod/mod-package-details.cxx10
-rw-r--r--mod/mod-repository-root.cxx23
-rw-r--r--mod/mod-repository-root.hxx2
-rw-r--r--mod/module.cli47
-rw-r--r--mod/utility.cxx69
-rw-r--r--mod/utility.hxx7
9 files changed, 537 insertions, 73 deletions
diff --git a/mod/mod-advanced-search.cxx b/mod/mod-advanced-search.cxx
new file mode 100644
index 0000000..5de3b9a
--- /dev/null
+++ b/mod/mod-advanced-search.cxx
@@ -0,0 +1,342 @@
+// file : mod/mod-advanced-search.cxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#include <mod/mod-advanced-search.hxx>
+
+#include <libstudxml/serializer.hxx>
+
+#include <odb/database.hxx>
+#include <odb/transaction.hxx>
+
+#include <web/server/module.hxx>
+#include <web/server/mime-url-encoding.hxx>
+
+#include <web/xhtml/serialization.hxx>
+
+#include <libbrep/package.hxx>
+#include <libbrep/package-odb.hxx>
+
+#include <mod/page.hxx>
+#include <mod/utility.hxx> // wildcard_to_similar_to_pattern()
+#include <mod/module-options.hxx>
+
+using namespace std;
+using namespace butl;
+using namespace web;
+using namespace odb::core;
+using namespace brep::cli;
+
+// While currently the user-defined copy constructor is not required (we don't
+// need to deep copy nullptr's), it is a good idea to keep the placeholder
+// ready for less trivial cases.
+//
+brep::advanced_search::
+advanced_search (const advanced_search& r)
+ : database_module (r),
+ options_ (r.initialized_ ? r.options_ : nullptr)
+{
+}
+
+void brep::advanced_search::
+init (scanner& s)
+{
+ HANDLER_DIAG;
+
+ options_ = make_shared<options::advanced_search> (
+ s, unknown_mode::fail, unknown_mode::fail);
+
+ database_module::init (*options_, options_->package_db_retry ());
+
+ if (options_->root ().empty ())
+ options_->root (dir_path ("/"));
+}
+
+template <typename T, typename C>
+static inline query<T>
+match (const C qc, const string& pattern)
+{
+ return qc +
+ "SIMILAR TO" +
+ query<T>::_val (brep::wildcard_to_similar_to_pattern (pattern));
+}
+
+template <typename T>
+static inline query<T>
+package_query (const brep::params::advanced_search& params)
+{
+ using namespace brep;
+ using query = query<T>;
+
+ query q (query::internal_repository.canonical_name.is_not_null ());
+
+ // Note that there is no error reported if the filter parameters parsing
+ // fails. Instead, it is considered that no package builds match such a
+ // query.
+ //
+ try
+ {
+ // Package name.
+ //
+ if (!params.name ().empty ())
+ q = q && match<T> (query::id.name, params.name ());
+
+ // Package version.
+ //
+ if (!params.version ().empty () && params.version () != "*")
+ {
+ // May throw invalid_argument.
+ //
+ version v (params.version (), version::none);
+
+ q = q && compare_version_eq (query::id.version,
+ canonical_version (v),
+ v.revision.has_value ());
+ }
+
+ // Package project.
+ //
+ if (!params.project ().empty ())
+ q = q && match<T> (query::project, params.project ());
+
+ // Reviews.
+ //
+ const string& rs (params.reviews ());
+
+ if (rs != "*")
+ {
+ if (rs == "reviewed")
+ q = q && query::reviews.pass.is_not_null ();
+ else if (rs == "unreviewed")
+ q = q && query::reviews.pass.is_null ();
+ else
+ throw invalid_argument ("");
+ }
+ }
+ catch (const invalid_argument&)
+ {
+ return query (false);
+ }
+
+ return q;
+}
+
+static const vector<pair<string, string>> reviews ({
+ {"*", "*"},
+ {"reviewed", "reviewed"},
+ {"unreviewed", "unreviewed"}});
+
+bool brep::advanced_search::
+handle (request& rq, response& rs)
+{
+ using namespace web::xhtml;
+
+ HANDLER_DIAG;
+
+ // Note that while we could potentially support the multi-tenant mode, that
+ // would require to invent the package/tenant view to filter out the private
+ // tenants from the search. This doesn't look of much use at the moment.
+ // Thus, let's keep it simple for now and just respond with the 501 status
+ // code (not implemented) if such a mode is detected.
+ //
+ if (!tenant.empty ())
+ throw invalid_request (501, "not implemented");
+
+ const size_t res_page (options_->search_page_entries ());
+ const dir_path& root (options_->root ());
+
+ params::advanced_search params;
+
+ try
+ {
+ name_value_scanner s (rq.parameters (8 * 1024));
+ params = params::advanced_search (s,
+ unknown_mode::fail,
+ unknown_mode::fail);
+ }
+ catch (const cli::exception& e)
+ {
+ throw invalid_request (400, e.what ());
+ }
+
+ const char* title ("Advanced Package Search");
+
+ xml::serializer s (rs.content (), title);
+
+ s << HTML
+ << HEAD
+ << TITLE << title << ~TITLE
+ << CSS_LINKS (path ("advanced-search.css"), root)
+ << ~HEAD
+ << BODY
+ << DIV_HEADER (options_->logo (), options_->menu (), root, tenant)
+ << DIV(ID="content");
+
+ transaction t (package_db_->begin ());
+
+ size_t count (
+ package_db_->query_value<package_count> (
+ package_query<package_count> (params)));
+
+ // Print the package builds filter form on the first page only.
+ //
+ size_t page (params.page ());
+
+ if (page == 0)
+ {
+ // The 'action' attribute is optional in HTML5. While the standard
+ // doesn't specify browser behavior explicitly for the case the
+ // attribute is omitted, the only reasonable behavior is to default it
+ // to the current document URL.
+ //
+ s << FORM
+ << TABLE(ID="filter", CLASS="proplist")
+ << TBODY
+ << TR_INPUT ("name", "advanced-search", params.name (), "*", true)
+ << TR_INPUT ("version", "pv", params.version (), "*")
+ << TR_INPUT ("project", "pr", params.project (), "*");
+
+ if (options_->reviews_url_specified ())
+ s << TR_SELECT ("reviews", "rv", params.reviews (), reviews);
+
+ s << ~TBODY
+ << ~TABLE
+ << TABLE(CLASS="form-table")
+ << TBODY
+ << TR
+ << TD(ID="package-version-count")
+ << DIV_COUNTER (count, "Package Version", "Package Versions")
+ << ~TD
+ << TD(ID="filter-btn")
+ << *INPUT(TYPE="submit", VALUE="Filter")
+ << ~TD
+ << ~TR
+ << ~TBODY
+ << ~TABLE
+ << ~FORM;
+ }
+ else
+ s << DIV_COUNTER (count, "Package Version", "Package Versions");
+
+ using query = query<package>;
+
+ // Note that we query an additional package version which we will not
+ // display, but will use to check if it belongs to the same package and/or
+ // project as the last displayed package version. If that's the case we will
+ // display the '...' mark(s) at the end of the page, indicating that there a
+ // more package versions from this package/project on the next page(s).
+ //
+ query q (package_query<package> (params) +
+ "ORDER BY tenant, project, name, version_epoch DESC, "
+ "version_canonical_upstream DESC, version_canonical_release DESC, "
+ "version_revision DESC" +
+ "OFFSET" + to_string (page * res_page) +
+ "LIMIT" + to_string (res_page + 1));
+
+ package_name prj;
+ package_name pkg;
+ size_t n (0);
+
+ for (package& p: package_db_->query<package> (q))
+ {
+ if (!p.id.tenant.empty ())
+ throw invalid_request (501, "not implemented");
+
+ if (n++ == res_page)
+ {
+ if (p.project == prj)
+ {
+ if (p.name == pkg)
+ s << DIV(ID="package-break") << "..." << ~DIV;
+
+ s << DIV(ID="project-break") << "..." << ~DIV;
+ }
+
+ break;
+ }
+
+ if (p.project != prj)
+ {
+ prj = move (p.project);
+ pkg = package_name ();
+
+ s << TABLE(CLASS="proplist project")
+ << TBODY
+ << TR_VALUE ("project", prj.string ())
+ << ~TBODY
+ << ~TABLE;
+ }
+
+ if (p.name != pkg)
+ {
+ pkg = move (p.name);
+
+ s << TABLE(CLASS="proplist package")
+ << TBODY
+ << TR_NAME (pkg, root, p.tenant)
+ << TR_SUMMARY (p.summary)
+ << TR_LICENSE (p.license_alternatives)
+ << ~TBODY
+ << ~TABLE;
+ }
+
+ s << TABLE(CLASS="proplist version")
+ << TBODY
+ << TR_VERSION (pkg, p.version, root, tenant, p.upstream_version);
+
+ assert (p.internal ());
+
+ const repository_location& rl (p.internal_repository.load ()->location);
+
+ s << TR_REPOSITORY (rl, root, tenant)
+ << TR_DEPENDS (p.dependencies, root, tenant)
+ << TR_REQUIRES (p.requirements);
+
+ if (options_->reviews_url_specified ())
+ {
+ package_db_->load (p, p.reviews_section);
+
+ s << TR_REVIEWS_SUMMARY (p.reviews, options_->reviews_url ());
+ }
+
+ s << ~TBODY
+ << ~TABLE;
+ }
+
+ t.commit ();
+
+ string u (root.string () + "?advanced-search");
+
+ if (!params.name ().empty ())
+ {
+ u += '=';
+ u += mime_url_encode (params.name ());
+ }
+
+ auto add_filter = [&u] (const char* pn,
+ const string& pv,
+ const char* def = "")
+ {
+ if (pv != def)
+ {
+ u += '&';
+ u += pn;
+ u += '=';
+ u += mime_url_encode (pv);
+ }
+ };
+
+ add_filter ("pv", params.version ());
+ add_filter ("pr", params.project ());
+ add_filter ("rv", params.reviews (), "*");
+
+ s << DIV_PAGER (page,
+ count,
+ res_page,
+ options_->search_pages (),
+ u)
+ << ~DIV
+ << ~BODY
+ << ~HTML;
+
+ return true;
+}
diff --git a/mod/mod-advanced-search.hxx b/mod/mod-advanced-search.hxx
new file mode 100644
index 0000000..4ab4d42
--- /dev/null
+++ b/mod/mod-advanced-search.hxx
@@ -0,0 +1,41 @@
+// file : mod/mod-advanced-search.hxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#ifndef MOD_MOD_ADVANCED_SEARCH_HXX
+#define MOD_MOD_ADVANCED_SEARCH_HXX
+
+#include <libbrep/types.hxx>
+#include <libbrep/utility.hxx>
+
+#include <mod/module-options.hxx>
+#include <mod/database-module.hxx>
+
+namespace brep
+{
+ class advanced_search: public database_module
+ {
+ public:
+ advanced_search () = default;
+
+ // Create a shallow copy (handling instance) if initialized and a deep
+ // copy (context exemplar) otherwise.
+ //
+ explicit
+ advanced_search (const advanced_search&);
+
+ virtual bool
+ handle (request&, response&);
+
+ virtual const cli::options&
+ cli_options () const {return options::advanced_search::description ();}
+
+ private:
+ virtual void
+ init (cli::scanner&);
+
+ private:
+ shared_ptr<options::advanced_search> options_;
+ };
+}
+
+#endif // MOD_MOD_ADVANCED_SEARCH_HXX
diff --git a/mod/mod-builds.cxx b/mod/mod-builds.cxx
index 81d4649..0155c2e 100644
--- a/mod/mod-builds.cxx
+++ b/mod/mod-builds.cxx
@@ -27,6 +27,7 @@
#include <libbrep/build-package-odb.hxx>
#include <mod/page.hxx>
+#include <mod/utility.hxx> // wildcard_to_similar_to_pattern()
#include <mod/module-options.hxx>
using namespace std;
@@ -63,71 +64,13 @@ init (scanner& s)
options_->root (dir_path ("/"));
}
-// Transform the wildcard to the SIMILAR TO-pattern.
-//
-static string
-transform (const string& pattern)
-{
- if (pattern.empty ())
- return "%";
-
- string r;
- for (const path_pattern_term& pt: path_pattern_iterator (pattern))
- {
- switch (pt.type)
- {
- case path_pattern_term_type::question: r += '_'; break;
- case path_pattern_term_type::star: r += '%'; break;
- case path_pattern_term_type::bracket:
- {
- // Copy the bracket expression translating the inverse character, if
- // present.
- //
- size_t n (r.size ());
- r.append (pt.begin, pt.end);
-
- if (r[n + 1] == '!') // ...[!... ?
- r[n + 1] = '^';
-
- break;
- }
- case path_pattern_term_type::literal:
- {
- char c (get_literal (pt));
-
- // Escape the special characters.
- //
- // Note that '.' is not a special character for SIMILAR TO.
- //
- switch (c)
- {
- case '\\':
- case '%':
- case '_':
- case '|':
- case '+':
- case '{':
- case '}':
- case '(':
- case ')':
- case '[':
- case ']': r += '\\'; break;
- }
-
- r += c;
- break;
- }
- }
- }
-
- return r;
-}
-
template <typename T, typename C>
static inline query<T>
match (const C qc, const string& pattern)
{
- return qc + "SIMILAR TO" + query<T>::_val (transform (pattern));
+ return qc +
+ "SIMILAR TO" +
+ query<T>::_val (brep::wildcard_to_similar_to_pattern (pattern));
}
// If tenant is absent, then query builds from all the public tenants.
@@ -450,9 +393,7 @@ handle (request& rq, response& rs)
// The 'action' attribute is optional in HTML5. While the standard
// doesn't specify browser behavior explicitly for the case the
// attribute is omitted, the only reasonable behavior is to default it
- // to the current document URL. Note that we specify the function name
- // using the "hidden" <input/> element since the action url must not
- // contain the query part.
+ // to the current document URL.
//
s << FORM
<< TABLE(ID="filter", CLASS="proplist")
diff --git a/mod/mod-package-details.cxx b/mod/mod-package-details.cxx
index 15a4115..ceb23c5 100644
--- a/mod/mod-package-details.cxx
+++ b/mod/mod-package-details.cxx
@@ -119,7 +119,7 @@ handle (request& rq, response& rs)
throw invalid_request (400, "invalid package name format");
}
- const package_name& name (pkg->name);
+ const package_name& name (pkg->name);
const string ename (mime_url_encode (name.string (), false));
auto url = [&ename] (bool f = false,
@@ -226,8 +226,8 @@ handle (request& rq, response& rs)
}
size_t pkg_count (
- package_db_->query_value<package_count> (
- search_params<package_count> (squery, tenant, name)));
+ package_db_->query_value<package_search_count> (
+ search_params<package_search_count> (squery, tenant, name)));
// Let's disable autofocus in the full page mode since clicking the full or
// more link the user most likely intends to read rather than search, while
@@ -244,8 +244,8 @@ handle (request& rq, response& rs)
search_params<package_search_rank> (squery, tenant, name) +
"ORDER BY rank DESC, version_epoch DESC, "
"version_canonical_upstream DESC, version_canonical_release DESC, "
- "version_revision DESC" +
- "OFFSET" + to_string (page * res_page) +
+ "version_revision DESC" +
+ "OFFSET" + to_string (page * res_page) +
"LIMIT" + to_string (res_page)))
{
shared_ptr<package> p (package_db_->load<package> (pr.id));
diff --git a/mod/mod-repository-root.cxx b/mod/mod-repository-root.cxx
index bc861a8..165302d 100644
--- a/mod/mod-repository-root.cxx
+++ b/mod/mod-repository-root.cxx
@@ -25,6 +25,7 @@
#include <mod/mod-build-result.hxx>
#include <mod/mod-build-configs.hxx>
#include <mod/mod-package-details.hxx>
+#include <mod/mod-advanced-search.hxx>
#include <mod/mod-repository-details.hxx>
#include <mod/mod-package-version-details.hxx>
@@ -118,6 +119,7 @@ namespace brep
//
tenant_service_map_ (make_shared<tenant_service_map> ()),
packages_ (make_shared<packages> ()),
+ advanced_search_ (make_shared<advanced_search> ()),
package_details_ (make_shared<package_details> ()),
package_version_details_ (make_shared<package_version_details> ()),
repository_details_ (make_shared<repository_details> ()),
@@ -153,6 +155,10 @@ namespace brep
r.initialized_
? r.packages_
: make_shared<packages> (*r.packages_)),
+ advanced_search_ (
+ r.initialized_
+ ? r.advanced_search_
+ : make_shared<advanced_search> (*r.advanced_search_)),
package_details_ (
r.initialized_
? r.package_details_
@@ -225,6 +231,7 @@ namespace brep
{
option_descriptions r (handler::options ());
append (r, packages_->options ());
+ append (r, advanced_search_->options ());
append (r, package_details_->options ());
append (r, package_version_details_->options ());
append (r, repository_details_->options ());
@@ -272,6 +279,7 @@ namespace brep
// Initialize sub-handlers.
//
sub_init (*packages_, "packages");
+ sub_init (*advanced_search_, "advanced_search");
sub_init (*package_details_, "package_details");
sub_init (*package_version_details_, "package_version_details");
sub_init (*repository_details_, "repository_details");
@@ -305,7 +313,13 @@ namespace brep
auto verify = [&fail] (const string& v, const char* what)
{
cstrings vs ({
- "packages", "builds", "build-configs", "about", "submit", "ci"});
+ "packages",
+ "advanced-search",
+ "builds",
+ "build-configs",
+ "about",
+ "submit",
+ "ci"});
if (find (vs.begin (), vs.end (), v) == vs.end ())
fail << what << " value '" << v << "' is invalid";
@@ -459,6 +473,13 @@ namespace brep
return handle ("packages", param);
}
+ else if (func == "advanced-search")
+ {
+ if (handler_ == nullptr)
+ handler_.reset (new advanced_search (*advanced_search_));
+
+ return handle ("advanced_search", param);
+ }
else if (func == "about")
{
if (handler_ == nullptr)
diff --git a/mod/mod-repository-root.hxx b/mod/mod-repository-root.hxx
index 990587e..5a57403 100644
--- a/mod/mod-repository-root.hxx
+++ b/mod/mod-repository-root.hxx
@@ -14,6 +14,7 @@
namespace brep
{
class packages;
+ class advanced_search;
class package_details;
class package_version_details;
class repository_details;
@@ -64,6 +65,7 @@ namespace brep
shared_ptr<tenant_service_map> tenant_service_map_;
shared_ptr<packages> packages_;
+ shared_ptr<advanced_search> advanced_search_;
shared_ptr<package_details> package_details_;
shared_ptr<package_version_details> package_version_details_;
shared_ptr<repository_details> repository_details_;
diff --git a/mod/module.cli b/mod/module.cli
index 7b0c0d2..41684bd 100644
--- a/mod/module.cli
+++ b/mod/module.cli
@@ -545,6 +545,15 @@ namespace brep
}
};
+ class advanced_search: package_db,
+ search,
+ page,
+ repository_url,
+ package_version_metadata,
+ handler
+ {
+ };
+
class package_details: package, package_db,
search,
page,
@@ -871,11 +880,11 @@ namespace brep
// Web handler HTTP request parameters.
//
+ // Use parameters long names in the C++ code, short aliases (if present) in
+ // HTTP URL.
+ //
namespace params
{
- // Use parameters long names in the C++ code, short aliases (if present)
- // in HTTP URL.
- //
class packages
{
// Display package search result list starting from this page.
@@ -890,6 +899,38 @@ namespace brep
string q | _;
};
+ class advanced_search
+ {
+ // Display advanced package search result list starting from this page.
+ //
+ uint16_t page | p;
+
+ // Advanced package search filter options.
+ //
+
+ // Package name wildcard. An empty value is treated the same way as *.
+ //
+ // Note that the advanced-search parameter is renamed to '_' by the root
+ // handler (see the request_proxy class for details).
+ //
+ string name | _;
+
+ // Package version. If empty or *, then no version constraint is applied.
+ // Otherwise the package version must match the value exactly.
+ //
+ string version | pv;
+
+ // Package project wildcard. An empty value is treated the same way as *.
+ //
+ string project | pr;
+
+ // Package version reviews. If *, then no reviews-related constraint is
+ // applied. Otherwise the value is supposed to be the one of the
+ // following statuses: reviewed and unreviewed.
+ //
+ string reviews | rv = "*";
+ };
+
class package_details
{
// Display package version search result list starting from this page.
diff --git a/mod/utility.cxx b/mod/utility.cxx
new file mode 100644
index 0000000..5ca16a0
--- /dev/null
+++ b/mod/utility.cxx
@@ -0,0 +1,69 @@
+// file : mod/utility.cxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#include <mod/utility.hxx>
+
+#include <libbutl/path-pattern.hxx>
+
+namespace brep
+{
+ string
+ wildcard_to_similar_to_pattern (const string& wildcard)
+ {
+ using namespace butl;
+
+ if (wildcard.empty ())
+ return "%";
+
+ string r;
+ for (const path_pattern_term& pt: path_pattern_iterator (wildcard))
+ {
+ switch (pt.type)
+ {
+ case path_pattern_term_type::question: r += '_'; break;
+ case path_pattern_term_type::star: r += '%'; break;
+ case path_pattern_term_type::bracket:
+ {
+ // Copy the bracket expression translating the inverse character, if
+ // present.
+ //
+ size_t n (r.size ());
+ r.append (pt.begin, pt.end);
+
+ if (r[n + 1] == '!') // ...[!... ?
+ r[n + 1] = '^';
+
+ break;
+ }
+ case path_pattern_term_type::literal:
+ {
+ char c (get_literal (pt));
+
+ // Escape the special characters.
+ //
+ // Note that '.' is not a special character for SIMILAR TO.
+ //
+ switch (c)
+ {
+ case '\\':
+ case '%':
+ case '_':
+ case '|':
+ case '+':
+ case '{':
+ case '}':
+ case '(':
+ case ')':
+ case '[':
+ case ']': r += '\\'; break;
+ }
+
+ r += c;
+ break;
+ }
+ }
+ }
+
+ return r;
+ }
+}
diff --git a/mod/utility.hxx b/mod/utility.hxx
index 43527ae..07fbf8b 100644
--- a/mod/utility.hxx
+++ b/mod/utility.hxx
@@ -19,6 +19,13 @@ namespace brep
? path_cast<dir_path> (dir / ('@' + tenant))
: dir;
}
+
+ // Transform the wildcard to the `SIMILAR TO` pattern.
+ //
+ // Note that the empty wildcard is transformed to the '%' pattern.
+ //
+ string
+ wildcard_to_similar_to_pattern (const string&);
}
#endif // MOD_UTILITY_HXX