--- /dev/null
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+// Produce System Messages Manual
+//
+// This tool reads all the .mes files in the directory tree whose root is given
+// on the command line and interprets them as message files. It pulls
+// all the messages and description out, sorts them by message ID, and writes
+// them out as a single (formatted) file.
+//
+// Invocation:
+// The code is invoked using the command line:
+//
+// kt_system_messages.py [-o <output-file>] [<top-source-directory>|<files>]
+//
+// If no output file is specified, output is written to stdout.
+
+#include <algorithm>
+#include <fstream>
+#include <iostream>
+#include <map>
+#include <string>
+#include <vector>
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <boost/filesystem.hpp>
+#include <boost/lexical_cast.hpp>
+
+typedef std::vector<std::string> lines_type;
+
+/// dictionary values
+struct Details {
+ std::string text;
+ lines_type description;
+ std::string filename;
+};
+
+/// Main dictionary holding all the messages. The messages are
+/// accumulated here before being printed in alphabetical order.
+typedef std::map<const std::string, Details> dictionary_type;
+dictionary_type dictionary;
+
+// The structure of the output page is:
+//
+// header
+// message
+// separator
+// message
+// separator
+// :
+// separator
+// message
+// trailer
+//
+// (Indentation is not relevant - it has only been added to the above
+// illustration to make the structure clearer.) The text of these section is:
+
+// Header - this is output before anything else.
+const std::string SEC_HEADER =
+"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
+<!DOCTYPE book PUBLIC \"-//OASIS//DTD DocBook XML V4.2//EN\"\n\
+\"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd\" [\n\
+<!ENTITY mdash \"—\" >\n\
+<!ENTITY % version SYSTEM \"version.ent\">\n\
+%version;\n\
+]>\n\
+<!--\n\
+ This XML document is generated using the system_messages.py tool\n\
+ based on the .mes message files.\n\
+\n\
+ Do not edit this file.\n\
+-->\n\
+<book>\n\
+ <?xml-stylesheet href=\"kea-guide.css\" type=\"text/css\"?>\n\
+\n\
+ <bookinfo>\n\
+ <title>Kea Messages Manual</title>\n\
+\n\
+ <copyright>\n\
+ <year>2011-2014</year><holder>Internet Systems Consortium, Inc.</holder>\n\
+ </copyright>\n\
+\n\
+ <abstract>\n\
+ <para>\n\
+ This is the messages manual for Kea version &__VERSION__;.\n\
+\t The most up-to-date version of this document, along with\n\
+\t other documents for Kea, can be found at\n\
+ <ulink url=\"http://kea.isc.org/docs\"/>.\n\
+ </para>\n\
+ </abstract>\n\
+\n\
+ <releaseinfo>This is the messages manual for Kea version\n\
+ &__VERSION__;.</releaseinfo>\n\
+ </bookinfo>\n\
+\n\
+ <chapter id=\"intro\">\n\
+ <title>Introduction</title>\n\
+ <para>\n\
+ This document lists each message that can be logged by the\n\
+ programs in the Kea package. Each entry in this manual\n\
+ is of the form:\n\
+ <screen>IDENTIFICATION message-text</screen>\n\
+ ... where \"IDENTIFICATION\" is the message identification included\n\
+ in each message logged and \"message-text\" is the accompanying\n\
+ message text. The \"message-text\" may include placeholders of the\n\
+ form \"%1\", \"%2\" etc.; these parameters are replaced by relevant\n\
+ values when the message is logged.\n\
+ </para>\n\
+ <para>\n\
+ Each entry is also accompanied by a description giving more\n\
+ information about the circumstances that result in the message\n\
+ being logged.\n\
+ </para>\n\
+ <para>\n\
+ For information on configuring and using Kea logging,\n\
+ refer to the <ulink url=\"kea-guide.html\">Kea Guide</ulink>.\n\
+ </para>\n\
+ </chapter>\n\
+\n\
+ <chapter id=\"messages\">\n\
+ <title>Kea Log Messages</title>\n\
+ <para>\n\
+ <variablelist>\n";
+
+// This is output once for each message. The string contains
+// substitution tokens: $I is replaced by the message identification,
+// $T by the message text, and $D by the message description.
+const std::string SEC_MESSAGE =
+"<varlistentry id=\"$I\">\n\
+<term>$I $T</term>\n\
+<listitem><para>\n\
+$D</para></listitem>\n\
+</varlistentry>";
+
+// A description may contain blank lines intended to separate
+// paragraphs. If so, each blank line is replaced by the following.
+const std::string SEC_BLANK = "</para><para>";
+
+// The separator is copied to the output verbatim after each message except
+// the last.
+const std::string SEC_SEPARATOR = "";
+
+// The trailier is copied to the output verbatim after the last message.
+const std::string SEC_TRAILER =
+" </variablelist>\n\
+ </para>\n\
+ </chapter>\n\
+</book>";
+
+/// Report an error and exit
+void reportError(const std::string& filename, const std::string& what)
+{
+ std::cerr << "*** ERROR in " << filename << "\n";
+ std::cerr << "*** REASON: " << what << "\n";
+ std::cerr << "*** System message generator terminating" << "\n";
+ exit(1);
+}
+
+
+/// Replaces the '<' and '>' in text about to be inserted into the template
+/// sections above with < and > to avoid problems with message text
+/// being interpreted as XML text.
+std::string replaceTag(const std::string& src)
+{
+ std::string result;
+ for (std::string::const_iterator it = src.begin(); it != src.end(); ++it) {
+ if (*it == '<') {
+ result.append("<");
+ } else if (*it == '>') {
+ result.append(">");
+ } else {
+ result.push_back(*it);
+ }
+ }
+ return result;
+}
+
+/// Replace $c in a string
+std::string replaceShell(const std::string& src, char c,
+ const std::string& val)
+{
+ std::string result;
+ bool shell = false;
+ for (std::string::const_iterator it = src.begin(); it != src.end(); ++it) {
+ if (shell) {
+ if (*it == c) {
+ result.append(val);
+ } else {
+ result.push_back('$');
+ result.push_back(*it);
+ }
+ shell = false;
+ } else if (*it == '$') {
+ shell = true;
+ } else {
+ result.push_back(*it);
+ }
+ }
+ return result;
+}
+
+/// Replaces blank lines in an array with the contents of the 'blank' section.
+lines_type replaceBlankLines(const lines_type lines)
+{
+ lines_type result;
+ for (lines_type::const_iterator l = lines.begin(); l != lines.end(); ++l) {
+ if (l->empty()) {
+ result.push_back(SEC_BLANK);
+ } else {
+ result.push_back(*l);
+ }
+ }
+ return result;
+}
+
+
+/// Printing functions
+void printHeader() {
+ std::cout << SEC_HEADER << "\n";
+}
+
+void printSeparator() {
+ std::cout << SEC_SEPARATOR << "\n";
+}
+
+void printMessage(const std::string& msgid)
+{
+ //In the message ID, replace "<" and ">" with XML-safe versions and
+ // substitute into the data.
+ const std::string m0 = SEC_MESSAGE;
+ const std::string m1 = replaceShell(m0, 'I', replaceTag(msgid));
+
+ // Do the same for the message text.
+ std::string m2 = replaceShell(m1, 'T',
+ replaceTag(dictionary[msgid].text));
+
+ // Do the same for the description then replace blank lines with the
+ // specified separator. (We do this in that order to avoid replacing
+ // the "<" and ">" in the XML tags in the separator.)
+ lines_type desc0 = dictionary[msgid].description;
+ lines_type desc1;
+ for (lines_type::iterator l = desc0.begin(); l != desc0.end(); ++l) {
+ desc1.push_back(replaceTag(*l));
+ }
+ lines_type desc2 = replaceBlankLines(desc1);
+
+ // Join the lines together to form a single string and insert into
+ // current text.
+ std::string m3;
+ for (lines_type::iterator l = desc2.begin(); l != desc2.end(); ++l) {
+ m3.append(*l);
+ m3.push_back('\n');
+ }
+
+ std::cout << replaceShell(m2, 'D', m3) << "\n";
+}
+
+void printTrailer() {
+ std::cout << SEC_TRAILER << "\n";
+}
+
+
+/// Removes leading and trailing empty lines.
+///
+/// A list of strings is passed as argument, some of which may be empty.
+/// This function removes from the start and end of list a contiguous
+/// sequence of empty lines and returns the result. Embedded sequence of
+/// empty lines are not touched.
+///
+/// Parameters:
+/// lines List of strings to be modified.
+///
+/// Return:
+/// Input list of strings with leading/trailing blank line sequences
+/// removed.
+lines_type removeEmptyLeadingTrailing(lines_type lines)
+{
+ lines_type retlines = lines;
+
+ // Dispose of degenerate case of empty array
+ if (retlines.empty()) {
+ return retlines;
+ }
+
+ // Search for first non-blank line
+ for (;;) {
+ lines_type::iterator start = retlines.begin();
+ if (start == retlines.end()) {
+ return retlines;
+ }
+ if (start->empty()) {
+ retlines.erase(start);
+ } else {
+ break;
+ }
+ }
+
+ // Search for last non-blank line
+ for (;;) {
+ lines_type::reverse_iterator finish = retlines.rbegin();
+ if (finish == retlines.rend()) {
+ return retlines;
+ }
+ if (finish->empty()) {
+ retlines.erase(retlines.end() - 1);
+ } else {
+ break;
+ }
+ }
+
+ return retlines;
+}
+
+/// Add the current message ID and associated information to the global
+/// dictionary. If a message with that ID already exists, loop appending
+/// suffixes of the form "(n)" to it until one is found that doesn't.
+///
+/// Parameters:
+/// msgid Message ID
+/// msgtext Message text
+/// desc Message description
+/// filename File from which the message came. Currently this is
+/// not used, but a future enhancement may wish to include the
+/// name of the message file in the messages manual.
+void addToDictionary(const std::string& msgid,
+ const std::string& msgtext,
+ const lines_type& desc,
+ const std::string& filename)
+{
+ // If the ID is in the dictionary, append a "(n)" to the name - this will
+ // flag that there are multiple instances. (However, this is an error -
+ // each ID should be unique in the code.)
+ std::string key = msgid;
+ if (dictionary.count(key) > 0) {
+ int i = 1;
+ std::string s = boost::lexical_cast<std::string>(i);
+ key = msgid + " (" + s + ")";
+ while (dictionary.count(key) > 0) {
+ i = i + 1;
+ s = boost::lexical_cast<std::string>(i);
+ key = msgid + " (" + s + ")";
+ }
+ }
+
+ // Remove leading and trailing blank lines in the description, then
+ // add everything into a subdictionary which is then added to the main
+ // one.
+ Details details;
+ details.text = msgtext;
+ details.description = removeEmptyLeadingTrailing(desc);
+ details.filename = filename;
+ dictionary.insert(std::pair<const std::string, Details>(key, details));
+}
+
+
+/// Processes file content. Messages and descriptions are identified and
+/// added to a dictionary (keyed by message ID). If the key already exists,
+/// a numeric suffix is added to it.
+///
+/// Parameters:
+/// filename Name of the message file being processed
+/// lines Lines read from the file
+void processFileContent(const std::string& filename,
+ const lines_type& lines)
+{
+ std::string prefix; // Last prefix encountered
+ std::string msgid; // Last message ID encountered
+ std::string msgtext; // Text of the message
+ lines_type description; // Description
+
+ for (lines_type::const_iterator l = lines.begin(); l != lines.end(); ++l) {
+ if (l->empty()) {
+ description.push_back(*l);
+ } else if (l->at(0) == '$') {
+ // Starts with "$". Ignore anything other than $PREFIX
+ char* line = new char [l->size() + 1];
+ std::strcpy(line, l->c_str());
+ char* word0 = strtok(line, " \t\r\n\t\v");
+ if (strcasecmp(word0, "$PREFIX") == 0) {
+ char* word1 = strtok(NULL, " \t\r\n\t\v");
+ prefix = word1;
+ }
+ } else if (l->at(0) == '%') {
+ // Start of a message. Add the message we were processing to the
+ // dictionary and clear everything apart from the file name.
+ if (!msgid.empty()) {
+ addToDictionary(msgid, msgtext, description, filename);
+ }
+ msgid.clear();
+ msgtext.clear();
+ description.clear();
+ // Start of a message
+ char* line = new char [l->size() + 1];
+ std::strcpy(line, l->c_str());
+ // Remove "%" and trim leading spaces
+ size_t start = l->find_first_not_of(" \t\r\n\t\v", 1);
+ if (start == std::string::npos) {
+ reportError(filename, "Line with single % found");
+ continue;
+ }
+ // Split into words. The first word is the message ID
+ char* word0 = strtok(line + start, " \t\r\n\t\v");
+ msgid = prefix;
+ msgid.append(word0);
+ std::transform(msgid.begin(), msgid.end(),
+ msgid.begin(), toupper);
+ char* word1 = strtok(NULL, " \t\r\n\t\v");
+ start = word1 - line;
+ size_t finish = l->find_last_not_of(" \t\r\n\t\v");
+ msgtext = l->substr(start, finish + 1 - start);
+ } else {
+ // Part of a description, so add to the current description array
+ description.push_back(*l);
+ }
+ }
+
+ // All done, add the last message to the global dictionaty.
+ if (!msgid.empty()) {
+ addToDictionary(msgid, msgtext, description, filename);
+ }
+}
+
+
+/// Processes a file by reading it in and stripping out all comments and
+/// and directives. Leading and trailing blank lines in the file are removed
+/// and the remainder passed for message processing.
+///
+/// Parameters:
+/// filename Name of the message file to process
+void processFile(const std::string& filename)
+{
+ std::ifstream cin;
+ cin.open(filename, std::ios::in);
+ lines_type lines0;
+ while (!cin.eof()) {
+ std::string line;
+ getline(cin, line);
+ lines0.push_back(line);
+ }
+
+ // Trim leading and trailing spaces from each line, and remove comments.
+ lines_type lines1;
+ for (lines_type::iterator l = lines0.begin(); l != lines0.end(); ++l) {
+ std::string line = *l;
+ if (line.empty()) {
+ lines1.push_back(line);
+ continue;
+ }
+ size_t start = line.find_first_not_of(" \t\r\n\t\v");
+ if (start != 0) {
+ line.erase(0, start);
+ }
+ if (line.empty()) {
+ lines1.push_back(line);
+ continue;
+ }
+ size_t finish = line.find_last_not_of(" \t\r\n\t\v");
+ if ((finish != std::string::npos) &&
+ (finish + 1 != line.size())) {
+ line.erase(finish + 1);
+ }
+ if (line.empty()) {
+ lines1.push_back(line);
+ continue;
+ }
+ if (line[0] != '#') {
+ lines1.push_back(line);
+ }
+ }
+
+ // Remove leading/trailing empty line sequences from the result
+ lines_type lines2 = removeEmptyLeadingTrailing(lines1);
+
+ // Interpret content
+ processFileContent(filename, lines2);
+}
+
+/// Iterates through all files in the tree starting at the given root and
+/// calls processFile for all .mes files found.
+///
+/// Parameters:
+/// root Directory that is the root of the source tree
+void processAllFiles(const boost::filesystem::path& root)
+{
+ boost::filesystem::directory_iterator endd;
+ for (boost::filesystem::directory_iterator file(root);
+ file != endd;
+ ++file) {
+ boost::filesystem::path path = file->path();
+ if (boost::filesystem::is_directory(path)) {
+ processAllFiles(path);
+ } else if (boost::filesystem::is_regular_file(path)) {
+ boost::filesystem::path extension = path.extension();
+ // Identify message files
+ if (extension == ".mes") {
+ processFile(path.native());
+ }
+ }
+ }
+}
+
+void usage(char* progname)
+{
+ std::cerr << "Usage: " << progname <<
+ " [--help | options] [root|files]\n";
+ std::cerr << " options: --output file: " <<
+ "output file name (default to stdout)\n";
+}
+
+int main(int argc, char* argv[])
+{
+ char* progname = argv[0];
+ std::ofstream fout;
+ while (argc > 1) {
+ --argc;
+ ++argv;
+ if (strcmp(argv[0], "--help") == 0) {
+ usage(progname);
+ exit(0);
+ }
+ // Redirect output if specified (errors are written to stderr)
+ if ((strcmp(argv[0], "-o") == 0) ||
+ (strcmp(argv[0], "--output") == 0)) {
+ --argc;
+ ++argv;
+ if (argc == 0) {
+ usage(progname);
+ exit(-1);
+ }
+ fout.open(argv[0], std::ofstream::out | std::ofstream::trunc);
+ std::cout.rdbuf(fout.rdbuf());
+ }
+ }
+
+ if (argc == 0) {
+ usage(progname);
+ exit(-1);
+ }
+ if (argc > 1) {
+ for (int i = 0; i < argc; ++i) {
+ processFile(argv[i]);
+ }
+ } else {
+ boost::filesystem::path root(argv[0]);
+ if (boost::filesystem::is_directory(root)) {
+ processAllFiles(root);
+ } else {
+ processFile(argv[0]);
+ }
+ }
+
+ // Now just print out everything we've read (in alphabetical order).
+ bool first = true;
+ printHeader();
+ for (dictionary_type::iterator it = dictionary.begin();
+ it != dictionary.end();
+ ++it) {
+ if (!first) {
+ printSeparator();
+ }
+ first = false;
+ printMessage(it->first);
+ }
+ printTrailer();
+ exit(0);
+}