first version, prototype, works sometimes v_0
authorFrantišek Kučera <franta-hg@frantovo.cz>
Sun, 14 May 2023 02:13:46 +0200
branchv_0
changeset 1 e82aaf24b0fe
parent 0 71b902e1c5ee
child 2 d4e0472e8e5d
first version, prototype, works sometimes
CMakeLists.txt
nbproject/configurations.xml
src/CLIParser.h
src/CMakeLists.txt
src/Configuration.h
src/ODSCommand.h
src/XMLDocumentConstructor.h
src/XMLTableCommand.h
src/relpipe-in-ods.cpp
--- a/CMakeLists.txt	Sun May 14 00:21:14 2023 +0200
+++ b/CMakeLists.txt	Sun May 14 02:13:46 2023 +0200
@@ -1,5 +1,5 @@
 # Relational pipes
-# Copyright © 2018 František Kučera (Frantovo.cz, GlobalCode.info)
+# Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
--- a/nbproject/configurations.xml	Sun May 14 00:21:14 2023 +0200
+++ b/nbproject/configurations.xml	Sun May 14 02:13:46 2023 +0200
@@ -42,7 +42,6 @@
   <logicalFolder name="root" displayName="root" projectFiles="true" kind="ROOT">
     <df root="." name="0">
       <df name="src">
-        <in>XMLDocumentConstructor.h</in>
         <in>relpipe-in-ods.cpp</in>
       </df>
     </df>
@@ -141,8 +140,6 @@
           <preBuildFirst>true</preBuildFirst>
         </preBuild>
       </makefileType>
-      <item path="src/XMLDocumentConstructor.h" ex="false" tool="3" flavor2="0">
-      </item>
       <item path="src/relpipe-in-ods.cpp" ex="false" tool="1" flavor2="0">
         <ccTool flags="0">
         </ccTool>
