aboutsummaryrefslogtreecommitdiff
path: root/mod
diff options
context:
space:
mode:
Diffstat (limited to 'mod')
-rw-r--r--mod/.gitignore2
-rw-r--r--mod/buildfile55
-rw-r--r--mod/database26
-rw-r--r--mod/database-module55
-rw-r--r--mod/database-module.cxx50
-rw-r--r--mod/database.cxx60
-rw-r--r--mod/diagnostics306
-rw-r--r--mod/diagnostics.cxx30
-rw-r--r--mod/mod-package-details42
-rw-r--r--mod/mod-package-details.cxx258
-rw-r--r--mod/mod-package-search42
-rw-r--r--mod/mod-package-search.cxx181
-rw-r--r--mod/mod-package-version-details45
-rw-r--r--mod/mod-package-version-details.cxx320
-rw-r--r--mod/mod-repository-details42
-rw-r--r--mod/mod-repository-details.cxx147
-rw-r--r--mod/mod-repository-root60
-rw-r--r--mod/mod-repository-root.cxx263
-rw-r--r--mod/module201
-rw-r--r--mod/module.cxx410
-rw-r--r--mod/options-types32
-rw-r--r--mod/options.cli211
-rw-r--r--mod/page403
-rw-r--r--mod/page.cxx693
-rw-r--r--mod/services.cxx15
-rw-r--r--mod/types-parsers57
-rw-r--r--mod/types-parsers.cxx114
27 files changed, 4120 insertions, 0 deletions
diff --git a/mod/.gitignore b/mod/.gitignore
new file mode 100644
index 0000000..ddd62b8
--- /dev/null
+++ b/mod/.gitignore
@@ -0,0 +1,2 @@
+options
+options.?xx
diff --git a/mod/buildfile b/mod/buildfile
new file mode 100644
index 0000000..574b34a
--- /dev/null
+++ b/mod/buildfile
@@ -0,0 +1,55 @@
+# file : mod/buildfile
+# copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+# license : MIT; see accompanying LICENSE file
+
+define mod: libso
+mod{*}: bin.libprefix = mod_
+mod{*}: install = libexec
+
+import libs += libodb%lib{odb}
+import libs += libodb-pgsql%lib{odb-pgsql}
+import libs += libbpkg%lib{bpkg}
+import libs += libstudxml%lib{studxml}
+
+include ../brep/
+
+mod{brep}: \
+ {hxx cxx}{ database } \
+ {hxx cxx}{ database-module } \
+ {hxx cxx}{ diagnostics } \
+ {hxx cxx}{ mod-package-details } \
+ {hxx cxx}{ mod-package-search } \
+ {hxx cxx}{ mod-package-version-details } \
+ {hxx cxx}{ mod-repository-details } \
+ {hxx cxx}{ mod-repository-root } \
+ {hxx cxx}{ module } \
+ {hxx ixx cxx}{ options } \
+ {hxx }{ options-types } \
+ {hxx cxx}{ page } \
+ { cxx}{ services } \
+ {hxx cxx}{ types-parsers } \
+ ../web/{hxx cxx}{ mime-url-encoding } \
+ ../web/{hxx }{ module } \
+ ../web/{hxx }{ xhtml } \
+ ../web/{hxx cxx}{ xhtml-fragment } \
+../web/apache/{hxx }{ log } \
+../web/apache/{hxx ixx cxx}{ request } \
+../web/apache/{hxx txx cxx}{ service } \
+../web/apache/{hxx }{ stream } \
+../brep/lib{brep} $libs
+
+# Don't install any of the module's headers.
+#
+{hxx ixx txx}{*}: install = false
+../web/{hxx ixx txx}{*}: install = false
+
+# Set option prefix to the empty value to handle all unknown request parameters
+# uniformly with a single catch block.
+#
+cli.options += --std c++11 -I $src_root --include-with-brackets \
+--include-prefix mod --guard-prefix MOD \
+--cxx-prologue "#include <mod/types-parsers>" \
+--cli-namespace brep::cli --generate-file-scanner --suppress-usage \
+--generate-modifier --generate-description --option-prefix ""
+
+{hxx ixx cxx}{options}: cli{options}
diff --git a/mod/database b/mod/database
new file mode 100644
index 0000000..2730449
--- /dev/null
+++ b/mod/database
@@ -0,0 +1,26 @@
+// file : mod/database -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef MOD_DATABASE
+#define MOD_DATABASE
+
+#include <odb/forward.hxx> // database
+
+#include <brep/types>
+#include <brep/utility>
+
+#include <mod/options>
+
+namespace brep
+{
+ // Returns pointer to the shared database instance, creating one on the
+ // first call. On subsequent calls ensures passed host and port equals
+ // to ones of the existing database instance throwing runtime_error
+ // otherwise. Is not thread-safe.
+ //
+ shared_ptr<odb::core::database>
+ shared_database (const options::db&);
+}
+
+#endif // MOD_DATABASE
diff --git a/mod/database-module b/mod/database-module
new file mode 100644
index 0000000..0933794
--- /dev/null
+++ b/mod/database-module
@@ -0,0 +1,55 @@
+// file : mod/database-module -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef MOD_DATABASE_MODULE
+#define MOD_DATABASE_MODULE
+
+#include <odb/forward.hxx> // database
+
+#include <brep/types>
+#include <brep/utility>
+
+#include <mod/module>
+#include <mod/options>
+
+namespace brep
+{
+ // A module that utilises the database. Specifically, it will retry the
+ // request in the face of recoverable database failures (deadlock, loss of
+ // connection, etc) up to a certain number of times.
+ //
+ class database_module: public module
+ {
+ protected:
+ database_module () = default;
+
+ // Create a shallow copy (handling instance) if initialized and a deep
+ // copy (context exemplar) otherwise.
+ //
+ explicit
+ database_module (const database_module&);
+
+ // Required to avoid getting warning from clang that
+ // database_module::init() hides module::init() virtual functions. This
+ // way all functions get to the same scope and become overloaded set.
+ //
+ using module::init;
+
+ void
+ init (const options::db&);
+
+ virtual bool
+ handle (request&, response&) = 0;
+
+ protected:
+ size_t retry_;
+ shared_ptr<odb::core::database> db_;
+
+ private:
+ virtual bool
+ handle (request&, response&, log&);
+ };
+}
+
+#endif // MOD_DATABASE_MODULE
diff --git a/mod/database-module.cxx b/mod/database-module.cxx
new file mode 100644
index 0000000..a794672
--- /dev/null
+++ b/mod/database-module.cxx
@@ -0,0 +1,50 @@
+// file : mod/database-module.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <mod/database-module>
+
+#include <odb/exceptions.hxx>
+
+#include <mod/options>
+#include <mod/database>
+
+namespace brep
+{
+ // 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.
+ //
+ database_module::
+ database_module (const database_module& r)
+ : module (r),
+ retry_ (r.retry_),
+ db_ (r.initialized_ ? r.db_ : nullptr)
+ {
+ }
+
+ void database_module::
+ init (const options::db& o)
+ {
+ retry_ = o.db_retry ();
+ db_ = shared_database (o);
+ }
+
+ bool database_module::
+ handle (request& rq, response& rs, log& l)
+ try
+ {
+ return module::handle (rq, rs, l);
+ }
+ catch (const odb::recoverable& e)
+ {
+ if (retry_-- > 0)
+ {
+ MODULE_DIAG;
+ l1 ([&]{trace << e.what () << "; " << retry_ + 1 << " retries left";});
+ throw retry ();
+ }
+
+ throw;
+ }
+}
diff --git a/mod/database.cxx b/mod/database.cxx
new file mode 100644
index 0000000..0f0b703
--- /dev/null
+++ b/mod/database.cxx
@@ -0,0 +1,60 @@
+// file : mod/database.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <mod/database>
+
+#include <map>
+
+#include <odb/pgsql/database.hxx>
+#include <odb/pgsql/connection-factory.hxx>
+
+namespace brep
+{
+ namespace options
+ {
+ bool
+ operator< (const db& x, const db& y)
+ {
+ int r;
+ if ((r = x.db_user ().compare (y.db_user ())) != 0 ||
+ (r = x.db_password ().compare (y.db_password ())) != 0 ||
+ (r = x.db_name ().compare (y.db_name ())) != 0 ||
+ (r = x.db_host ().compare (y.db_host ())))
+ return r < 0;
+
+ return x.db_port () < y.db_port ();
+ }
+ }
+
+ using namespace odb;
+
+ shared_ptr<database>
+ shared_database (const options::db& o)
+ {
+ static std::map<options::db, weak_ptr<database>> databases;
+
+ auto i (databases.find (o));
+ if (i != databases.end ())
+ {
+ if (shared_ptr<database> d = i->second.lock ())
+ return d;
+ }
+
+ unique_ptr<pgsql::connection_factory>
+ f (new pgsql::connection_pool_factory (o.db_max_connections ()));
+
+ shared_ptr<database> d (
+ make_shared<pgsql::database> (
+ o.db_user (),
+ o.db_password (),
+ o.db_name (),
+ o.db_host (),
+ o.db_port (),
+ "options='-c default_transaction_isolation=serializable'",
+ move (f)));
+
+ databases[o] = d;
+ return d;
+ }
+}
diff --git a/mod/diagnostics b/mod/diagnostics
new file mode 100644
index 0000000..38286d4
--- /dev/null
+++ b/mod/diagnostics
@@ -0,0 +1,306 @@
+// file : mod/diagnostics -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef MOD_DIAGNOSTICS
+#define MOD_DIAGNOSTICS
+
+#include <sstream>
+
+#include <brep/types>
+#include <brep/utility>
+
+namespace brep
+{
+ struct location
+ {
+ location (): line (0), column (0) {}
+ location (string f, uint64_t l, uint64_t c)
+ : file (move (f)), line (l), column (c) {}
+
+ string file;
+ uint64_t line;
+ uint64_t column;
+ };
+
+ enum class severity {error, warning, info, trace};
+
+ struct diag_entry
+ {
+ severity sev;
+ const char* name {nullptr}; // E.g., a function name in tracing.
+ location loc;
+ string msg;
+ };
+
+ using diag_data = vector<diag_entry>;
+
+ //
+ //
+ template <typename> struct diag_prologue;
+ template <typename> struct diag_mark;
+
+ using diag_epilogue = function<void (diag_data&&)>;
+
+ struct diag_record
+ {
+ template <typename T>
+ friend const diag_record&
+ operator<< (const diag_record& r, const T& x)
+ {
+ r.os_ << x;
+ return r;
+ }
+
+ diag_record () = default;
+
+ template <typename B>
+ explicit
+ diag_record (const diag_prologue<B>& p) {*this << p;} // See below.
+
+ template <typename B>
+ explicit
+ diag_record (const diag_mark<B>& m) {*this << m;} // See below.
+
+ ~diag_record () noexcept(false);
+
+ void
+ append (const diag_epilogue& e) const
+ {
+ if (epilogue_ == nullptr) // Keep the first epilogue (think 'fail').
+ epilogue_ = &e;
+
+ if (!data_.empty ())
+ {
+ data_.back ().msg = os_.str ();
+
+ // Reset the stream. There got to be a more efficient way to do it.
+ //
+ os_.clear ();
+ os_.str ("");
+ }
+
+ data_.push_back (diag_entry ());
+ }
+
+ diag_entry&
+ current () const {return data_.back ();}
+
+ // Move constructible-only type.
+ //
+ /*
+ @@ libstdc++ doesn't yet have the ostringstream move support.
+
+ diag_record (diag_record&& r)
+ : data_ (move (r.data_)), os_ (move (r.os_))
+ {
+ epilogue_ = r.epilogue_;
+ r.data_.clear (); // Empty.
+ }
+ */
+
+ diag_record (diag_record&& r): data_ (move (r.data_))
+ {
+ if (!data_.empty ())
+ os_ << r.os_.str ();
+
+ epilogue_ = r.epilogue_;
+ r.data_.clear (); // Empty.
+ }
+
+ diag_record& operator= (diag_record&&) = delete;
+
+ diag_record (const diag_record&) = delete;
+ diag_record& operator= (const diag_record&) = delete;
+
+ private:
+ mutable diag_data data_;
+ mutable std::ostringstream os_;
+ mutable const diag_epilogue* epilogue_ {nullptr};
+ };
+
+ // Base (B) should provide operator() that configures diag_record.
+ //
+ template <typename B>
+ struct diag_prologue: B
+ {
+ diag_prologue (const diag_epilogue& e): B (), epilogue_ (e) {}
+
+ template <typename... A>
+ diag_prologue (const diag_epilogue& e, A&&... a)
+ : B (forward<A> (a)...), epilogue_ (e) {}
+
+ template <typename T>
+ diag_record
+ operator<< (const T& x) const
+ {
+ diag_record r;
+ r.append (epilogue_);
+ B::operator() (r);
+ r << x;
+ return r;
+ }
+
+ friend const diag_record&
+ operator<< (const diag_record& r, const diag_prologue& p)
+ {
+ r.append (p.epilogue_);
+ p (r);
+ return r;
+ }
+
+ private:
+ const diag_epilogue& epilogue_;
+ };
+
+ // Base (B) should provide operator() that returns diag_prologue.
+ //
+ template <typename B>
+ struct diag_mark: B
+ {
+ diag_mark (): B () {}
+
+ template <typename... A>
+ diag_mark (A&&... a): B (forward<A> (a)...) {}
+
+ template <typename T>
+ diag_record
+ operator<< (const T& x) const
+ {
+ return B::operator() () << x;
+ }
+
+ friend const diag_record&
+ operator<< (const diag_record& r, const diag_mark& m)
+ {
+ return r << m ();
+ }
+ };
+
+ // Prologues.
+ //
+ struct simple_prologue_base
+ {
+ explicit
+ simple_prologue_base (severity s, const char* name)
+ : sev_ (s), name_ (name) {}
+
+ void
+ operator() (const diag_record& r) const
+ {
+ diag_entry& e (r.current ());
+ e.sev = sev_;
+ e.name = name_;
+ }
+
+ private:
+ severity sev_;
+ const char* name_;
+ };
+ typedef diag_prologue<simple_prologue_base> simple_prologue;
+
+ struct location_prologue_base
+ {
+ location_prologue_base (severity s,
+ const char* name,
+ const location& l)
+ : sev_ (s), name_ (name), loc_ (l) {}
+
+ void
+ operator() (const diag_record& r) const
+ {
+ diag_entry& e (r.current ());
+ e.sev = sev_;
+ e.name = name_;
+ e.loc = loc_; //@@ I think we can probably move it.
+ }
+
+ private:
+ severity sev_;
+ const char* name_;
+ const location loc_;
+ };
+ typedef diag_prologue<location_prologue_base> location_prologue;
+
+ // Marks.
+ //
+ struct basic_mark_base
+ {
+ explicit
+ basic_mark_base (severity s,
+ const diag_epilogue& e,
+ const char* name = nullptr,
+ const void* data = nullptr)
+ : sev_ (s), epilogue_ (e), name_ (name), data_ (data) {}
+
+ simple_prologue
+ operator() () const
+ {
+ return simple_prologue (epilogue_, sev_, name_);
+ }
+
+ location_prologue
+ operator() (const location& l) const
+ {
+ return location_prologue (epilogue_, sev_, name_, l);
+ }
+
+ template <typename L>
+ location_prologue
+ operator() (const L& l) const
+ {
+ // get_location() is the user-supplied ADL-searched function.
+ //
+ return location_prologue (
+ epilogue_, sev_, name_, get_location (l, data_));
+ }
+
+ private:
+ severity sev_;
+ const diag_epilogue& epilogue_;
+ const char* name_;
+ const void* data_;
+ };
+ typedef diag_mark<basic_mark_base> basic_mark;
+
+ template <typename E>
+ struct fail_mark_base
+ {
+ explicit
+ fail_mark_base (const char* name = nullptr, const void* data = nullptr)
+ : name_ (name), data_ (data) {}
+
+ simple_prologue
+ operator() () const
+ {
+ return simple_prologue (epilogue_, severity::error, name_);
+ }
+
+ location_prologue
+ operator() (const location& l) const
+ {
+ return location_prologue (epilogue_, severity::error, name_, l);
+ }
+
+ template <typename L>
+ location_prologue
+ operator() (const L& l) const
+ {
+ return location_prologue (
+ epilogue_, severity::error, name_, get_location (l, data_));
+ }
+
+ static void
+ epilogue (diag_data&& d) {throw E (move (d));}
+
+ private:
+ const diag_epilogue epilogue_ {&epilogue};
+ const char* name_;
+ const void* data_;
+ };
+
+ template <typename E>
+ using fail_mark = diag_mark<fail_mark_base<E>>;
+}
+
+#endif // MOD_DIAGNOSTICS
diff --git a/mod/diagnostics.cxx b/mod/diagnostics.cxx
new file mode 100644
index 0000000..6512517
--- /dev/null
+++ b/mod/diagnostics.cxx
@@ -0,0 +1,30 @@
+// file : mod/diagnostics.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <mod/diagnostics>
+
+using namespace std;
+
+namespace brep
+{
+ diag_record::
+ ~diag_record () noexcept(false)
+ {
+ // Don't flush the record if this destructor was called as part of
+ // the stack unwinding. Right now this means we cannot use this
+ // mechanism in destructors, which is not a big deal, except for
+ // one place: exception_guard. So for now we are going to have
+ // this ugly special check which we will be able to get rid of
+ // once C++17 uncaught_exceptions() becomes available.
+ //
+ if (!data_.empty () &&
+ (!uncaught_exception () /*|| exception_unwinding_dtor*/))
+ {
+ data_.back ().msg = os_.str (); // Save last message.
+
+ assert (epilogue_ != nullptr);
+ (*epilogue_) (move (data_)); // Can throw.
+ }
+ }
+}
diff --git a/mod/mod-package-details b/mod/mod-package-details
new file mode 100644
index 0000000..b324bfb
--- /dev/null
+++ b/mod/mod-package-details
@@ -0,0 +1,42 @@
+// file : mod/mod-package-details -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef MOD_MOD_PACKAGE_DETAILS
+#define MOD_MOD_PACKAGE_DETAILS
+
+#include <brep/types>
+#include <brep/utility>
+
+#include <mod/options>
+#include <mod/database-module>
+
+namespace brep
+{
+ class package_details: public database_module
+ {
+ public:
+ package_details () = default;
+
+ // Create a shallow copy (handling instance) if initialized and a deep
+ // copy (context exemplar) otherwise.
+ //
+ explicit
+ package_details (const package_details&);
+
+ virtual bool
+ handle (request&, response&);
+
+ virtual const cli::options&
+ cli_options () const {return options::package_details::description ();}
+
+ private:
+ virtual void
+ init (cli::scanner&);
+
+ private:
+ shared_ptr<options::package_details> options_;
+ };
+}
+
+#endif // MOD_MOD_PACKAGE_DETAILS
diff --git a/mod/mod-package-details.cxx b/mod/mod-package-details.cxx
new file mode 100644
index 0000000..2ff93c9
--- /dev/null
+++ b/mod/mod-package-details.cxx
@@ -0,0 +1,258 @@
+// file : mod/mod-package-details.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <mod/mod-package-details>
+
+#include <xml/serializer>
+
+#include <odb/session.hxx>
+#include <odb/database.hxx>
+#include <odb/transaction.hxx>
+
+#include <web/xhtml>
+#include <web/module>
+#include <web/xhtml-fragment>
+#include <web/mime-url-encoding>
+
+#include <brep/package>
+#include <brep/package-odb>
+
+#include <mod/page>
+#include <mod/options>
+
+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::package_details::
+package_details (const package_details& r)
+ : database_module (r),
+ options_ (r.initialized_ ? r.options_ : nullptr)
+{
+}
+
+void brep::package_details::
+init (scanner& s)
+{
+ MODULE_DIAG;
+
+ options_ = make_shared<options::package_details> (
+ s, unknown_mode::fail, unknown_mode::fail);
+
+ database_module::init (*options_);
+
+ if (options_->root ().empty ())
+ options_->root (dir_path ("/"));
+}
+
+template <typename T>
+static inline query<T>
+search_params (const brep::string& n, const brep::string& q)
+{
+ using query = query<T>;
+
+ return "(" +
+ (q.empty ()
+ ? query ("NULL")
+ : "plainto_tsquery (" + query::_val (q) + ")") +
+ "," +
+ query::_val (n) +
+ ")";
+}
+
+bool brep::package_details::
+handle (request& rq, response& rs)
+{
+ using namespace web;
+ using namespace web::xhtml;
+
+ MODULE_DIAG;
+
+ const size_t res_page (options_->search_results ());
+ const dir_path& root (options_->root ());
+
+ const string& name (*rq.path ().rbegin ());
+ const string ename (mime_url_encode (name));
+
+ params::package_details params;
+ bool full;
+
+ try
+ {
+ name_value_scanner s (rq.parameters ());
+ params = params::package_details (
+ s, unknown_mode::fail, unknown_mode::fail);
+
+ full = params.form () == page_form::full;
+ }
+ catch (const cli::exception& e)
+ {
+ throw invalid_request (400, e.what ());
+ }
+
+ size_t page (params.page ());
+ const string& squery (params.query ());
+
+ auto url (
+ [&ename](bool f = false,
+ const string& q = "",
+ size_t p = 0,
+ const string& a = "") -> string
+ {
+ string s ("?");
+ string u (ename);
+
+ if (f) { u += "?f=full"; s = "&"; }
+ if (!q.empty ()) { u += s + "q=" + mime_url_encode (q); s = "&"; }
+ if (p > 0) { u += s + "p=" + to_string (p); s = "&"; }
+ if (!a.empty ()) { u += '#' + a; }
+ return u;
+ });
+
+ xml::serializer s (rs.content (), name);
+
+ s << HTML
+ << HEAD
+ << TITLE
+ << name;
+
+ if (!squery.empty ())
+ s << " " << squery;
+
+ s << ~TITLE
+ << CSS_LINKS (path ("package-details.css"), root)
+ //
+ // This hack is required to avoid the "flash of unstyled content", which
+ // happens due to the presence of the autofocus attribute in the input
+ // element of the search form. The problem appears in Firefox and has a
+ // (4-year old, at the time of this writing) bug report:
+ //
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=712130.
+ //
+ << SCRIPT << " " << ~SCRIPT
+ << ~HEAD
+ << BODY
+ << DIV_HEADER (root, options_->logo (), options_->menu ())
+ << DIV(ID="content");
+
+ if (full)
+ s << CLASS("full");
+
+ s << DIV(ID="heading")
+ << H1 << A(HREF=url ()) << name << ~A << ~H1
+ << A(HREF=url (!full, squery, page))
+ << (full ? "[brief]" : "[full]")
+ << ~A
+ << ~DIV;
+
+ session sn;
+ transaction t (db_->begin ());
+
+ shared_ptr<package> pkg;
+ {
+ latest_package lp;
+ if (!db_->query_one<latest_package> (
+ query<latest_package>(
+ "(" + query<latest_package>::_val (name) + ")"), lp))
+ throw invalid_request (404, "Package '" + name + "' not found");
+
+ pkg = db_->load<package> (lp.id);
+ }
+
+ const auto& licenses (pkg->license_alternatives);
+
+ if (page == 0)
+ {
+ // Display package details on the first page only.
+ //
+ s << H2 << pkg->summary << ~H2;
+
+ static const string id ("description");
+ if (const auto& d = pkg->description)
+ s << (full
+ ? P_DESCRIPTION (*d, id)
+ : P_DESCRIPTION (*d, options_->package_description (),
+ url (!full, squery, page, id)));
+
+ s << TABLE(CLASS="proplist", ID="package")
+ << TBODY
+ << TR_LICENSE (licenses)
+ << TR_URL (pkg->url)
+ << TR_EMAIL (pkg->email)
+ << TR_TAGS (pkg->tags, root)
+ << ~TBODY
+ << ~TABLE;
+ }
+
+ auto pkg_count (
+ db_->query_value<package_count> (
+ search_params<package_count> (name, squery)));
+
+ s << FORM_SEARCH (squery)
+ << DIV_COUNTER (pkg_count, "Version", "Versions");
+
+ // Enclose the subsequent tables to be able to use nth-child CSS selector.
+ //
+ s << DIV;
+ for (const auto& pr:
+ db_->query<package_search_rank> (
+ search_params<package_search_rank> (name, squery) +
+ "ORDER BY rank DESC, 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)))
+ {
+ shared_ptr<package> p (db_->load<package> (pr.id));
+
+ s << TABLE(CLASS="proplist version")
+ << TBODY
+ << TR_VERSION (name, p->version.string (), root)
+
+ // @@ Shouldn't we skip low priority row ? Don't think so, why?
+ //
+ << TR_PRIORITY (p->priority);
+
+ // Comparing objects of the license_alternatives class as being of the
+ // vector<vector<string>> class, so comments are not considered.
+ //
+ if (p->license_alternatives != licenses)
+ s << TR_LICENSE (p->license_alternatives);
+
+ assert (p->internal ());
+
+ // @@ Shouldn't we make package location to be a link to the proper
+ // place of the About page, describing corresponding repository?
+ // Yes, I think that's sounds reasonable, once we have about.
+ // Or maybe it can be something more valuable like a link to the
+ // repository package search page ?
+ //
+ // @@ In most cases package location will be the same for all versions
+ // of the same package. Shouldn't we put package location to the
+ // package summary part and display it here only if it differs
+ // from the one in the summary ?
+ //
+ // Hm, I am not so sure about this. Consider: stable/testing/unstable.
+ //
+ s << TR_LOCATION (p->internal_repository.object_id (), root)
+ << TR_DEPENDS (p->dependencies, root)
+ << TR_REQUIRES (p->requirements)
+ << ~TBODY
+ << ~TABLE;
+ }
+ s << ~DIV;
+
+ t.commit ();
+
+ s << DIV_PAGER (page, pkg_count, res_page, options_->search_pages (),
+ url (full, squery))
+ << ~DIV
+ << ~BODY
+ << ~HTML;
+
+ return true;
+}
diff --git a/mod/mod-package-search b/mod/mod-package-search
new file mode 100644
index 0000000..e463e2a
--- /dev/null
+++ b/mod/mod-package-search
@@ -0,0 +1,42 @@
+// file : mod/mod-package-search -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef MOD_MOD_PACKAGE_SEARCH
+#define MOD_MOD_PACKAGE_SEARCH
+
+#include <brep/types>
+#include <brep/utility>
+
+#include <mod/options>
+#include <mod/database-module>
+
+namespace brep
+{
+ class package_search: public database_module
+ {
+ public:
+ package_search () = default;
+
+ // Create a shallow copy (handling instance) if initialized and a deep
+ // copy (context exemplar) otherwise.
+ //
+ explicit
+ package_search (const package_search&);
+
+ virtual bool
+ handle (request&, response&);
+
+ virtual const cli::options&
+ cli_options () const {return options::package_search::description ();}
+
+ private:
+ virtual void
+ init (cli::scanner&);
+
+ private:
+ shared_ptr<options::package_search> options_;
+ };
+}
+
+#endif // MOD_MOD_PACKAGE_SEARCH
diff --git a/mod/mod-package-search.cxx b/mod/mod-package-search.cxx
new file mode 100644
index 0000000..5654427
--- /dev/null
+++ b/mod/mod-package-search.cxx
@@ -0,0 +1,181 @@
+// file : mod/mod-package-search.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <mod/mod-package-search>
+
+#include <xml/serializer>
+
+#include <odb/session.hxx>
+#include <odb/database.hxx>
+#include <odb/transaction.hxx>
+#include <odb/schema-catalog.hxx>
+
+#include <web/xhtml>
+#include <web/module>
+#include <web/xhtml-fragment>
+#include <web/mime-url-encoding>
+
+#include <brep/version>
+#include <brep/package>
+#include <brep/package-odb>
+
+#include <mod/page>
+#include <mod/options>
+
+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::package_search::
+package_search (const package_search& r)
+ : database_module (r),
+ options_ (r.initialized_ ? r.options_ : nullptr)
+{
+}
+
+void brep::package_search::
+init (scanner& s)
+{
+ MODULE_DIAG;
+
+ options_ = make_shared<options::package_search> (
+ s, unknown_mode::fail, unknown_mode::fail);
+
+ database_module::init (*options_);
+
+ if (options_->root ().empty ())
+ options_->root (dir_path ("/"));
+
+ // Check that the database schema matches the current one. It's enough to
+ // perform the check in just a single module implementation (and we don't
+ // do in the dispatcher because it doesn't use the database).
+ //
+ // Note that the failure can be reported by each web server worker process.
+ // While it could be tempting to move the check to the
+ // repository_root::version() function, it would be wrong. The function can
+ // be called by a different process (usually the web server root one) not
+ // having the proper permissions to access the database.
+ //
+ if (schema_catalog::current_version (*db_) != db_->schema_version ())
+ fail << "database schema differs from the current one (module "
+ << BREP_VERSION_STR << ")";
+}
+
+template <typename T>
+static inline query<T>
+search_param (const brep::string& q)
+{
+ using query = query<T>;
+ return "(" +
+ (q.empty ()
+ ? query ("NULL")
+ : "plainto_tsquery (" + query::_val (q) + ")") +
+ ")";
+}
+
+bool brep::package_search::
+handle (request& rq, response& rs)
+{
+ using namespace web::xhtml;
+
+ MODULE_DIAG;
+
+ const size_t res_page (options_->search_results ());
+ const dir_path& root (options_->root ());
+
+ params::package_search params;
+
+ try
+ {
+ name_value_scanner s (rq.parameters ());
+ params = params::package_search (
+ s, unknown_mode::fail, unknown_mode::fail);
+ }
+ catch (const unknown_argument& e)
+ {
+ throw invalid_request (400, e.what ());
+ }
+
+ size_t page (params.page ());
+ const string& squery (params.query ());
+ string squery_param (squery.empty ()
+ ? ""
+ : "?q=" + web::mime_url_encode (squery));
+
+
+ static const string title ("Packages");
+ xml::serializer s (rs.content (), title);
+
+ s << HTML
+ << HEAD
+ << TITLE
+ << title;
+
+ if (!squery.empty ())
+ s << " " << squery;
+
+ s << ~TITLE
+ << CSS_LINKS (path ("package-search.css"), root)
+ //
+ // This hack is required to avoid the "flash of unstyled content", which
+ // happens due to the presence of the autofocus attribute in the input
+ // element of the search form. The problem appears in Firefox and has a
+ // (4-year old, at the time of this writing) bug report:
+ //
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=712130.
+ //
+ << SCRIPT << " " << ~SCRIPT
+ << ~HEAD
+ << BODY
+ << DIV_HEADER (root, options_->logo (), options_->menu ())
+ << DIV(ID="content");
+
+ session sn;
+ transaction t (db_->begin ());
+
+ auto pkg_count (
+ db_->query_value<latest_package_count> (
+ search_param<latest_package_count> (squery)));
+
+ s << FORM_SEARCH (squery)
+ << DIV_COUNTER (pkg_count, "Package", "Packages");
+
+ // Enclose the subsequent tables to be able to use nth-child CSS selector.
+ //
+ s << DIV;
+ for (const auto& pr:
+ db_->query<latest_package_search_rank> (
+ search_param<latest_package_search_rank> (squery) +
+ "ORDER BY rank DESC, name" +
+ "OFFSET" + to_string (page * res_page) +
+ "LIMIT" + to_string (res_page)))
+ {
+ shared_ptr<package> p (db_->load<package> (pr.id));
+
+ s << TABLE(CLASS="proplist package")
+ << TBODY
+ << TR_NAME (p->id.name, squery_param, root)
+ << TR_SUMMARY (p->summary)
+ << TR_LICENSE (p->license_alternatives)
+ << TR_TAGS (p->tags, root)
+ << TR_DEPENDS (p->dependencies, root)
+ << TR_REQUIRES (p->requirements)
+ << ~TBODY
+ << ~TABLE;
+ }
+ s << ~DIV;
+
+ t.commit ();
+
+ s << DIV_PAGER (page, pkg_count, res_page, options_->search_pages (),
+ root.string () + squery_param)
+ << ~DIV
+ << ~BODY
+ << ~HTML;
+
+ return true;
+}
diff --git a/mod/mod-package-version-details b/mod/mod-package-version-details
new file mode 100644
index 0000000..0fba2bf
--- /dev/null
+++ b/mod/mod-package-version-details
@@ -0,0 +1,45 @@
+// file : mod/mod-package-version-details -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef MOD_MOD_PACKAGE_VERSION_DETAILS
+#define MOD_MOD_PACKAGE_VERSION_DETAILS
+
+#include <brep/types>
+#include <brep/utility>
+
+#include <mod/options>
+#include <mod/database-module>
+
+namespace brep
+{
+ class package_version_details: public database_module
+ {
+ public:
+ package_version_details () = default;
+
+ // Create a shallow copy (handling instance) if initialized and a deep
+ // copy (context exemplar) otherwise.
+ //
+ explicit
+ package_version_details (const package_version_details&);
+
+ virtual bool
+ handle (request&, response&);
+
+ virtual const cli::options&
+ cli_options () const
+ {
+ return options::package_version_details::description ();
+ }
+
+ private:
+ virtual void
+ init (cli::scanner&);
+
+ private:
+ shared_ptr<options::package_version_details> options_;
+ };
+}
+
+#endif // MOD_MOD_PACKAGE_VERSION_DETAILS
diff --git a/mod/mod-package-version-details.cxx b/mod/mod-package-version-details.cxx
new file mode 100644
index 0000000..149c8f9
--- /dev/null
+++ b/mod/mod-package-version-details.cxx
@@ -0,0 +1,320 @@
+// file : mod/mod-package-version-details.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <mod/mod-package-version-details>
+
+#include <xml/serializer>
+
+#include <odb/session.hxx>
+#include <odb/database.hxx>
+#include <odb/transaction.hxx>
+
+#include <web/xhtml>
+#include <web/module>
+#include <web/xhtml-fragment>
+#include <web/mime-url-encoding>
+
+#include <brep/package>
+#include <brep/package-odb>
+
+#include <mod/page>
+#include <mod/options>
+
+using namespace std;
+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::package_version_details::
+package_version_details (const package_version_details& r)
+ : database_module (r),
+ options_ (r.initialized_ ? r.options_ : nullptr)
+{
+}
+
+void brep::package_version_details::
+init (scanner& s)
+{
+ MODULE_DIAG;
+
+ options_ = make_shared<options::package_version_details> (
+ s, unknown_mode::fail, unknown_mode::fail);
+
+ database_module::init (*options_);
+
+ if (options_->root ().empty ())
+ options_->root (dir_path ("/"));
+}
+
+bool brep::package_version_details::
+handle (request& rq, response& rs)
+{
+ using namespace web;
+ using namespace web::xhtml;
+ using brep::version; // Not to confuse with module::version.
+
+ MODULE_DIAG;
+
+ const dir_path& root (options_->root ());
+
+ auto i (rq.path ().rbegin ());
+ version ver;
+
+ try
+ {
+ ver = version (*i++);
+ }
+ catch (const invalid_argument& )
+ {
+ throw invalid_request (400, "invalid package version format");
+ }
+
+ const string& sver (ver.string ());
+
+ assert (i != rq.path ().rend ());
+ const string& name (*i);
+
+ params::package_version_details params;
+ bool full;
+
+ try
+ {
+ name_value_scanner s (rq.parameters ());
+ params = params::package_version_details (
+ s, unknown_mode::fail, unknown_mode::fail);
+
+ full = params.form () == page_form::full;
+ }
+ catch (const unknown_argument& e)
+ {
+ throw invalid_request (400, e.what ());
+ }
+
+ auto url (
+ [&sver](bool f = false, const string& a = "") -> string
+ {
+ string u (sver);
+
+ if (f) { u += "?f=full"; }
+ if (!a.empty ()) { u += '#' + a; }
+ return u;
+ });
+
+ const string title (name + " " + sver);
+ xml::serializer s (rs.content (), title);
+
+ s << HTML
+ << HEAD
+ << TITLE << title << ~TITLE
+ << CSS_LINKS (path ("package-version-details.css"), root)
+ << ~HEAD
+ << BODY
+ << DIV_HEADER (root, options_->logo (), options_->menu ())
+ << DIV(ID="content");
+
+ if (full)
+ s << CLASS("full");
+
+ s << DIV(ID="heading")
+ << H1
+ << A(HREF=root / path (mime_url_encode (name))) << name << ~A
+ << "/"
+ << A(HREF=url ()) << sver << ~A
+ << ~H1
+ << A(HREF=url (!full)) << (full ? "[brief]" : "[full]") << ~A
+ << ~DIV;
+
+ bool not_found (false);
+ shared_ptr<package> pkg;
+
+ session sn;
+ transaction t (db_->begin ());
+
+ try
+ {
+ pkg = db_->load<package> (package_id (name, ver));
+
+ // If the requested package turned up to be an "external" one just
+ // respond that no "internal" package is present.
+ //
+ not_found = !pkg->internal ();
+ }
+ catch (const object_not_persistent& )
+ {
+ not_found = true;
+ }
+
+ if (not_found)
+ throw invalid_request (404, "Package '" + title + "' not found");
+
+ s << H2 << pkg->summary << ~H2;
+
+ static const string id ("description");
+ if (const auto& d = pkg->description)
+ s << (full
+ ? P_DESCRIPTION (*d, id)
+ : P_DESCRIPTION (*d, options_->package_description (),
+ url (!full, id)));
+
+ assert (pkg->location && pkg->sha256sum);
+
+ s << TABLE(CLASS="proplist", ID="version")
+ << TBODY
+
+ // Repeat version here since it can be cut out in the header.
+ //
+ << TR_VERSION (pkg->version.string ())
+
+ << TR_PRIORITY (pkg->priority)
+ << TR_LICENSES (pkg->license_alternatives)
+ << TR_LOCATION (pkg->internal_repository.object_id (), root)
+ << TR_DOWNLOAD (pkg->internal_repository.load ()->location.string () +
+ "/" + pkg->location->string ())
+ << TR_SHA256SUM (*pkg->sha256sum)
+ << ~TBODY
+ << ~TABLE
+
+ << TABLE(CLASS="proplist", ID="package")
+ << TBODY
+ << TR_URL (pkg->url)
+ << TR_EMAIL (pkg->email);
+
+ const auto& pu (pkg->package_url);
+ if (pu && *pu != pkg->url)
+ s << TR_URL (*pu, "pkg-url");
+
+ const auto& pe (pkg->package_email);
+ if (pe && *pe != pkg->email)
+ s << TR_EMAIL (*pe, "pkg-email");
+
+ s << TR_TAGS (pkg->tags, root)
+ << ~TBODY
+ << ~TABLE;
+
+ const auto& ds (pkg->dependencies);
+ if (!ds.empty ())
+ {
+ s << H3 << "Depends" << ~H3
+ << TABLE(CLASS="proplist", ID="depends")
+ << TBODY;
+
+ for (const auto& da: ds)
+ {
+ s << TR(CLASS="depends")
+ << TH;
+
+ if (da.conditional)
+ s << "?";
+
+ s << ~TH
+ << TD
+ << SPAN(CLASS="value");
+
+ for (const auto& d: da)
+ {
+ if (&d != &da[0])
+ s << " | ";
+
+ shared_ptr<package> p (d.package.load ());
+ assert (p->internal () || !p->other_repositories.empty ());
+
+ shared_ptr<repository> r (
+ p->internal ()
+ ? p->internal_repository.load ()
+ : p->other_repositories[0].load ());
+
+ const auto& dcon (d.constraint);
+ const string& dname (p->id.name);
+ string ename (mime_url_encode (dname));
+
+ if (r->url)
+ {
+ string u (*r->url + ename);
+ s << A(HREF=u) << dname << ~A;
+
+ if (dcon)
+ s << ' ' << A(HREF=u + "/" + p->version.string ()) << *dcon << ~A;
+ }
+ else if (p->internal ())
+ {
+ path u (root / path (ename));
+ s << A(HREF=u) << dname << ~A;
+
+ if (dcon)
+ s << ' ' << A(HREF=u / path (p->version.string ())) << *dcon << ~A;
+ }
+ else
+ // Display the dependency as a plain text if no repository URL
+ // available.
+ //
+ s << d;
+ }
+
+ s << ~SPAN
+ << SPAN_COMMENT (da.comment)
+ << ~TD
+ << ~TR;
+ }
+
+ s << ~TBODY
+ << ~TABLE;
+ }
+
+ t.commit ();
+
+ const auto& rm (pkg->requirements);
+ if (!rm.empty ())
+ {
+ s << H3 << "Requires" << ~H3
+ << TABLE(CLASS="proplist", ID="requires")
+ << TBODY;
+
+ for (const auto& ra: rm)
+ {
+ s << TR(CLASS="requires")
+ << TH;
+
+ if (ra.conditional)
+ s << "?";
+
+ s << ~TH
+ << TD
+ << SPAN(CLASS="value");
+
+ for (const auto& r: ra)
+ {
+ if (&r != &ra[0])
+ s << " | ";
+
+ s << r;
+ }
+
+ s << ~SPAN
+ << SPAN_COMMENT (ra.comment)
+ << ~TD
+ << ~TR;
+ }
+
+ s << ~TBODY
+ << ~TABLE;
+ }
+
+ const auto& ch (pkg->changes);
+ if (!ch.empty ())
+ s << H3 << "Changes" << ~H3
+ << (full
+ ? PRE_CHANGES (ch)
+ : PRE_CHANGES (ch,
+ options_->package_changes (),
+ url (!full, "changes")));
+
+ s << ~DIV
+ << ~BODY
+ << ~HTML;
+
+ return true;
+}
diff --git a/mod/mod-repository-details b/mod/mod-repository-details
new file mode 100644
index 0000000..2532613
--- /dev/null
+++ b/mod/mod-repository-details
@@ -0,0 +1,42 @@
+// file : mod/mod-repository-details -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef MOD_MOD_REPOSITORY_DETAILS
+#define MOD_MOD_REPOSITORY_DETAILS
+
+#include <brep/types>
+#include <brep/utility>
+
+#include <mod/options>
+#include <mod/database-module>
+
+namespace brep
+{
+ class repository_details: public database_module
+ {
+ public:
+ repository_details () = default;
+
+ // Create a shallow copy (handling instance) if initialized and a deep
+ // copy (context exemplar) otherwise.
+ //
+ explicit
+ repository_details (const repository_details&);
+
+ virtual bool
+ handle (request&, response&);
+
+ virtual const cli::options&
+ cli_options () const {return options::repository_details::description ();}
+
+ private:
+ virtual void
+ init (cli::scanner&);
+
+ private:
+ shared_ptr<options::repository_details> options_;
+ };
+}
+
+#endif // MOD_MOD_REPOSITORY_DETAILS
diff --git a/mod/mod-repository-details.cxx b/mod/mod-repository-details.cxx
new file mode 100644
index 0000000..91f0d1b
--- /dev/null
+++ b/mod/mod-repository-details.cxx
@@ -0,0 +1,147 @@
+// file : mod/mod-repository-details.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <mod/mod-repository-details>
+
+#include <time.h> // tzset()
+
+#include <sstream>
+#include <algorithm> // max()
+
+#include <xml/serializer>
+
+#include <odb/database.hxx>
+#include <odb/transaction.hxx>
+
+#include <butl/timestamp>
+
+#include <web/xhtml>
+#include <web/module>
+#include <web/xhtml-fragment>
+#include <web/mime-url-encoding>
+
+#include <brep/package>
+#include <brep/package-odb>
+
+#include <mod/page>
+#include <mod/options>
+
+using namespace std;
+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::repository_details::
+repository_details (const repository_details& r)
+ : database_module (r),
+ options_ (r.initialized_ ? r.options_ : nullptr)
+{
+}
+
+void brep::repository_details::
+init (scanner& s)
+{
+ MODULE_DIAG;
+
+ options_ = make_shared<options::repository_details> (
+ s, unknown_mode::fail, unknown_mode::fail);
+
+ database_module::init (*options_);
+
+ if (options_->root ().empty ())
+ options_->root (dir_path ("/"));
+
+ tzset (); // To use butl::to_stream() later on.
+}
+
+bool brep::repository_details::
+handle (request& rq, response& rs)
+{
+ using namespace web::xhtml;
+
+ MODULE_DIAG;
+
+ const dir_path& root (options_->root ());
+
+ // Make sure no parameters passed.
+ //
+ try
+ {
+ name_value_scanner s (rq.parameters ());
+ params::repository_details (s, unknown_mode::fail, unknown_mode::fail);
+ }
+ catch (const unknown_argument& e)
+ {
+ throw invalid_request (400, e.what ());
+ }
+
+ static const string title ("About");
+ xml::serializer s (rs.content (), title);
+
+ s << HTML
+ << HEAD
+ << TITLE << title << ~TITLE
+ << CSS_LINKS (path ("repository-details.css"), root)
+ << ~HEAD
+ << BODY
+ << DIV_HEADER (root, options_->logo (), options_->menu ())
+ << DIV(ID="content");
+
+ transaction t (db_->begin ());
+
+ using query = query<repository>;
+
+ for (const auto& r:
+ db_->query<repository> (
+ query::internal + "ORDER BY" + query::priority))
+ {
+ //@@ Feels like a lot of trouble (e.g., id_attribute()) for very
+ // dubious value. A link to the package search page just for
+ // this repository would probably be more useful.
+ //
+ string id (html_id (r.name));
+ s << H1(ID=id)
+ << A(HREF="#" + web::mime_url_encode (id)) << r.display_name << ~A
+ << ~H1;
+
+ if (r.summary)
+ s << H2 << *r.summary << ~H2;
+
+ if (r.description)
+ s << P_DESCRIPTION (*r.description);
+
+ if (r.email)
+ {
+ const email& e (*r.email);
+
+ s << P
+ << A(HREF="mailto:" + e) << e << ~A;
+
+ if (!e.comment.empty ())
+ s << " (" << e.comment << ")";
+
+ s << ~P;
+ }
+
+ ostringstream o;
+ butl::to_stream (o,
+ max (r.packages_timestamp, r.repositories_timestamp),
+ "%Y-%m-%d %H:%M:%S%[.N] %Z",
+ true,
+ true);
+
+ s << P << o.str () << ~P;
+ }
+
+ t.commit ();
+
+ s << ~DIV
+ << ~BODY
+ << ~HTML;
+
+ return true;
+}
diff --git a/mod/mod-repository-root b/mod/mod-repository-root
new file mode 100644
index 0000000..db13b58
--- /dev/null
+++ b/mod/mod-repository-root
@@ -0,0 +1,60 @@
+// file : mod/mod-repository-root -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef MOD_MOD_REPOSITORY_ROOT
+#define MOD_MOD_REPOSITORY_ROOT
+
+#include <brep/types>
+#include <brep/utility>
+
+#include <mod/module>
+#include <mod/options>
+
+namespace brep
+{
+ class package_search;
+ class package_details;
+ class package_version_details;
+ class repository_details;
+
+ class repository_root: public module
+ {
+ public:
+ repository_root ();
+
+ // Create a shallow copy (handling instance) if initialized and a deep
+ // copy (context exemplar) otherwise.
+ //
+ explicit
+ repository_root (const repository_root&);
+
+ private:
+ virtual bool
+ handle (request&, response&);
+
+ virtual const cli::options&
+ cli_options () const {return options::repository_root::description ();}
+
+ virtual option_descriptions
+ options ();
+
+ virtual void
+ init (const name_values&);
+
+ virtual void
+ init (cli::scanner&);
+
+ virtual void
+ version ();
+
+ private:
+ shared_ptr<package_search> package_search_;
+ shared_ptr<package_details> package_details_;
+ shared_ptr<package_version_details> package_version_details_;
+ shared_ptr<repository_details> repository_details_;
+ shared_ptr<options::repository_root> options_;
+ };
+}
+
+#endif // MOD_MOD_REPOSITORY_ROOT
diff --git a/mod/mod-repository-root.cxx b/mod/mod-repository-root.cxx
new file mode 100644
index 0000000..295117e
--- /dev/null
+++ b/mod/mod-repository-root.cxx
@@ -0,0 +1,263 @@
+// file : mod/mod-repository-root.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <mod/mod-repository-root>
+
+#include <sstream>
+
+#include <web/module>
+
+#include <brep/version>
+
+#include <mod/module>
+#include <mod/options>
+#include <mod/mod-package-search>
+#include <mod/mod-package-details>
+#include <mod/mod-repository-details>
+#include <mod/mod-package-version-details>
+
+using namespace std;
+using namespace brep::cli;
+
+namespace brep
+{
+ // request_proxy
+ //
+ class request_proxy: public request
+ {
+ public:
+ request_proxy (request& r, const name_values& p)
+ : request_ (r), parameters_ (p) {}
+
+ virtual const path_type&
+ path () {return request_.path ();}
+
+ virtual const name_values&
+ parameters () {return parameters_;}
+
+ virtual const name_values&
+ cookies () {return request_.cookies ();}
+
+ virtual istream&
+ content (bool buffer) {return request_.content (buffer);}
+
+ private:
+ request& request_;
+ const name_values& parameters_;
+ };
+
+ // repository_root
+ //
+ repository_root::
+ repository_root ()
+ : package_search_ (make_shared<package_search> ()),
+ package_details_ (make_shared<package_details> ()),
+ package_version_details_ (make_shared<package_version_details> ()),
+ repository_details_ (make_shared<repository_details> ())
+ {
+ }
+
+ repository_root::
+ repository_root (const repository_root& r)
+ : module (r),
+ //
+ // Deep/shallow-copy sub-modules depending on whether this is an
+ // exemplar/handler.
+ //
+ package_search_ (
+ r.initialized_
+ ? r.package_search_
+ : make_shared<package_search> (*r.package_search_)),
+ package_details_ (
+ r.initialized_
+ ? r.package_details_
+ : make_shared<package_details> (*r.package_details_)),
+ package_version_details_ (
+ r.initialized_
+ ? r.package_version_details_
+ : make_shared<package_version_details> (
+ *r.package_version_details_)),
+ repository_details_ (
+ r.initialized_
+ ? r.repository_details_
+ : make_shared<repository_details> (*r.repository_details_)),
+ options_ (
+ r.initialized_
+ ? r.options_
+ : nullptr)
+ {
+ }
+
+ // Return amalgamation of repository_root and all its sub-modules option
+ // descriptions.
+ //
+ option_descriptions repository_root::
+ options ()
+ {
+ option_descriptions r (module::options ());
+ append (r, package_search_->options ());
+ append (r, package_details_->options ());
+ append (r, package_version_details_->options ());
+ append (r, repository_details_->options ());
+ return r;
+ }
+
+ // Initialize sub-modules and parse own configuration options.
+ //
+ void repository_root::
+ init (const name_values& v)
+ {
+ auto sub_init ([this, &v](module& m)
+ {
+ m.init (filter (v, m.options ()), *log_);
+ });
+
+ // Initialize sub-modules.
+ //
+ sub_init (*package_search_);
+ sub_init (*package_details_);
+ sub_init (*package_version_details_);
+ sub_init (*repository_details_);
+
+ // Parse own configuration options.
+ //
+ module::init (
+ filter (v, convert (options::repository_root::description ())));
+ }
+
+ void repository_root::
+ init (scanner& s)
+ {
+ MODULE_DIAG;
+
+ options_ = make_shared<options::repository_root> (
+ s, unknown_mode::fail, unknown_mode::fail);
+
+ if (options_->root ().empty ())
+ options_->root (dir_path ("/"));
+ }
+
+ bool repository_root::
+ handle (request& rq, response& rs)
+ {
+ MODULE_DIAG;
+
+ const dir_path& root (options_->root ());
+
+ const path& rpath (rq.path ());
+ if (!rpath.sub (root))
+ return false;
+
+ const path& lpath (rpath.leaf (root));
+
+ // Delegate the request handling to the sub-module. Intercept exception
+ // handling to add sub-module attribution.
+ //
+ auto handle = [&rs, this](module& m, request& rq, const char* name) -> bool
+ {
+ try
+ {
+ return m.handle (rq, rs, *log_);
+ }
+ catch (const invalid_request&)
+ {
+ // Preserve invalid_request exception type, so the web server can
+ // properly respond to the client with a 4XX error code.
+ //
+ throw;
+ }
+ catch (const std::exception& e)
+ {
+ // All exception types inherited from std::exception (and different
+ // from invalid_request) are handled by the web server as
+ // std::exception. The only sensible way to handle them is to respond
+ // to the client with the internal server error (500) code. By that
+ // reason it is valid to reduce all these types to a single one.
+ // Note that the server_error exception is handled internally by the
+ // module::handle() function call.
+ //
+ throw runtime_error (string (name) + ": " + e.what ());
+ }
+ };
+
+ if (lpath.empty ())
+ {
+ // Dispatch request handling to the repository_details or the
+ // package_search module depending on the function name passed as a
+ // first HTTP request parameter. The parameter should have no value
+ // specified. Example: cppget.org/?about
+ //
+ const name_values& params (rq.parameters ());
+ if (!params.empty () && !params.front ().value)
+ {
+ if (params.front ().name == "about")
+ {
+ // Cleanup not to confuse the selected module with the unknown
+ // parameter.
+ //
+ name_values p (params);
+ p.erase (p.begin ());
+
+ request_proxy rp (rq, p);
+ repository_details m (*repository_details_);
+ return handle (m, rp, "repository_details");
+ }
+
+ throw invalid_request (400, "unknown function");
+ }
+ else
+ {
+ package_search m (*package_search_);
+ return handle (m, rq, "package_search");
+ }
+ }
+ else
+ {
+ // Dispatch request handling to the package_details or the
+ // package_version_details module depending on the HTTP request URL path.
+ //
+ auto i (lpath.begin ());
+ assert (i != lpath.end ());
+
+ const string& n (*i++); // Package name.
+
+ // Check if this is a package name and not a brep static content files
+ // (CSS) directory name, a repository directory name, or a special file
+ // name (the one starting with '.').
+ //
+ // @@ Shouldn't we validate that the package name is not "@", is not
+ // digit-only, does not start with '.' while parsing and serializing
+ // the package manifest ? Probably also need to mention these
+ // contraints in the manifest.txt file.
+ //
+ if (n != "@" && n.find_first_not_of ("0123456789") != string::npos &&
+ n[0] != '.')
+ {
+ if (i == lpath.end ())
+ {
+ package_details m (*package_details_);
+ return handle (m, rq, "package_details");
+ }
+ else if (++i == lpath.end ())
+ {
+ package_version_details m (*package_version_details_);
+ return handle (m, rq, "package_version_details");
+ }
+ }
+ }
+
+ return false;
+ }
+
+ void repository_root::
+ version ()
+ {
+ MODULE_DIAG;
+
+ info << "module " << BREP_VERSION_STR
+ << ", libbrep " << LIBBREP_VERSION_STR
+ << ", libbpkg " << LIBBPKG_VERSION_STR
+ << ", libbutl " << LIBBUTL_VERSION_STR;
+ }
+}
diff --git a/mod/module b/mod/module
new file mode 100644
index 0000000..b704a53
--- /dev/null
+++ b/mod/module
@@ -0,0 +1,201 @@
+// file : mod/module -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef MOD_MODULE
+#define MOD_MODULE
+
+#include <web/module>
+
+#include <brep/types>
+#include <brep/utility>
+
+#include <mod/options>
+#include <mod/diagnostics>
+
+namespace brep
+{
+ // Bring in commonly used names from the web namespace.
+ //
+ // @@ Maybe doing using namespace is the right way to handle this.
+ // There will, however, most likely be a conflict between
+ // web::module and our module. Or maybe not, need to try.
+ //
+ using web::status_code;
+ using web::invalid_request;
+ using web::sequence_error;
+ using web::option_descriptions;
+ using web::name_value;
+ using web::name_values;
+ using web::request;
+ using web::response;
+ using web::log;
+
+ // This exception indicated a server error (5XX). In particular,
+ // it is thrown by the fail diagnostics stream and is caught by the
+ // module implementation where it is both logged as an error and
+ // returned to the user with the 5XX status code.
+ //
+ struct server_error
+ {
+ diag_data data;
+
+ server_error (diag_data&& d): data (move (d)) {}
+ };
+
+ // Every module member function that needs to produce any diagnostics
+ // shall begin with:
+ //
+ // MODULE_DIAG;
+ //
+ // This will instantiate the fail, error, warn, info, and trace
+ // diagnostics streams with the function's name.
+ //
+#define MODULE_DIAG \
+ const fail_mark<server_error> fail (__PRETTY_FUNCTION__); \
+ const basic_mark error (severity::error, \
+ this->log_writer_, \
+ __PRETTY_FUNCTION__); \
+ const basic_mark warn (severity::warning, \
+ this->log_writer_, \
+ __PRETTY_FUNCTION__); \
+ const basic_mark info (severity::info, \
+ this->log_writer_, \
+ __PRETTY_FUNCTION__); \
+ const basic_mark trace (severity::trace, \
+ this->log_writer_, \
+ __PRETTY_FUNCTION__)
+
+ // Adaptation of the web::module to our needs.
+ //
+ class module: public web::module
+ {
+ // Diagnostics.
+ //
+ protected:
+ // Trace verbosity level.
+ //
+ // 0 - tracing disabled.
+ // 1 - brief information regarding irregular situations, which not being
+ // an error can be of some interest.
+ // 2 - @@ TODO: document
+ //
+ // While uint8 is more than enough, use uint16 for the ease of printing.
+ //
+ uint16_t verb_ = 0;
+
+ template <class F> void l1 (const F& f) const {if (verb_ >= 1) f ();}
+ template <class F> void l2 (const F& f) const {if (verb_ >= 2) f ();}
+
+ // Set to true when the module is successfully initialized.
+ //
+ bool initialized_ {false};
+
+ // Implementation details.
+ //
+ protected:
+ module ();
+ module (const module& );
+
+ static name_values
+ filter (const name_values&, const option_descriptions&);
+
+ static option_descriptions
+ convert (const cli::options&);
+
+ static void
+ append (option_descriptions& dst, const cli::options& src);
+
+ static void
+ append (option_descriptions& dst, const option_descriptions& src);
+
+ // Can be used by module implementation to parse HTTP request parameters.
+ //
+ class name_value_scanner: public cli::scanner
+ {
+ public:
+ name_value_scanner (const name_values&) noexcept;
+
+ virtual bool
+ more ();
+
+ virtual const char*
+ peek ();
+
+ virtual const char*
+ next ();
+
+ virtual void
+ skip ();
+
+ private:
+ const name_values& name_values_;
+ name_values::const_iterator i_;
+ bool name_;
+ };
+
+ public:
+ virtual const cli::options&
+ cli_options () const = 0;
+
+ virtual void
+ init (cli::scanner&) = 0;
+
+ // Can be overriden by custom request dispatcher to initialize
+ // sub-modules.
+ //
+ virtual void
+ init (const name_values&);
+
+ virtual void
+ init (const name_values&, log&);
+
+ virtual bool
+ handle (request&, response&) = 0;
+
+ virtual bool
+ handle (request&, response&, log&);
+
+ // web::module interface.
+ //
+ public:
+ // Custom request dispatcher can aggregate its own option descriptions
+ // with sub-modules option descriptions. In this case it should still call
+ // the base implementation in order to include the brep::module's options.
+ //
+ virtual option_descriptions
+ options ();
+
+ private:
+ virtual void
+ version (log&);
+
+ // Can be overriden by the module implementation to log version, etc.
+ //
+ virtual void
+ version () {}
+
+ name_values
+ expand_options (const name_values&);
+
+ // Diagnostics implementation details.
+ //
+ protected:
+ log* log_ {nullptr}; // Diagnostics backend provided by the web server.
+
+ private:
+ // Extract function name from a __PRETTY_FUNCTION__.
+ // Throw invalid_argument if fail to parse.
+ //
+ static string
+ func_name (const char* pretty_name);
+
+ void
+ log_write (const diag_data&) const;
+
+ protected:
+ const diag_epilogue log_writer_;
+ };
+}
+
+#endif // MOD_MODULE
diff --git a/mod/module.cxx b/mod/module.cxx
new file mode 100644
index 0000000..5e3a4b1
--- /dev/null
+++ b/mod/module.cxx
@@ -0,0 +1,410 @@
+// file : mod/module.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <mod/module>
+
+#include <httpd.h>
+#include <http_log.h>
+
+#include <sstream>
+#include <cstring> // strchr()
+#include <functional> // bind()
+
+#include <web/module>
+#include <web/apache/log>
+
+#include <mod/options>
+
+using namespace std;
+using namespace placeholders; // For std::bind's _1, etc.
+
+namespace brep
+{
+ // module
+ //
+ bool module::
+ handle (request& rq, response& rs, log& l)
+ {
+ log_ = &l;
+
+ try
+ {
+ // Web server should terminate if initialization failed.
+ //
+ assert (initialized_);
+
+ return handle (rq, rs);
+ }
+ catch (const server_error& e)
+ {
+ log_write (e.data);
+
+ try
+ {
+ static const char* sev_str[] = {"error", "warning", "info", "trace"};
+ ostream& o (rs.content (500, "text/plain;charset=utf-8"));
+
+ for (const auto& d: e.data)
+ {
+ string name;
+
+ try
+ {
+ name = func_name (d.name);
+ }
+ catch (const invalid_argument&)
+ {
+ // Log "pretty" function description, see in log file & fix.
+ name = d.name;
+ }
+
+ o << name << ": " << sev_str[static_cast<size_t> (d.sev)] << ": "
+ << d.msg << endl;
+ }
+ }
+ catch (const sequence_error&)
+ {
+ // We tried to return the error status/description but some
+ // content has already been written. Nothing we can do about
+ // it.
+ }
+ }
+
+ return true;
+ }
+
+ option_descriptions module::
+ convert (const cli::options& o)
+ {
+ option_descriptions r;
+ append (r, o);
+ return r;
+ }
+
+ void module::
+ append (option_descriptions& dst, const cli::options& src)
+ {
+ for (const auto& o: src)
+ {
+ bool v (!o.flag ());
+ auto i (dst.emplace (o.name (), v));
+ assert (i.first->second == v); // Consistent option/flag.
+
+ for (const auto& a: o.aliases ())
+ {
+ i = dst.emplace (a, v);
+ assert (i.first->second == v);
+ }
+ }
+ }
+
+ void module::
+ append (option_descriptions& dst, const option_descriptions& src)
+ {
+ for (const auto& o: src)
+ {
+ auto i (dst.emplace (o));
+ assert (i.first->second == o.second); // Consistent option/flag.
+ }
+ }
+
+ name_values module::
+ filter (const name_values& v, const option_descriptions& d)
+ {
+ name_values r;
+ for (const auto& nv: v)
+ {
+ if (d.find (nv.name) != d.end ())
+ r.push_back (nv);
+ }
+
+ return r;
+ }
+
+ // Convert CLI option descriptions to the general interface of option
+ // descriptions, extend with brep::module own option descriptions.
+ //
+ option_descriptions module::
+ options ()
+ {
+ option_descriptions r ({{"conf", true}});
+ append (r, options::module::description ());
+ append (r, cli_options ());
+ return r;
+ }
+
+ // Expand option list parsing configuration files.
+ //
+ name_values module::
+ expand_options (const name_values& v)
+ {
+ using namespace cli;
+
+ vector<const char*> argv;
+ for (const auto& nv: v)
+ {
+ argv.push_back (nv.name.c_str ());
+
+ if (nv.value)
+ argv.push_back (nv.value->c_str ());
+ }
+
+ int argc (argv.size ());
+ argv_file_scanner s (0, argc, const_cast<char**> (argv.data ()), "conf");
+
+ name_values r;
+ const option_descriptions& o (options ());
+
+ while (s.more ())
+ {
+ string n (s.next ());
+ auto i (o.find (n));
+
+ if (i == o.end ())
+ throw unknown_argument (n);
+
+ optional<string> v;
+ if (i->second)
+ v = s.next ();
+
+ r.emplace_back (move (n), move (v));
+ }
+
+ return r;
+ }
+
+ // Parse options with a cli-generated scanner. Options verb and conf are
+ // recognized by brep::module::init while others to be interpreted by the
+ // derived init(). If there is an option which can not be interpreted
+ // neither by brep::module nor by the derived class, then the web server
+ // is terminated with a corresponding error message being logged. Though
+ // this should not happen if the options() function returned the correct
+ // set of options.
+ //
+ void module::
+ init (const name_values& options, log& log)
+ {
+ assert (!initialized_);
+
+ log_ = &log;
+
+ try
+ {
+ name_values opts (expand_options (options));
+
+ // Read module implementation configuration.
+ //
+ init (opts);
+
+ // Read brep::module configuration.
+ //
+ static option_descriptions od (
+ convert (options::module::description ()));
+
+ name_values mo (filter (opts, od));
+ name_value_scanner s (mo);
+ options::module o (s, cli::unknown_mode::fail, cli::unknown_mode::fail);
+
+ verb_ = o.verbosity ();
+ initialized_ = true;
+ }
+ catch (const server_error& e)
+ {
+ log_write (e.data);
+ throw runtime_error ("initialization failed");
+ }
+ catch (const cli::exception& e)
+ {
+ ostringstream o;
+ e.print (o);
+ throw runtime_error (o.str ());
+ }
+ }
+
+ void module::
+ init (const name_values& options)
+ {
+ name_value_scanner s (options);
+ init (s);
+ assert (!s.more ()); // Module didn't handle its options.
+ }
+
+ module::
+ module (): log_writer_ (bind (&module::log_write, this, _1)) {}
+
+ // Custom copy constructor is required to initialize log_writer_ properly.
+ //
+ module::
+ module (const module& m): module ()
+ {
+ verb_ = m.verb_;
+ initialized_ = m.initialized_;
+ }
+
+// For function func declared like this:
+// using B = std::string (*)(int);
+// using A = B (*)(int,int);
+// A func(B (*)(char),B (*)(wchar_t));
+// __PRETTY_FUNCTION__ looks like this:
+// virtual std::string (* (* brep::search::func(std::string (* (*)(char))(int)
+// ,std::string (* (*)(wchar_t))(int)) const)(int, int))(int)
+//
+ string module::
+ func_name (const char* pretty_name)
+ {
+ const char* e (strchr (pretty_name, ')'));
+
+ if (e && e > pretty_name)
+ {
+ // Position e at last matching '(' which is the beginning of the
+ // argument list..
+ //
+ size_t d (1);
+
+ do
+ {
+ switch (*--e)
+ {
+ case ')': ++d; break;
+ case '(': --d; break;
+ }
+ }
+ while (d && e > pretty_name);
+
+ if (!d && e > pretty_name)
+ {
+ // Position e at the character following the function name.
+ //
+ while (e > pretty_name &&
+ (*e != '(' || *(e - 1) == ' ' || *(e - 1) == ')'))
+ --e;
+
+ if (e > pretty_name)
+ {
+ // Position b at the beginning of the qualified function name.
+ //
+ const char* b (e);
+ while (--b > pretty_name && *b != ' ');
+ if (*b == ' ') ++b;
+
+ return string (b, e - b);
+ }
+ }
+ }
+
+ throw invalid_argument ("::brep::module::func_name");
+ }
+
+ void module::
+ log_write (const diag_data& d) const
+ {
+ if (log_ == nullptr)
+ return; // No backend yet.
+
+ //@@ Cast log_ to apache::log and write the records.
+ //
+ auto al (dynamic_cast<web::apache::log*> (log_));
+
+ if (al)
+ {
+ // Considered using lambda for mapping but looks too verbose while can
+ // be a bit safer in runtime.
+ //
+ // Use APLOG_INFO (as opposed to APLOG_TRACE1) as a mapping for
+ // severity::trace. "LogLevel trace1" configuration directive switches
+ // on the avalanche of log messages from various modules. Would be good
+ // to avoid wading through them.
+ //
+ static int s[] = {APLOG_ERR, APLOG_WARNING, APLOG_INFO, APLOG_INFO};
+
+ for (const auto& e: d)
+ {
+ string name;
+
+ try
+ {
+ name = func_name (e.name);
+ }
+ catch (const invalid_argument&)
+ {
+ // Log "pretty" function description, see in log file & fix.
+ name = e.name;
+ }
+
+ al->write (e.loc.file.c_str (),
+ e.loc.line,
+ name.c_str (),
+ s[static_cast<size_t> (e.sev)],
+ e.msg.c_str ());
+ }
+ }
+ }
+
+ void module::
+ version (log& l)
+ {
+ log_ = &l;
+ version ();
+ }
+
+ // module::name_value_scanner
+ //
+ module::name_value_scanner::
+ name_value_scanner (const name_values& nv) noexcept
+ : name_values_ (nv),
+ i_ (nv.begin ()),
+ name_ (true)
+ {
+ }
+
+ bool module::name_value_scanner::
+ more ()
+ {
+ return i_ != name_values_.end ();
+ }
+
+ const char* module::name_value_scanner::
+ peek ()
+ {
+ if (i_ != name_values_.end ())
+ return name_ ? i_->name.c_str () : i_->value->c_str ();
+ else
+ throw cli::eos_reached ();
+ }
+
+ const char* module::name_value_scanner::
+ next ()
+ {
+ if (i_ != name_values_.end ())
+ {
+ const char* r (name_ ? i_->name.c_str () : i_->value->c_str ());
+ skip ();
+ return r;
+ }
+ else
+ throw cli::eos_reached ();
+ }
+
+ void module::name_value_scanner::
+ skip ()
+ {
+ if (i_ != name_values_.end ())
+ {
+ if (name_)
+ {
+ if (i_->value)
+ name_ = false;
+ else
+ ++i_;
+ }
+ else
+ {
+ ++i_;
+ name_ = true;
+ }
+ }
+ else
+ throw cli::eos_reached ();
+ }
+}
diff --git a/mod/options-types b/mod/options-types
new file mode 100644
index 0000000..e696139
--- /dev/null
+++ b/mod/options-types
@@ -0,0 +1,32 @@
+// file : mod/options-types -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef MOD_OPTIONS_TYPES
+#define MOD_OPTIONS_TYPES
+
+#include <brep/types>
+#include <brep/utility>
+
+namespace brep
+{
+ // brep types
+ //
+ enum class page_form
+ {
+ full,
+ brief
+ };
+
+ struct page_menu
+ {
+ string label;
+ string link;
+
+ page_menu () = default;
+ page_menu (string b, string l): label (move (b)), link (move (l)) {}
+ };
+
+}
+
+#endif // MOD_OPTIONS_TYPES
diff --git a/mod/options.cli b/mod/options.cli
new file mode 100644
index 0000000..ba8b37b
--- /dev/null
+++ b/mod/options.cli
@@ -0,0 +1,211 @@
+// file : mod/options.cli -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+include <web/xhtml-fragment>;
+
+include <brep/types>;
+
+include <mod/options-types>;
+
+namespace brep
+{
+ // Web module configuration options.
+ //
+ namespace options
+ {
+ // Option groups.
+ //
+ class module
+ {
+ dir_path root = "/"
+ {
+ "<path>"
+ "Repository root. That is, this is the part of the URL between the
+ host name and the start of the repository. For example, root value
+ '\cb{/pkg}' means the repository URL is http://example.org/pkg/.
+ Specify '\cb{/}' to use the web server root (http://example.org/)."
+ }
+
+ uint16_t verbosity = 0
+ {
+ "<level>",
+ "Trace verbosity level. Level 0 disables tracing, which is also the
+ default."
+ }
+ };
+
+ class db
+ {
+ string db-user
+ {
+ "<user>",
+ "Database user name. If not specified, then operating system (login)
+ name is used."
+ }
+
+ string db-password
+ {
+ "<pass>",
+ "Database password. If not specified, then login without password is
+ expected to work."
+ }
+
+ string db-name = "brep"
+ {
+ "<name>",
+ "Database name. If not specified, then '\cb{brep}' is used by
+ default."
+ }
+
+ string db-host
+ {
+ "<host>",
+ "Database host name, address, or socket. If not specified, then
+ connect to \cb{localhost} using the operating system-default
+ mechanism (Unix-domain socket, etc)."
+ }
+
+ uint16_t db-port = 0
+ {
+ "<port>",
+ "Database port number. If not specified, the default port is used."
+ }
+
+ size_t db-max-connections = 5
+ {
+ "<num>",
+ "The maximum number of concurrent database connections per web server
+ process. If 0, then no limitation is applied. The default is 5."
+ }
+
+ size_t db-retry = 10
+ {
+ "<num>",
+ "The maximum number of times to retry database transactions in the
+ face of recoverable failures (deadlock, loss of connection, etc). The
+ default is 10."
+ }
+ };
+
+ class page
+ {
+ web::xhtml::fragment logo
+ {
+ "<xhtml>",
+ "Web page logo. It is displayed in the page header aligned to the left
+ edge. The value is treated as an XHTML5 fragment."
+ }
+
+ vector<page_menu> menu;
+ {
+ "<label=link>",
+ "Web page menu. Each entry is displayed in the page header in the
+ order specified and aligned to the right edge. A link target that
+ starts with '\cb{/}' or contains '\cb{:}' is used as is. Otherwise,
+ it is prefixed with the repository web interface root."
+ }
+ };
+
+ class search
+ {
+ uint16_t search-results = 10
+ {
+ "<num>",
+ "Number of results per page. The default is 10."
+ }
+
+ uint16_t search-pages = 5
+ {
+ "<num>",
+ "Number of pages in navigation (pager). The default is 5."
+ }
+ };
+
+ class package
+ {
+ uint16_t package-description = 500
+ {
+ "<len>",
+ "Number of package description characters to display in brief pages.
+ The default is 500 (~ 80 characters * 6 lines)."
+ }
+
+ uint16_t package-changes = 5000;
+ {
+ "<len>",
+ "Number of package changes characters to display in brief pages. The
+ default is 5000 (~ 80 chars x 60 lines)."
+ }
+ };
+
+ // Module options.
+ //
+ class package_search: search, db, page, module
+ {
+ };
+
+ class package_details: package, search, db, page, module
+ {
+ };
+
+ class package_version_details: package, db, page, module
+ {
+ };
+
+ class repository_details: db, page, module
+ {
+ };
+
+ class repository_root: module
+ {
+ };
+ }
+
+ // Web module HTTP request parameters.
+ //
+ namespace params
+ {
+ // Use parameters long names in the C++ code, short aliases (if present)
+ // in HTTP URL.
+ //
+ class package_search
+ {
+ // Display package search result list starting from this page.
+ //
+ uint16_t page | p;
+
+ // Package search criteria.
+ //
+ string query | q;
+ };
+
+ class package_details
+ {
+ // Display package version search result list starting from this page.
+ //
+ uint16_t page | p;
+
+ // Package version search criteria.
+ //
+ string query | q;
+
+ // Page form.
+ //
+ page_form form | f = page_form::brief;
+ };
+
+ class package_version_details
+ {
+ // Page form.
+ //
+ page_form form | f = page_form::brief;
+ };
+
+ class repository_details
+ {
+ // No parameters so far.
+ //
+ };
+ }
+}
diff --git a/mod/page b/mod/page
new file mode 100644
index 0000000..b326d6c
--- /dev/null
+++ b/mod/page
@@ -0,0 +1,403 @@
+// file : mod/page -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef MOD_PAGE
+#define MOD_PAGE
+
+#include <xml/forward>
+
+#include <web/xhtml-fragment>
+
+#include <brep/types>
+#include <brep/utility>
+
+#include <brep/package>
+
+#include <mod/options-types> // page_menu
+
+namespace brep
+{
+ // Page common building blocks.
+ //
+
+ // Generates CSS link elements.
+ //
+ class CSS_LINKS
+ {
+ public:
+ CSS_LINKS (const path& p, const dir_path& r): path_ (p), root_ (r) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const path& path_;
+ const dir_path& root_;
+ };
+
+ // Generates page header element.
+ //
+ class DIV_HEADER
+ {
+ public:
+ DIV_HEADER (const dir_path& root,
+ const web::xhtml::fragment& logo,
+ const vector<page_menu>& menu):
+ root_ (root), logo_ (logo), menu_ (menu) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const dir_path& root_;
+ const web::xhtml::fragment& logo_;
+ const vector<page_menu>& menu_;
+ };
+
+ // Generates package search form element.
+ //
+ class FORM_SEARCH
+ {
+ public:
+ FORM_SEARCH (const string& q): query_ (q) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const string& query_;
+ };
+
+ // Generates counter element.
+ //
+ // It could be redunant to distinguish between singular and plural word forms
+ // if it wouldn't be so cheap in English, and phrase '1 Packages' wouldn't
+ // look that ugly.
+ //
+ class DIV_COUNTER
+ {
+ public:
+ DIV_COUNTER (size_t c, const char* s, const char* p)
+ : count_ (c), singular_ (s), plural_ (p) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ size_t count_;
+ const char* singular_;
+ const char* plural_;
+ };
+
+ // Generates package name element.
+ //
+ class TR_NAME
+ {
+ public:
+ TR_NAME (const string& n, const string& q, const dir_path& r)
+ : name_ (n), query_param_ (q), root_ (r) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const string& name_;
+ const string& query_param_;
+ const dir_path& root_;
+ };
+
+ // Generates package version element.
+ //
+ class TR_VERSION
+ {
+ public:
+ // Display the version as a link to the package version details page.
+ //
+ TR_VERSION (const string& p, const string& v, const dir_path& r)
+ : package_ (&p), version_ (v), root_ (&r) {}
+
+ // Display the version as a regular text.
+ //
+ TR_VERSION (const string& v)
+ : package_ (nullptr), version_ (v), root_ (nullptr) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const string* package_;
+ const string& version_;
+ const dir_path* root_;
+ };
+
+ // Generates package summary element.
+ //
+ class TR_SUMMARY
+ {
+ public:
+ TR_SUMMARY (const string& s): summary_ (s) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const string& summary_;
+ };
+
+ // Generates package license alternatives element.
+ //
+ class TR_LICENSE
+ {
+ public:
+ TR_LICENSE (const license_alternatives& l): licenses_ (l) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const license_alternatives& licenses_;
+ };
+
+ // Generates package license alternatives elements. Differs from TR_LICENSE
+ // by producing multiple rows instead of a single one.
+ //
+ class TR_LICENSES
+ {
+ public:
+ TR_LICENSES (const license_alternatives& l): licenses_ (l) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const license_alternatives& licenses_;
+ };
+
+ // Generates package tags element.
+ //
+ class TR_TAGS
+ {
+ public:
+ TR_TAGS (const strings& ts, const dir_path& r): tags_ (ts), root_ (r) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const strings& tags_;
+ const dir_path& root_;
+ };
+
+ // Generates package dependencies element.
+ //
+ class TR_DEPENDS
+ {
+ public:
+ TR_DEPENDS (const dependencies& d, const dir_path& r)
+ : dependencies_ (d), root_ (r) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const dependencies& dependencies_;
+ const dir_path& root_;
+ };
+
+ // Generates package requirements element.
+ //
+ class TR_REQUIRES
+ {
+ public:
+ TR_REQUIRES (const requirements& r): requirements_ (r) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const requirements& requirements_;
+ };
+
+ // Generates url element.
+ //
+ class TR_URL
+ {
+ public:
+ TR_URL (const url& u, const char* l = "url"): url_ (u), label_ (l) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const url& url_;
+ const char* label_;
+ };
+
+ // Generates email element.
+ //
+ class TR_EMAIL
+ {
+ public:
+ TR_EMAIL (const email& e, const char* l = "email")
+ : email_ (e), label_ (l) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const email& email_;
+ const char* label_;
+ };
+
+ // Generates package version priority element.
+ //
+ class TR_PRIORITY
+ {
+ public:
+ TR_PRIORITY (const priority& p): priority_ (p) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const priority& priority_;
+ };
+
+ // Generates package location element.
+ //
+ class TR_LOCATION
+ {
+ public:
+ TR_LOCATION (const string& n, const dir_path& r)
+ : name_ (n), root_ (r) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const string& name_;
+ const dir_path& root_;
+ };
+
+ // Generates package download URL element.
+ //
+ class TR_DOWNLOAD
+ {
+ public:
+ TR_DOWNLOAD (const string& u): url_ (u) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const string& url_;
+ };
+
+ // Generates sha256sum element.
+ //
+ class TR_SHA256SUM
+ {
+ public:
+ TR_SHA256SUM (const string& s): sha256sum_ (s) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const string& sha256sum_;
+ };
+
+ // Generates comment element.
+ //
+ class SPAN_COMMENT
+ {
+ public:
+ SPAN_COMMENT (const string& c): comment_ (c) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const string& comment_;
+ };
+
+ // Generates package description element.
+ //
+ class P_DESCRIPTION
+ {
+ public:
+ // Genereate full description.
+ //
+ P_DESCRIPTION (const string& d, const string& id = "")
+ : description_ (d), length_ (d.size ()), url_ (nullptr), id_ (id) {}
+
+ // Genereate brief description.
+ //
+ P_DESCRIPTION (const string& d, size_t l, const string& u)
+ : description_ (d), length_ (l), url_ (&u) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const string& description_;
+ size_t length_;
+ const string* url_; // Full page url.
+ string id_;
+ };
+
+ // Generates package description element.
+ //
+ class PRE_CHANGES
+ {
+ public:
+ // Genereate full changes info.
+ //
+ PRE_CHANGES (const string& c)
+ : changes_ (c), length_ (c.size ()), url_ (nullptr) {}
+
+ // Genereate brief changes info.
+ //
+ PRE_CHANGES (const string& c, size_t l, const string& u)
+ : changes_ (c), length_ (l), url_ (&u) {}
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ const string& changes_;
+ size_t length_;
+ const string* url_; // Full page url.
+ };
+
+ // Generates paging element.
+ //
+ class DIV_PAGER
+ {
+ public:
+ DIV_PAGER (size_t current_page,
+ size_t item_count,
+ size_t item_per_page,
+ size_t page_number_count,
+ const string& url);
+
+ void
+ operator() (xml::serializer&) const;
+
+ private:
+ size_t current_page_;
+ size_t item_count_;
+ size_t item_per_page_;
+ size_t page_number_count_;
+ const string& url_;
+ };
+
+ // Convert the argument to a string representing the valid HTML 5 'id'
+ // attribute value.
+ //
+ string
+ html_id (const string&);
+}
+
+#endif // MOD_PAGE
diff --git a/mod/page.cxx b/mod/page.cxx
new file mode 100644
index 0000000..7fc2e90
--- /dev/null
+++ b/mod/page.cxx
@@ -0,0 +1,693 @@
+// file : mod/page.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <mod/page>
+
+#include <set>
+#include <ios> // hex, uppercase, right
+#include <sstream>
+#include <iomanip> // setw(), setfill()
+#include <algorithm> // min()
+
+#include <xml/serializer>
+
+#include <web/xhtml>
+#include <web/mime-url-encoding>
+
+#include <brep/package>
+#include <brep/package-odb>
+
+using namespace std;
+using namespace xml;
+using namespace web;
+using namespace web::xhtml;
+
+namespace brep
+{
+ // CSS_LINKS
+ //
+ void CSS_LINKS::
+ operator() (serializer& s) const
+ {
+ static const path css ("@");
+
+ s << *LINK(REL="stylesheet", TYPE="text/css", HREF=root_ / css / path_);
+ }
+
+ // DIV_HEADER
+ //
+ void DIV_HEADER::
+ operator() (serializer& s) const
+ {
+ if (!logo_.empty () || !menu_.empty ())
+ {
+ s << DIV(ID="header-bar")
+ << DIV(ID="header");
+
+ if (!logo_.empty ())
+ s << DIV(ID="header-logo") << logo_ << ~DIV;
+
+ if (!menu_.empty ())
+ {
+ s << DIV(ID="header-menu")
+ << DIV(ID="header-menu-body");
+
+ for (const auto& m: menu_)
+ {
+ const string& l (m.link[0] == '/' || m.link.find (':') != string::npos
+ ? m.link
+ : root_.string () + m.link);
+
+ s << A(HREF=l) << m.label << ~A;
+ }
+
+ s << ~DIV
+ << ~DIV;
+ }
+
+ s << ~DIV
+ << ~DIV;
+ }
+ }
+
+ // FORM_SEARCH
+ //
+ void FORM_SEARCH::
+ operator() (serializer& s) const
+ {
+ // The 'action' attribute is optional in HTML5. While the standard doesn't
+ // specify browser behavior explicitly for the case the attribute is
+ // ommited, the only reasonable behavior is to default it to the current
+ // document URL.
+ //
+ s << FORM(ID="search")
+ << TABLE(CLASS="form-table")
+ << TBODY
+ << TR
+ << TD(ID="search-txt")
+ << *INPUT(TYPE="search", NAME="q", VALUE=query_,
+ AUTOFOCUS="autofocus")
+ << ~TD
+ << TD(ID="search-btn")
+ << *INPUT(TYPE="submit", VALUE="Search")
+ << ~TD
+ << ~TR
+ << ~TBODY
+ << ~TABLE
+ << ~FORM;
+ }
+
+ // DIV_COUNTER
+ //
+ void DIV_COUNTER::
+ operator() (serializer& s) const
+ {
+ s << DIV(ID="count")
+ << count_ << " "
+ << (count_ % 10 == 1 && count_ % 100 != 11 ? singular_ : plural_)
+ << ~DIV;
+ }
+
+ // TR_NAME
+ //
+ void TR_NAME::
+ operator() (serializer& s) const
+ {
+ s << TR(CLASS="name")
+ << TH << "name" << ~TH
+ << TD
+ << SPAN(CLASS="value")
+ << A
+ << HREF
+
+ // Propagate search criteria to the package details page.
+ //
+ << root_ / path (mime_url_encode (name_)) << query_param_
+
+ << ~HREF
+ << name_
+ << ~A
+ << ~SPAN
+ << ~TD
+ << ~TR;
+ }
+
+ void TR_VERSION::
+ operator() (serializer& s) const
+ {
+ s << TR(CLASS="version")
+ << TH << "version" << ~TH
+ << TD
+ << SPAN(CLASS="value");
+
+ if (package_ == nullptr)
+ s << version_;
+ else
+ {
+ assert (root_ != nullptr);
+ s << A(HREF=*root_ / path (mime_url_encode (*package_)) / path (version_))
+ << version_
+ << ~A;
+ }
+
+ s << ~SPAN
+ << ~TD
+ << ~TR;
+ }
+
+ // TR_SUMMARY
+ //
+ void TR_SUMMARY::
+ operator() (serializer& s) const
+ {
+ s << TR(CLASS="summary")
+ << TH << "summary" << ~TH
+ << TD << SPAN(CLASS="value") << summary_ << ~SPAN << ~TD
+ << ~TR;
+ }
+
+ // TR_LICENSE
+ //
+ void TR_LICENSE::
+ operator() (serializer& s) const
+ {
+ s << TR(CLASS="license")
+ << TH << "license" << ~TH
+ << TD
+ << SPAN(CLASS="value");
+
+ for (const auto& la: licenses_)
+ {
+ if (&la != &licenses_[0])
+ s << " " << EM << "or" << ~EM << " ";
+
+ bool m (la.size () > 1);
+
+ if (m)
+ s << "(";
+
+ for (const auto& l: la)
+ {
+ if (&l != &la[0])
+ s << " " << EM << "and" << ~EM << " ";
+
+ s << l;
+ }
+
+ if (m)
+ s << ")";
+ }
+
+ s << ~SPAN
+ << ~TD
+ << ~TR;
+ }
+
+ // TR_LICENSES
+ //
+ void TR_LICENSES::
+ operator() (serializer& s) const
+ {
+ for (const auto& la: licenses_)
+ {
+ s << TR(CLASS="license")
+ << TH << "license" << ~TH
+ << TD
+ << SPAN(CLASS="value");
+
+ for (const auto& l: la)
+ {
+ if (&l != &la[0])
+ s << " " << EM << "and" << ~EM << " ";
+
+ s << l;
+ }
+
+ s << ~SPAN
+ << SPAN_COMMENT (la.comment)
+ << ~TD
+ << ~TR;
+ }
+ }
+
+ // TR_TAGS
+ //
+ void TR_TAGS::
+ operator() (serializer& s) const
+ {
+ if (!tags_.empty ())
+ {
+ s << TR(CLASS="tags")
+ << TH << "tags" << ~TH
+ << TD
+ << SPAN(CLASS="value");
+
+ for (const auto& t: tags_)
+ {
+ if (&t != &tags_[0])
+ s << " ";
+
+ s << A << HREF << root_ << "?q=" << mime_url_encode (t) << ~HREF
+ << t
+ << ~A;
+ }
+
+ s << ~SPAN
+ << ~TD
+ << ~TR;
+ }
+ }
+
+ // TR_DEPENDS
+ //
+ void TR_DEPENDS::
+ operator() (serializer& s) const
+ {
+ s << TR(CLASS="depends")
+ << TH << "depends" << ~TH
+ << TD
+ << SPAN(CLASS="value")
+ << dependencies_.size ();
+
+ if (!dependencies_.empty ())
+ s << "; ";
+
+ for (const auto& d: dependencies_)
+ {
+ if (&d != &dependencies_[0])
+ s << ", ";
+
+ if (d.conditional)
+ s << "?";
+
+ // Suppress package name duplicates.
+ //
+ set<string> names;
+ for (const auto& da: d)
+ names.emplace (da.name ());
+
+ bool mult (names.size () > 1);
+
+ if (mult)
+ s << "(";
+
+ bool first (true);
+ for (const auto& da: d)
+ {
+ string n (da.name ());
+ if (names.find (n) != names.end ())
+ {
+ names.erase (n);
+
+ if (first)
+ first = false;
+ else
+ s << " | ";
+
+ shared_ptr<package> p (da.package.load ());
+ assert (p->internal () || !p->other_repositories.empty ());
+
+ shared_ptr<repository> r (
+ p->internal ()
+ ? p->internal_repository.load ()
+ : p->other_repositories[0].load ());
+
+ auto en (mime_url_encode (n));
+
+ if (r->url)
+ s << A(HREF=*r->url + en) << n << ~A;
+ else if (p->internal ())
+ s << A(HREF=root_ / path (en)) << n << ~A;
+ else
+ // Display the dependency as a plain text if no repository URL
+ // available.
+ //
+ s << n;
+ }
+ }
+
+ if (mult)
+ s << ")";
+ }
+
+ s << ~SPAN
+ << ~TD
+ << ~TR;
+ }
+
+ // TR_REQUIRES
+ //
+ void TR_REQUIRES::
+ operator() (serializer& s) const
+ {
+ // If there are no requirements, then we omit it, unlike depends, where we
+ // show 0 explicitly.
+ //
+ if (requirements_.empty ())
+ return;
+
+ s << TR(CLASS="requires")
+ << TH << "requires" << ~TH
+ << TD
+ << SPAN(CLASS="value")
+ << requirements_.size () << "; ";
+
+ for (const auto& r: requirements_)
+ {
+ if (&r != &requirements_[0])
+ s << ", ";
+
+ if (r.conditional)
+ s << "?";
+
+ if (r.empty ())
+ {
+ // If there is no requirement alternatives specified, then
+ // print the comment first word.
+ //
+ const auto& c (r.comment);
+ if (!c.empty ())
+ {
+ auto n (c.find (' '));
+ s << string (c, 0, n);
+
+ if (n != string::npos)
+ s << "...";
+ }
+ }
+ else
+ {
+ bool mult (r.size () > 1);
+
+ if (mult)
+ s << "(";
+
+ for (const auto& ra: r)
+ {
+ if (&ra != &r[0])
+ s << " | ";
+
+ s << ra;
+ }
+
+ if (mult)
+ s << ")";
+ }
+ }
+
+ s << ~SPAN
+ << ~TD
+ << ~TR;
+ }
+
+ // TR_URL
+ //
+ void TR_URL::
+ operator() (serializer& s) const
+ {
+ s << TR(CLASS=label_)
+ << TH << label_ << ~TH
+ << TD
+ << SPAN(CLASS="value") << A(HREF=url_) << url_ << ~A << ~SPAN
+ << SPAN_COMMENT (url_.comment)
+ << ~TD
+ << ~TR;
+ }
+
+ // TR_EMAIL
+ //
+ void TR_EMAIL::
+ operator() (serializer& s) const
+ {
+ s << TR(CLASS=label_)
+ << TH << label_ << ~TH
+ << TD
+ << SPAN(CLASS="value")
+ << A(HREF="mailto:" + email_) << email_ << ~A
+ << ~SPAN
+ << SPAN_COMMENT (email_.comment)
+ << ~TD
+ << ~TR;
+ }
+
+ // TR_PRIORITY
+ //
+ void TR_PRIORITY::
+ operator() (serializer& s) const
+ {
+ static const strings priority_names ({"low", "medium", "high", "security"});
+ assert (priority_ < priority_names.size ());
+
+ s << TR(CLASS="priority")
+ << TH << "priority" << ~TH
+ << TD
+ << SPAN(CLASS="value") << priority_names[priority_] << ~SPAN
+ << SPAN_COMMENT (priority_.comment)
+ << ~TD
+ << ~TR;
+ }
+
+ // TR_LOCATION
+ //
+ void TR_LOCATION::
+ operator() (serializer& s) const
+ {
+ s << TR(CLASS="location")
+ << TH << "location" << ~TH
+ << TD
+ << SPAN(CLASS="value")
+ << A
+ << HREF
+ << root_ << "?about#" << mime_url_encode (html_id (name_))
+ << ~HREF
+ << name_
+ << ~A
+ << ~SPAN
+ << ~TD
+ << ~TR;
+ }
+
+ // TR_DOWNLOAD
+ //
+ void TR_DOWNLOAD::
+ operator() (serializer& s) const
+ {
+ s << TR(CLASS="download")
+ << TH << "download" << ~TH
+ << TD
+ << SPAN(CLASS="value") << A(HREF=url_) << url_ << ~A << ~SPAN
+ << ~TD
+ << ~TR;
+ }
+
+ // TR_SHA256SUM
+ //
+ void TR_SHA256SUM::
+ operator() (serializer& s) const
+ {
+ s << TR(CLASS="sha256")
+ << TH << "sha256" << ~TH
+ << TD << SPAN(CLASS="value") << sha256sum_ << ~SPAN << ~TD
+ << ~TR;
+ }
+
+ // SPAN_COMMENT
+ //
+ void SPAN_COMMENT::
+ operator() (serializer& s) const
+ {
+ if (size_t l = comment_.size ())
+ s << SPAN(CLASS="comment")
+ << (comment_.back () == '.' ? string (comment_, 0, l - 1) : comment_)
+ << ~SPAN;
+ }
+
+ // P_DESCRIPTION
+ //
+ void P_DESCRIPTION::
+ operator() (serializer& s) const
+ {
+ if (description_.empty ())
+ return;
+
+ auto n (description_.find_first_of (" \t\n", length_));
+ bool full (n == string::npos); // Description length is below the limit.
+
+ // Truncate description if length exceed the limit.
+ //
+ const string& d (full ? description_ : string (description_, 0, n));
+
+ // Format the description into paragraphs, recognizing a blank line as
+ // paragraph separator, and replacing single newlines with a space.
+ //
+ s << P;
+
+ if (!id_.empty ())
+ s << ID(id_);
+
+ bool nl (false); // The previous character is '\n'.
+ for (const auto& c: d)
+ {
+ if (c == '\n')
+ {
+ if (nl)
+ {
+ s << ~P << P;
+ nl = false;
+ }
+ else
+ nl = true; // Delay printing until the next character.
+ }
+ else
+ {
+ if (nl)
+ {
+ s << ' '; // Replace the previous newline with a space.
+ nl = false;
+ }
+
+ s << c;
+ }
+ }
+
+ if (!full)
+ {
+ assert (url_ != nullptr);
+ s << "... " << A(HREF=*url_) << "More" << ~A;
+ }
+
+ s << ~P;
+ }
+
+ // PRE_CHANGES
+ //
+ void PRE_CHANGES::
+ operator() (serializer& s) const
+ {
+ if (changes_.empty ())
+ return;
+
+ auto n (changes_.find_first_of (" \t\n", length_));
+ bool full (n == string::npos); // Changes length is below the limit.
+
+ // Truncate changes if length exceed the limit.
+ //
+ const string& c (full ? changes_ : string (changes_, 0, n));
+ s << PRE(ID="changes") << c;
+
+ if (!full)
+ {
+ assert (url_ != nullptr);
+ s << "... " << A(HREF=*url_) << "More" << ~A;
+ }
+
+ s << ~PRE;
+ }
+
+ // DIV_PAGER
+ //
+ DIV_PAGER::
+ DIV_PAGER (size_t current_page,
+ size_t item_count,
+ size_t item_per_page,
+ size_t page_number_count,
+ const string& url)
+ : current_page_ (current_page),
+ item_count_ (item_count),
+ item_per_page_ (item_per_page),
+ page_number_count_ (page_number_count),
+ url_ (url)
+ {
+ }
+
+ void DIV_PAGER::
+ operator() (serializer& s) const
+ {
+ if (item_count_ == 0 || item_per_page_ == 0)
+ return;
+
+ size_t pcount (item_count_ / item_per_page_); // Page count.
+
+ if (item_count_ % item_per_page_)
+ ++pcount;
+
+ if (pcount > 1)
+ {
+ auto url (
+ [this](size_t page) -> string
+ {
+ return page == 0
+ ? url_
+ : url_ + (url_.find ('?') == string::npos ? "?p=" : "&p=") +
+ to_string (page);
+ });
+
+ s << DIV(ID="pager");
+
+ if (current_page_ > 0)
+ s << A(ID="prev", HREF=url (current_page_ - 1)) << "Prev" << ~A;
+
+ if (page_number_count_)
+ {
+ size_t offset (page_number_count_ / 2);
+ size_t from (current_page_ > offset ? current_page_ - offset : 0);
+ size_t to (min (from + page_number_count_, pcount));
+
+ for (size_t p (from); p < to; ++p)
+ {
+ s << A(HREF=url (p));
+
+ if (p == current_page_)
+ s << ID("curr");
+
+ s << p + 1
+ << ~A;
+ }
+ }
+
+ if (current_page_ < pcount - 1)
+ s << A(ID="next", HREF=url (current_page_ + 1)) << "Next" << ~A;
+
+ s << ~DIV;
+ }
+ }
+
+ // Convert the argument to a string conformant to the section
+ // "3.2.5.1 The id attribute" of the HTML 5 specification at
+ // http://www.w3.org/TR/html5/dom.html#the-id-attribute.
+ //
+ string
+ html_id (const string& v)
+ {
+ ostringstream o;
+ o << hex << uppercase << right << setfill ('0');
+
+ // Replace space characters (as specified at
+ // http://www.w3.org/TR/html5/infrastructure.html#space-character) with
+ // the respective escape sequences.
+ //
+ for (auto c: v)
+ {
+ switch (c)
+ {
+ case ' ':
+ case '\t':
+ case '\n':
+ case '\r':
+ case '\f':
+ case '~':
+ {
+ // We use '~' as an escape character because it doesn't require
+ // escaping in URLs.
+ //
+ o << "~" << setw (2) << static_cast<unsigned short> (c);
+ break;
+ }
+ default: o << c; break;
+ }
+ }
+
+ return o.str ();
+ }
+}
diff --git a/mod/services.cxx b/mod/services.cxx
new file mode 100644
index 0000000..b0c5834
--- /dev/null
+++ b/mod/services.cxx
@@ -0,0 +1,15 @@
+// file : mod/services.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <ap_config.h> // AP_MODULE_DECLARE_DATA
+
+#include <web/apache/service>
+
+#include <brep/types>
+#include <brep/utility>
+
+#include <mod/mod-repository-root>
+
+static brep::repository_root mod;
+web::apache::service AP_MODULE_DECLARE_DATA brep_module ("brep", mod);
diff --git a/mod/types-parsers b/mod/types-parsers
new file mode 100644
index 0000000..cbea1bd
--- /dev/null
+++ b/mod/types-parsers
@@ -0,0 +1,57 @@
+// file : mod/types-parsers -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+// CLI parsers, included into the generated source files.
+//
+
+#ifndef MOD_TYPES_PARSERS
+#define MOD_TYPES_PARSERS
+
+#include <web/xhtml-fragment>
+
+#include <brep/types>
+#include <brep/utility>
+
+#include <mod/options-types>
+
+namespace brep
+{
+ namespace cli
+ {
+ class scanner;
+
+ template <typename T>
+ struct parser;
+
+ template <>
+ struct parser<dir_path>
+ {
+ static void
+ parse (dir_path&, scanner&);
+ };
+
+ template <>
+ struct parser<page_form>
+ {
+ static void
+ parse (page_form&, scanner&);
+ };
+
+ template <>
+ struct parser<page_menu>
+ {
+ static void
+ parse (page_menu&, scanner&);
+ };
+
+ template <>
+ struct parser<web::xhtml::fragment>
+ {
+ static void
+ parse (web::xhtml::fragment&, scanner&);
+ };
+ }
+}
+
+#endif // MOD_TYPES_PARSERS
diff --git a/mod/types-parsers.cxx b/mod/types-parsers.cxx
new file mode 100644
index 0000000..279ab58
--- /dev/null
+++ b/mod/types-parsers.cxx
@@ -0,0 +1,114 @@
+// file : mod/types-parsers.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <mod/types-parsers>
+
+#include <mod/options>
+
+using namespace std;
+using namespace web::xhtml;
+
+namespace brep
+{
+ namespace cli
+ {
+ // Parse path.
+ //
+ template <typename T>
+ static void
+ parse_path (T& x, scanner& s)
+ {
+ const char* o (s.next ());
+
+ if (!s.more ())
+ throw missing_value (o);
+
+ const char* v (s.next ());
+
+ try
+ {
+ x = T (v);
+ }
+ catch (const invalid_path&)
+ {
+ throw invalid_value (o, v);
+ }
+ }
+
+ void parser<dir_path>::
+ parse (dir_path& x, scanner& s)
+ {
+ parse_path (x, s);
+ }
+
+ // Parse page_form.
+ //
+ void parser<page_form>::
+ parse (page_form& x, scanner& s)
+ {
+ const char* o (s.next ());
+
+ if (!s.more ())
+ throw missing_value (o);
+
+ const string v (s.next ());
+ if (v == "full")
+ x = page_form::full;
+ else if (v == "brief")
+ x = page_form::brief;
+ else
+ throw invalid_value (o, v);
+ }
+
+ // Parse page_menu.
+ //
+ void parser<page_menu>::
+ parse (page_menu& x, scanner& s)
+ {
+ const char* o (s.next ());
+
+ if (!s.more ())
+ throw missing_value (o);
+
+ const string v (s.next ());
+
+ auto p (v.find ('='));
+ if (p != string::npos)
+ {
+ string label (v, 0, p);
+ string link (v, p + 1);
+
+ if (!label.empty ())
+ {
+ x = page_menu (move (label), move (link));
+ return;
+ }
+ }
+
+ throw invalid_value (o, v);
+ }
+
+ // Parse web::xhtml::fragment.
+ //
+ void parser<fragment>::
+ parse (fragment& x, scanner& s)
+ {
+ const char* o (s.next ());
+
+ if (!s.more ())
+ throw missing_value (o);
+
+ const char* v (s.next ());
+
+ try
+ {
+ x = fragment (v, o);
+ }
+ catch (const xml::parsing&)
+ {
+ throw invalid_value (o, v);
+ }
+ }
+ }
+}