--- a/src/CLIParser.h	Sun May 14 00:21:14 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,149 +0,0 @@
-/**
- * Relational pipes
- * Copyright © 2019 František Kučera (Frantovo.cz, GlobalCode.info)
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-#pragma once
-
-#include <vector>
-
-#include <relpipe/writer/typedefs.h>
-#include <relpipe/cli/CLI.h>
-#include <relpipe/cli/RelpipeCLIException.h>
-
-#include "Configuration.h"
-
-namespace relpipe {
-namespace in {
-namespace xmltable {
-
-class CLIParser {
-private:
-
-	string_t readNext(std::vector<string_t> arguments, int& i) {
-		if (i < arguments.size()) return arguments[i++];
-		else throw relpipe::cli::RelpipeCLIException(L"Missing CLI argument" + (i > 0 ? (L" after " + arguments[i - 1]) : L""), relpipe::cli::CLI::EXIT_CODE_BAD_CLI_ARGUMENTS);
-	}
-
-	/**
-	 * TODO: use a common method
-	 */
-	bool parseBoolean(const string_t& value) {
-		if (value == L"true") return true;
-		else if (value == L"false") return false;
-		else throw relpipe::cli::RelpipeCLIException(L"Unable to parse boolean value: " + value + L" (expecting true or false)", relpipe::cli::CLI::EXIT_CODE_BAD_CLI_ARGUMENTS);
-	}
-
-	void addRelation(Configuration& c, RelationConfiguration& currentRelation) {
-		if (currentRelation.relation.size()) {
-			c.relationConfigurations.push_back(currentRelation);
-			currentRelation = RelationConfiguration();
-		}
-	}
-
-	relpipe::writer::TypeId parseTypeId(const string_t& value) {
-		using t = relpipe::writer::TypeId;
-		if (value == L"string") return t::STRING;
-		else if (value == L"integer") return t::INTEGER;
-		else if (value == L"boolean") return t::BOOLEAN;
-		else throw relpipe::cli::RelpipeCLIException(L"Unable to parse TypeId: " + value, relpipe::cli::CLI::EXIT_CODE_BAD_CLI_ARGUMENTS);
-	}
-
-public:
-
-	static const string_t OPTION_NAMESPACE;
-	static const string_t OPTION_PARSER_OPTION;
-	static const string_t OPTION_RELATION;
-	static const string_t OPTION_NAME_IS_XPATH;
-	static const string_t OPTION_RECORDS;
-	static const string_t OPTION_ATTRIBUTE;
-	static const string_t OPTION_XINCLUDE;
-	static const string_t OPTION_MODE;
-	static const string_t OPTION_RAW_XML_NODELIST_WRAPPER;
-	static const string_t OPTION_RAW_XML_ATTRIBUTE_WRAPPER;
-
-	Configuration parse(const std::vector<string_t>& arguments) {
-		Configuration c;
-		RelationConfiguration currentRelation;
-
-		for (int i = 0; i < arguments.size();) {
-			string_t option = readNext(arguments, i);
-
-			if (option == OPTION_NAMESPACE) {
-				c.namespaceMappings.push_back(readNext(arguments, i));
-				c.namespaceMappings.push_back(readNext(arguments, i));
-			} else if (option == OPTION_PARSER_OPTION) {
-				c.parserOptions.push_back({readNext(arguments, i), readNext(arguments, i)});
-			} else if (option == OPTION_XINCLUDE) {
-				c.xinclude = parseBoolean(readNext(arguments, i));
-			} else if (option == OPTION_RELATION) {
-				addRelation(c, currentRelation); // previous relation
-				currentRelation.relation = readNext(arguments, i);
-			} else if (option == OPTION_NAME_IS_XPATH) {
-				currentRelation.nameIsXPath = true;
-			} else if (option == OPTION_RECORDS) {
-				currentRelation.xpath = readNext(arguments, i);
-			} else if (option == OPTION_ATTRIBUTE) {
-				AttributeRecipe attribute;
-				attribute.mode = currentRelation.mode;
-				attribute.rawXmlNodeListWrapper = currentRelation.rawXmlNodeListWrapper;
-				attribute.rawXmlAttributeWrapper = currentRelation.rawXmlAttributeWrapper;
-				attribute.name = readNext(arguments, i);
-				attribute.type = parseTypeId(readNext(arguments, i));
-				attribute.xpath = readNext(arguments, i);
-				currentRelation.attributes.push_back(attribute);
-			} else if (option == OPTION_MODE) {
-				string_t modeName = readNext(arguments, i);
-				Mode mode;
-				if (modeName == L"string") mode = Mode::STRING;
-				else if (modeName == L"boolean") mode = Mode::BOOLEAN;
-				else if (modeName == L"raw-xml") mode = Mode::RAW_XML;
-				else if (modeName == L"line-number") mode = Mode::LINE_NUMBER;
-				else if (modeName == L"xpath") mode = Mode::XPATH;
-				else throw relpipe::cli::RelpipeCLIException(L"Unsupported mode: " + modeName, relpipe::cli::CLI::EXIT_CODE_BAD_CLI_ARGUMENTS);
-				if (currentRelation.attributes.size()) currentRelation.attributes.back().mode = mode;
-				else currentRelation.mode = mode;
-			} else if (option == OPTION_RAW_XML_NODELIST_WRAPPER) {
-				XmlElementSkeleton w = {readNext(arguments, i), readNext(arguments, i), readNext(arguments, i)};
-				if (currentRelation.attributes.size()) currentRelation.attributes.back().rawXmlNodeListWrapper = w;
-				else currentRelation.rawXmlNodeListWrapper = w;
-			} else if (option == OPTION_RAW_XML_ATTRIBUTE_WRAPPER) {
-				XmlElementSkeleton w = {readNext(arguments, i), readNext(arguments, i), readNext(arguments, i)};
-				if (currentRelation.attributes.size()) currentRelation.attributes.back().rawXmlAttributeWrapper = w;
-				else currentRelation.rawXmlAttributeWrapper = w;
-			} else throw relpipe::cli::RelpipeCLIException(L"Unsupported CLI option: " + option, relpipe::cli::CLI::EXIT_CODE_BAD_CLI_ARGUMENTS);
-		}
-		addRelation(c, currentRelation); // last relation
-
-		return c;
-	}
-
-	virtual ~CLIParser() {
-	}
-};
-
-const string_t CLIParser::OPTION_NAMESPACE = L"--namespace";
-const string_t CLIParser::OPTION_PARSER_OPTION = L"--parser-option";
-const string_t CLIParser::OPTION_RELATION = L"--relation";
-const string_t CLIParser::OPTION_NAME_IS_XPATH = L"--name-is-xpath";
-const string_t CLIParser::OPTION_RECORDS = L"--records";
-const string_t CLIParser::OPTION_ATTRIBUTE = L"--attribute";
-const string_t CLIParser::OPTION_XINCLUDE = L"--xinclude";
-const string_t CLIParser::OPTION_MODE = L"--mode";
-const string_t CLIParser::OPTION_RAW_XML_NODELIST_WRAPPER = L"--raw-xml-nodelist-wrapper";
-const string_t CLIParser::OPTION_RAW_XML_ATTRIBUTE_WRAPPER = L"--raw-xml-attribute-wrapper";
-
-}
-}
-}
--- a/src/CMakeLists.txt	Sun May 14 00:21:14 2023 +0200
+++ b/src/CMakeLists.txt	Sun May 14 02:13:46 2023 +0200
@@ -1,5 +1,5 @@
 # Relational pipes
-# Copyright © 2018 František Kučera (Frantovo.cz, GlobalCode.info)
+# Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
--- a/src/Configuration.h	Sun May 14 00:21:14 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,107 +0,0 @@
-/**
- * Relational pipes
- * Copyright © 2019 František Kučera (Frantovo.cz, GlobalCode.info)
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-#pragma once
-
-#include <vector>
-
-#include <relpipe/writer/typedefs.h>
-
-
-namespace relpipe {
-namespace in {
-namespace xmltable {
-
-enum class Mode {
-	STRING,
-	BOOLEAN,
-	// TODO: support also XML number, when we have a rational or decimal numbers in Relational pipes
-	RAW_XML,
-	LINE_NUMBER,
-	XPATH
-};
-
-class XmlElementSkeleton {
-public:
-	relpipe::writer::string_t name;
-	relpipe::writer::string_t uri;
-	relpipe::writer::string_t prefix;
-
-	XmlElementSkeleton() {
-	}
-
-	XmlElementSkeleton(relpipe::writer::string_t name, relpipe::writer::string_t uri = L"", relpipe::writer::string_t prefix = L"") : name(name), uri(uri), prefix(prefix) {
-	}
-
-	virtual ~XmlElementSkeleton() {
-	}
-};
-
-class AttributeRecipe {
-public:
-
-	virtual ~AttributeRecipe() {
-	}
-
-	relpipe::writer::string_t name;
-	relpipe::writer::TypeId type;
-	relpipe::writer::string_t xpath;
-	Mode mode = Mode::STRING;
-	XmlElementSkeleton rawXmlNodeListWrapper;
-	XmlElementSkeleton rawXmlAttributeWrapper;
-
-};
-
-class ParserOptionRecipe {
-public:
-	relpipe::writer::string_t uri;
-	relpipe::writer::string_t value;
-
-	ParserOptionRecipe(relpipe::writer::string_t uri, relpipe::writer::string_t value) : uri(uri), value(value) {
-	}
-};
-
-class RelationConfiguration {
-public:
-
-	virtual ~RelationConfiguration() {
-	}
-
-	relpipe::writer::string_t relation;
-	relpipe::writer::boolean_t nameIsXPath = false;
-	relpipe::writer::string_t xpath;
-	std::vector<AttributeRecipe> attributes;
-
-	// Defaults/templates for AttributeRecipe:	
-	Mode mode = Mode::STRING;
-	XmlElementSkeleton rawXmlNodeListWrapper;
-	XmlElementSkeleton rawXmlAttributeWrapper = {L"attribute"};
-};
-
-class Configuration {
-public:
-	std::vector<RelationConfiguration> relationConfigurations;
-	std::vector<relpipe::writer::string_t> namespaceMappings;
-	std::vector<ParserOptionRecipe> parserOptions;
-	bool xinclude = false;
-
-	virtual ~Configuration() {
-	}
-};
-
-}
-}
-}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/ODSCommand.h	Sun May 14 02:13:46 2023 +0200
@@ -0,0 +1,128 @@
+/**
+ * Relational pipes
+ * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include <cstdlib>
+#include <iostream>
+#include <string>
+#include <sstream>
+#include <vector>
+#include <algorithm>
+#include <exception>
+#include <regex>
+
+#include <libxml++-2.6/libxml++/libxml++.h>
+
+#include <relpipe/writer/typedefs.h>
+
+namespace relpipe {
+namespace in {
+namespace ods {
+
+using namespace relpipe::writer;
+
+class ODSCommand {
+private:
+	std::wstring_convert<codecvt_utf8<wchar_t>> convertor;
+
+	xmlpp::Node::PrefixNsMap ns;
+	std::shared_ptr<RelationalWriter> writer;
+
+	string_t xpath(xmlpp::Node* node, std::string xpath) {
+		return convertor.from_bytes(node->eval_to_string(xpath, ns));
+	}
+
+	void processCell(xmlpp::Node* c, AttributeMetadata& am) {
+		string_t value = xpath(c, "@o:value");
+		if (value.size()) writer->writeAttribute(value);
+		else writer->writeAttribute(xpath(c, "tx:p"));
+		// TODO: be aware of the current data types
+	}
+
+	void processRow(xmlpp::Node* r, std::vector<AttributeMetadata>& am) {
+		for (int i = 0; i < am.size(); i++) {
+			auto xpe = std::string("t:table-cell[")
+					+ std::to_string(i + 1)
+					+ "]";
+			auto cells = r->find(xpe, ns);
+			if (cells.size() == 1) {
+				processCell(cells[0], am[i]);
+			} else {
+				writer->writeAttribute(L"");
+				// TODO: support also other data types
+			}
+		}
+
+		// FIXME: support sparse data / missing values:
+		// <table:table-row  table:number-rows-repeated="2">
+		// <table:table-cell table:number-columns-repeated="3"/>
+
+	}
+
+	void processTable(xmlpp::Node* t) {
+		auto relation = xpath(t, "@t:name");
+		std::vector<AttributeMetadata> metadata;
+
+		for (xmlpp::Node* c : t->find("t:table-row[1]/t:table-cell", ns)) {
+			auto name = xpath(c, "tx:p");
+			if (name.size()) {
+				metadata.push_back({name, TypeId::STRING});
+				// TODO: detect and support other data types
+			}
+		}
+
+		writer->startRelation(relation, metadata, true);
+
+		int i = 0;
+		for (xmlpp::Node* r : t->find("t:table-row", ns)) {
+			i++;
+			if (i == 1) continue; // skip header row
+			processRow(r, metadata);
+		}
+	}
+
+public:
+
+	ODSCommand(std::shared_ptr<RelationalWriter> writer) : writer(writer) {
+		ns["o"] = "urn:oasis:names:tc:opendocument:xmlns:office:1.0";
+		ns["t"] = "urn:oasis:names:tc:opendocument:xmlns:table:1.0";
+		ns["tx"] = "urn:oasis:names:tc:opendocument:xmlns:text:1.0";
+	}
+
+	void process(std::istream & input) {
+		xmlpp::DomParser parser;
+		parser.parse_stream(input);
+
+		xmlpp::Element* root = parser.get_document()->get_root_node();
+		auto spreadsheets = root->find("/o:document/o:body/o:spreadsheet", ns);
+
+		if (spreadsheets.size() == 1) {
+			for (xmlpp::Node* table : spreadsheets[0]->find("t:table", ns)) {
+				processTable(table);
+			}
+		} else {
+			throw RelpipeWriterException(L"Invalid XML structure. "
+					"Expecting OpenDocument spreadsheet.");
+		}
+
+
+	}
+};
+
+}
+}
+}
--- a/src/XMLDocumentConstructor.h	Sun May 14 00:21:14 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,44 +0,0 @@
-/**
- * Relational pipes
- * Copyright © 2019 František Kučera (Frantovo.cz, GlobalCode.info)
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-#pragma once
-
-namespace relpipe {
-namespace in {
-namespace xmltable {
-
-#include <libxml++-2.6/libxml++/libxml++.h>
-
-class XMLDocumentConstructor {
-private:
-	std::istream* input = nullptr;
-	xmlpp::DomParser* parser = nullptr;
-public:
-
-	XMLDocumentConstructor(std::istream* input, xmlpp::DomParser* parser) : input(input), parser(parser) {
-	}
-	
-	void setOption(const std::string& uri, const std::string& value) {
-	}
-
-	void process() {
-		parser->parse_stream(*input);
-	}
-};
-
-}
-}
-}
\ No newline at end of file
--- a/src/XMLTableCommand.h	Sun May 14 00:21:14 2023 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,140 +0,0 @@
-/**
- * Relational pipes
- * Copyright © 2019 František Kučera (Frantovo.cz, GlobalCode.info)
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-#pragma once
-
-#include <cstdlib>
-#include <iostream>
-#include <string>
-#include <sstream>
-#include <vector>
-#include <algorithm>
-#include <exception>
-#include <regex>
-
-#include <libxml++-2.6/libxml++/libxml++.h>
-
-#include <relpipe/writer/typedefs.h>
-
-#include "Configuration.h"
-#include "XMLDocumentConstructor.h"
-
-namespace relpipe {
-namespace in {
-namespace xmltable {
-
-using namespace relpipe::writer;
-
-class XMLCommand {
-private:
-	std::wstring_convert<codecvt_utf8<wchar_t>> convertor; // TODO: support also other encodings.
-
-	string_t formatRawXML(string_t rawXML) {
-		std::wregex pattern(L"^(<\\?xml version=\"1.0\" encoding=\"UTF-8\"\\?>\\s*)+|\n$");
-		// libxml sometimes returns doubled XML declaration (probably a bug), see:
-		// --relation ini --records '/' --attribute 'xml' string '.' --mode raw-xml # (but not for --records '//*')
-		// so we remove all of them and also trailing line ends (if any).
-		return std::regex_replace(rawXML, pattern, L"");
-	}
-
-	void importNode(xmlpp::Node* parent, xmlpp::Node* child, AttributeRecipe attributeRecipe) {
-		if (dynamic_cast<xmlpp::AttributeNode*> (child)) parent->add_child_with_new_ns(
-				convertor.to_bytes(attributeRecipe.rawXmlAttributeWrapper.name),
-				convertor.to_bytes(attributeRecipe.rawXmlAttributeWrapper.uri),
-				convertor.to_bytes(attributeRecipe.rawXmlAttributeWrapper.prefix))->import_node(child);
-		else parent->import_node(child, true);
-	}
-
-	void importNode(xmlpp::Document* document, xmlpp::Node* child, AttributeRecipe attributeRecipe) {
-		if (dynamic_cast<xmlpp::AttributeNode*> (child)) document->create_root_node(
-				convertor.to_bytes(attributeRecipe.rawXmlAttributeWrapper.name),
-				convertor.to_bytes(attributeRecipe.rawXmlAttributeWrapper.uri),
-				convertor.to_bytes(attributeRecipe.rawXmlAttributeWrapper.prefix))->import_node(child);
-		else document->create_root_node_by_import(child, true);
-	}
-
-	string_t toRawXML(xmlpp::Node* parent, AttributeRecipe attributeRecipe, xmlpp::Node::PrefixNsMap ns) {
-		xmlpp::Document d;
-		xmlpp::NodeSet nodes = parent->find(convertor.to_bytes(attributeRecipe.xpath), ns);
-
-		if (attributeRecipe.rawXmlNodeListWrapper.name.size()) {
-			d.create_root_node(
-					convertor.to_bytes(attributeRecipe.rawXmlNodeListWrapper.name),
-					convertor.to_bytes(attributeRecipe.rawXmlNodeListWrapper.uri),
-					convertor.to_bytes(attributeRecipe.rawXmlNodeListWrapper.prefix));
-			for (xmlpp::Node* node : nodes) importNode(d.get_root_node(), node, attributeRecipe);
-		} else {
-			if (nodes.size() == 1) importNode(&d, nodes[0], attributeRecipe);
-			else if (nodes.size() > 1) throw std::invalid_argument("Multiple nodes found where only one was expected. Use nodelist wrapper."); // TODO: better relpipe exception
-			else return L""; // TODO: null
-		}
-		return formatRawXML(convertor.from_bytes(d.write_to_string()));
-	}
-
-public:
-
-	void process(std::istream& input, std::ostream& output, Configuration& configuration) {
-		std::shared_ptr<RelationalWriter> writer(Factory::create(output));
-
-		xmlpp::DomParser parser;
-		XMLDocumentConstructor documentConstructor(&input, &parser);
-		for (ParserOptionRecipe o : configuration.parserOptions) documentConstructor.setOption(convertor.to_bytes(o.uri), convertor.to_bytes(o.value));
-		documentConstructor.process();
-		if (configuration.xinclude) parser.get_document()->process_xinclude(true);
-		xmlpp::Element* root = parser.get_document()->get_root_node();
-
-		xmlpp::Node::PrefixNsMap ns;
-		for (int i = 0; i < configuration.namespaceMappings.size(); i++) {
-			std::string prefix = convertor.to_bytes(configuration.namespaceMappings[i]);
-			std::string uri = convertor.to_bytes(configuration.namespaceMappings[++i]);
-			ns[prefix] = uri;
-		}
-
-		for (const RelationConfiguration& r : configuration.relationConfigurations) {
-			std::vector<relpipe::writer::AttributeMetadata> attributesMetadata;
-			for (AttributeRecipe a : r.attributes) attributesMetadata.push_back(AttributeMetadata{a.name, a.type});
-			relpipe::writer::string_t name = r.nameIsXPath ? convertor.from_bytes(root->eval_to_string(convertor.to_bytes(r.relation), ns)) : r.relation;
-			writer->startRelation(name, attributesMetadata, true);
-			for (xmlpp::Node* n : root->find(convertor.to_bytes(r.xpath), ns)) {
-				for (AttributeRecipe a : r.attributes) {
-					// TODO: convert to bytes only once
-					std::string attributeXpath = convertor.to_bytes(a.xpath);
-					if (a.mode == Mode::STRING) {
-						writer->writeAttribute(convertor.from_bytes(n->eval_to_string(attributeXpath, ns)));
-					} else if (a.mode == Mode::BOOLEAN) {
-						writer->writeAttribute(n->eval_to_boolean(attributeXpath, ns) ? L"true" : L"false");
-					} else if (a.mode == Mode::LINE_NUMBER) {
-						xmlpp::NodeSet attributeNodes = n->find(attributeXpath, ns);
-						string_t line = attributeNodes.size() ? std::to_wstring(attributeNodes[0]->get_line()) : L""; // TODO: null
-						writer->writeAttribute(line);
-					} else if (a.mode == Mode::XPATH) {
-						xmlpp::NodeSet attributeNodes = n->find(attributeXpath, ns);
-						string_t line = attributeNodes.size() ? convertor.from_bytes(attributeNodes[0]->get_path()) : L""; // TODO: null
-						writer->writeAttribute(line);
-					} else if (a.mode == Mode::RAW_XML) {
-						writer->writeAttribute(toRawXML(n, a, ns));
-					} else {
-						throw logic_error("Unsupported mode."); // should never happer, TODO: better relpipe exception
-					}
-				}
-			}
-		}
-	}
-};
-
-}
-}
-}
--- a/src/relpipe-in-ods.cpp	Sun May 14 00:21:14 2023 +0200
+++ b/src/relpipe-in-ods.cpp	Sun May 14 02:13:46 2023 +0200
@@ -1,6 +1,6 @@
 /**
  * Relational pipes
- * Copyright © 2019 František Kučera (Frantovo.cz, GlobalCode.info)
+ * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -21,8 +21,6 @@
 #include <algorithm>
 #include <unistd.h>
 
-#include <libxml++-2.6/libxml++/libxml++.h>
-
 #include <relpipe/writer/RelationalWriter.h>
 #include <relpipe/writer/RelpipeWriterException.h>
 #include <relpipe/writer/Factory.h>
@@ -30,13 +28,11 @@
 
 #include <relpipe/cli/CLI.h>
 
-#include "XMLTableCommand.h"
-#include "CLIParser.h"
-#include "Configuration.h"
+#include "ODSCommand.h"
 
 using namespace relpipe::cli;
 using namespace relpipe::writer;
-using namespace relpipe::in::xmltable;
+using namespace relpipe::in::ods;
 
 int main(int argc, char** argv) {
 	setlocale(LC_ALL, "");
@@ -46,19 +42,14 @@
 	int resultCode = CLI::EXIT_CODE_UNEXPECTED_ERROR;
 
 	try {
-		CLIParser cliParser;
-		Configuration configuration = cliParser.parse(cli.arguments());
-		XMLCommand command;
-		command.process(cin, cout, configuration);
+		std::shared_ptr<RelationalWriter> writer(Factory::create(cout));
+		ODSCommand command(writer);
+		command.process(cin);
 		resultCode = CLI::EXIT_CODE_SUCCESS;
 	} catch (RelpipeWriterException& e) {
 		fwprintf(stderr, L"Caught Writer exception: %ls\n", e.getMessage().c_str());
 		fwprintf(stderr, L"Debug: Input stream: eof=%ls, lastRead=%d\n", (cin.eof() ? L"true" : L"false"), cin.gcount());
 		resultCode = CLI::EXIT_CODE_DATA_ERROR;
-	} catch (RelpipeCLIException& e) {
-		fwprintf(stderr, L"Caught CLI exception: %ls\n", e.getMessage().c_str());
-		fwprintf(stderr, L"Debug: Input stream: eof=%ls, lastRead=%d\n", (cin.eof() ? L"true" : L"false"), cin.gcount());
-		resultCode = e.getExitCode();
 	} // TODO: catch xmlpp::exception
 
 	return resultCode;