1 // SPDX-License-Identifier: GPL-2.0-or-later
3 * @file ProjectFile.cpp
5 * @brief Implementation file for ProjectFile class.
9 #include "ProjectFile.h"
12 #include <Poco/FileStream.h>
13 #include <Poco/XML/XMLWriter.h>
14 #include <Poco/SAX/SAXParser.h>
15 #include <Poco/SAX/ContentHandler.h>
16 #include <Poco/Exception.h>
17 #include "UnicodeString.h"
20 using Poco::FileStream;
21 using Poco::XML::SAXParser;
22 using Poco::XML::ContentHandler;
23 using Poco::XML::Locator;
24 using Poco::XML::XMLWriter;
25 using Poco::XML::XMLChar;
26 using Poco::XML::XMLString;
27 using Poco::XML::Attributes;
28 using Poco::Exception;
32 // Constants for xml element names
33 const char Root_element_name[] = "project";
34 const char Paths_element_name[] = "paths";
35 const char Left_element_name[] = "left";
36 const char Middle_element_name[] = "middle";
37 const char Right_element_name[] = "right";
38 const char Left_desc_element_name[] = "left-desc";
39 const char Middle_desc_element_name[] = "middle-desc";
40 const char Right_desc_element_name[] = "right-desc";
41 const char Filter_element_name[] = "filter";
42 const char Subfolders_element_name[] = "subfolders";
43 const char Left_ro_element_name[] = "left-readonly";
44 const char Middle_ro_element_name[] = "middle-readonly";
45 const char Right_ro_element_name[] = "right-readonly";
46 const char Unpacker_element_name[] = "unpacker";
47 const char Prediffer_element_name[] = "prediffer";
48 const char Window_type_element_name[] = "window-type";
49 const char Table_delimiter_element_name[] = "table-delimiter";
50 const char Table_quote_element_name[] = "table-quote";
51 const char Table_allownewlinesinquotes_element_name[] = "table-allownewlinesinquotes";
52 const char White_spaces_element_name[] = "white-spaces";
53 const char Ignore_blank_lines_element_name[] = "ignore-blank-lines";
54 const char Ignore_case_element_name[] = "ignore-case";
55 const char Ignore_cr_diff_element_name[] = "ignore-carriage-return-diff";
56 const char Ignore_numbers_element_name[] = "ignore-numbers";
57 const char Ignore_codepage_diff_element_name[] = "ignore-codepage-diff";
58 const char Ignore_comment_diff_element_name[] = "ignore-comment-diff";
59 const char Compare_method_element_name[] = "compare-method";
60 const char Hidden_list_element_name[] = "hidden-list";
61 const char Hidden_items_element_name[] = "hidden-item";
66 String xmlch2tstr(const XMLChar *ch, int length)
68 return toTString(std::string(ch, length));
71 void writeElement(XMLWriter& writer, const std::string& tagname, const std::string& characters)
73 writer.startElement("", "", tagname);
74 writer.characters(characters);
75 writer.endElement("", "", tagname);
78 void saveHiddenItems(XMLWriter& writer, const std::vector<String>& hiddenItems)
80 writer.startElement("", "", Hidden_list_element_name);
81 for (const auto& hiddenItem : hiddenItems) {
82 writeElement(writer, Hidden_items_element_name, toUTF8(hiddenItem));
84 writer.endElement("", "", Hidden_list_element_name);
89 class ProjectFileHandler: public ContentHandler
92 explicit ProjectFileHandler(std::list<ProjectFileItem> *pProject) : m_pProject(pProject) {}
94 void setDocumentLocator(const Locator* loc) {}
95 void startDocument() {}
97 void startElement(const XMLString& uri, const XMLString& localName, const XMLString& qname, const Attributes& attributes)
99 if (localName == Paths_element_name)
100 m_pProject->push_back(ProjectFileItem{});
101 m_stack.push(localName);
103 void endElement(const XMLString& uri, const XMLString& localName, const XMLString& qname)
107 void characters(const XMLChar ch[], int start, int length)
109 //process .winmerge entries of third level deep only
110 if (m_stack.size() != 3 && m_pProject->size() == 0)
113 ProjectFileItem& currentItem = m_pProject->back();
115 const std::string& nodename = m_stack.top();
116 const std::string& token = std::string(ch + start, length);
118 if (nodename == Left_element_name)
120 currentItem.m_paths.SetLeft(currentItem.m_paths.GetLeft() + xmlch2tstr(ch + start, length), false);
121 currentItem.m_bHasLeft = true;
123 else if (nodename == Middle_element_name)
125 currentItem.m_paths.SetMiddle(currentItem.m_paths.GetMiddle() + xmlch2tstr(ch + start, length), false);
126 currentItem.m_bHasMiddle = true;
128 else if (nodename == Right_element_name)
130 currentItem.m_paths.SetRight(currentItem.m_paths.GetRight() + xmlch2tstr(ch + start, length), false);
131 currentItem.m_bHasRight = true;
133 else if (nodename == Left_desc_element_name)
135 currentItem.m_leftDesc += xmlch2tstr(ch + start, length);
136 currentItem.m_bHasLeftDesc = true;
138 else if (nodename == Middle_desc_element_name)
140 currentItem.m_middleDesc += xmlch2tstr(ch + start, length);
141 currentItem.m_bHasMiddleDesc = true;
143 else if (nodename == Right_desc_element_name)
145 currentItem.m_rightDesc += xmlch2tstr(ch + start, length);
146 currentItem.m_bHasRightDesc = true;
148 else if (nodename == Filter_element_name)
150 currentItem.m_filter += xmlch2tstr(ch + start, length);
151 currentItem.m_bHasFilter = true;
153 else if (nodename == Subfolders_element_name)
155 currentItem.m_subfolders = atoi(token.c_str());
156 currentItem.m_bHasSubfolders = true;
158 else if (nodename == Left_ro_element_name)
160 currentItem.m_bLeftReadOnly = atoi(token.c_str()) != 0;
162 else if (nodename == Middle_ro_element_name)
164 currentItem.m_bMiddleReadOnly = atoi(token.c_str()) != 0;
166 else if (nodename == Right_ro_element_name)
168 currentItem.m_bRightReadOnly = atoi(token.c_str()) != 0;
170 else if (nodename == Unpacker_element_name)
172 currentItem.m_unpacker += xmlch2tstr(ch + start, length);
173 currentItem.m_bHasUnpacker = true;
175 else if (nodename == Prediffer_element_name)
177 currentItem.m_prediffer += xmlch2tstr(ch + start, length);
178 currentItem.m_bHasPrediffer = true;
180 else if (nodename == Window_type_element_name)
182 currentItem.m_nWindowType = atoi(token.c_str());
183 currentItem.m_bHasWindowType = true;
185 else if (nodename == Table_delimiter_element_name)
187 currentItem.m_cTableDelimiter = token.c_str()[0];
188 currentItem.m_bHasTableDelimiter = true;
190 else if (nodename == Table_quote_element_name)
192 currentItem.m_cTableQuote = token.c_str()[0];
193 currentItem.m_bHasTableQuote = true;
195 else if (nodename == Table_allownewlinesinquotes_element_name)
197 currentItem.m_bTableAllowNewLinesInQuotes = atoi(token.c_str());
198 currentItem.m_bHasTableAllowNewLinesInQuotes = true;
200 else if (nodename == White_spaces_element_name)
202 currentItem.m_nIgnoreWhite = atoi(token.c_str());
203 currentItem.m_bHasIgnoreWhite = true;
205 else if (nodename == Ignore_blank_lines_element_name)
207 currentItem.m_bIgnoreBlankLines = atoi(token.c_str()) != 0;
208 currentItem.m_bHasIgnoreBlankLines = true;
210 else if (nodename == Ignore_case_element_name)
212 currentItem.m_bIgnoreCase = atoi(token.c_str()) != 0;
213 currentItem.m_bHasIgnoreCase = true;
215 else if (nodename == Ignore_cr_diff_element_name)
217 currentItem.m_bIgnoreEol = atoi(token.c_str()) != 0;
218 currentItem.m_bHasIgnoreEol = true;
220 else if (nodename == Ignore_numbers_element_name)
222 currentItem.m_bIgnoreNumbers = atoi(token.c_str()) != 0;
223 currentItem.m_bHasIgnoreNumbers = true;
225 else if (nodename == Ignore_codepage_diff_element_name)
227 currentItem.m_bIgnoreCodepage = atoi(token.c_str()) != 0;
228 currentItem.m_bHasIgnoreCodepage = true;
230 else if (nodename == Ignore_comment_diff_element_name)
232 currentItem.m_bFilterCommentsLines = atoi(token.c_str()) != 0;
233 currentItem.m_bHasFilterCommentsLines = true;
235 else if (nodename == Compare_method_element_name)
237 currentItem.m_nCompareMethod = atoi(token.c_str());
238 currentItem.m_bHasCompareMethod = true;
240 //This nodes are under Hidden_list_element_name
241 else if (nodename == Hidden_items_element_name)
243 currentItem.m_vSavedHiddenItems.push_back(toTString(token));
244 currentItem.m_bHasHiddenItems = true;
247 void ignorableWhitespace(const XMLChar ch[], int start, int length) {}
248 void processingInstruction(const XMLString& target, const XMLString& data) {}
249 void startPrefixMapping(const XMLString& prefix, const XMLString& uri) {}
250 void endPrefixMapping(const XMLString& prefix) {}
251 void skippedEntity(const XMLString& name) {}
254 std::list<ProjectFileItem> *m_pProject = nullptr;
255 std::stack<std::string> m_stack;
258 /** @brief File extension for path files */
259 const String ProjectFile::PROJECTFILE_EXT = toTString("WinMerge");
262 * @brief Standard constructor.
264 ProjectFileItem::ProjectFileItem()
266 , m_bHasMiddle(false)
268 , m_bHasLeftDesc(false)
269 , m_bHasMiddleDesc(false)
270 , m_bHasRightDesc(false)
271 , m_bHasFilter(false)
272 , m_bHasSubfolders(false)
273 , m_bHasUnpacker(false)
274 , m_bHasPrediffer(false)
276 , m_bLeftReadOnly(false)
277 , m_bMiddleReadOnly(false)
278 , m_bRightReadOnly(false)
279 , m_bHasWindowType(false)
281 , m_bHasTableDelimiter(false)
282 , m_cTableDelimiter(',')
283 , m_bHasTableQuote(false)
284 , m_cTableQuote('\"')
285 , m_bHasTableAllowNewLinesInQuotes(false)
286 , m_bTableAllowNewLinesInQuotes(true)
287 , m_bHasIgnoreWhite(false)
289 , m_bHasIgnoreBlankLines(false)
290 , m_bIgnoreBlankLines(false)
291 , m_bHasIgnoreCase(false)
292 , m_bIgnoreCase(false)
293 , m_bHasIgnoreEol(false)
294 , m_bIgnoreEol(false)
295 , m_bHasIgnoreNumbers(false)
296 , m_bIgnoreNumbers(false)
297 , m_bHasIgnoreCodepage(false)
298 , m_bIgnoreCodepage(false)
299 , m_bHasFilterCommentsLines(false)
300 , m_bFilterCommentsLines(false)
301 , m_bHasCompareMethod(false)
302 , m_nCompareMethod(0)
303 , m_bHasHiddenItems(false)
304 , m_bSaveFilter(true)
305 , m_bSaveSubfolders(true)
306 , m_bSaveUnpacker(true)
307 , m_bSavePrediffer(true)
308 , m_bSaveIgnoreWhite(true)
309 , m_bSaveIgnoreBlankLines(true)
310 , m_bSaveIgnoreCase(true)
311 , m_bSaveIgnoreEol(true)
312 , m_bSaveIgnoreNumbers(true)
313 , m_bSaveIgnoreCodepage(true)
314 , m_bSaveFilterCommentsLines(true)
315 , m_bSaveCompareMethod(true)
316 , m_bSaveHiddenItems(true)
321 * @brief Returns left path.
322 * @param [out] pReadOnly true if readonly was specified for path.
325 String ProjectFileItem::GetLeft(bool * pReadOnly /*= nullptr*/) const
327 if (pReadOnly != nullptr)
328 *pReadOnly = m_bLeftReadOnly;
329 return m_paths.GetLeft();
333 * @brief Set left path, returns old left path.
334 * @param [in] sLeft Left path.
335 * @param [in] bReadOnly Will path be recorded read-only?
337 void ProjectFileItem::SetLeft(const String& sLeft, const bool * pReadOnly /*= nullptr*/)
339 m_paths.SetLeft(sLeft, false);
340 if (pReadOnly != nullptr)
341 m_bLeftReadOnly = *pReadOnly;
345 * @brief Returns middle path.
346 * @param [out] pReadOnly true if readonly was specified for path.
348 String ProjectFileItem::GetMiddle(bool * pReadOnly /*= nullptr*/) const
350 if (pReadOnly != nullptr)
351 *pReadOnly = m_bMiddleReadOnly;
352 return m_paths.GetMiddle();
356 * @brief Set middle path, returns old middle path.
357 * @param [in] sMiddle Middle path.
358 * @param [in] bReadOnly Will path be recorded read-only?
360 void ProjectFileItem::SetMiddle(const String& sMiddle, const bool * pReadOnly /*= nullptr*/)
362 m_paths.SetMiddle(sMiddle, false);
363 if (pReadOnly != nullptr)
364 m_bMiddleReadOnly = *pReadOnly;
370 * @brief Returns right path.
371 * @param [out] pReadOnly true if readonly was specified for path.
372 * @return Right path.
374 String ProjectFileItem::GetRight(bool * pReadOnly /*= nullptr*/) const
376 if (pReadOnly != nullptr)
377 *pReadOnly = m_bRightReadOnly;
378 return m_paths.GetRight();
382 * @brief Set right path, returns old right path.
383 * @param [in] sRight Right path.
384 * @param [in] bReadOnly Will path be recorded read-only?
386 void ProjectFileItem::SetRight(const String& sRight, const bool * pReadOnly /*= nullptr*/)
388 m_paths.SetRight(sRight, false);
389 if (pReadOnly != nullptr)
390 m_bRightReadOnly = *pReadOnly;
394 * @brief Returns left and right paths and recursive from project file
396 * @param [out] files Files in project
397 * @param [out] bSubFolders If true subfolders included (recursive compare)
399 void ProjectFileItem::GetPaths(PathContext& files, bool & bSubfolders) const
403 bSubfolders = (GetSubfolders() == 1);
407 * @brief Open given path-file and read data from it to member variables.
408 * @param [in] path Path to project file.
409 * @param [out] sError Error string if error happened.
410 * @return true if reading succeeded, false if error happened.
412 bool ProjectFile::Read(const String& path)
414 ProjectFileHandler handler(&m_items);
416 parser.setContentHandler(&handler);
417 parser.parse(toUTF8(path));
422 * @brief Save data from member variables to path-file.
423 * @param [in] path Path to project file.
424 * @param [out] sError Error string if error happened.
425 * @return true if saving succeeded, false if error happened.
427 bool ProjectFile::Save(const String& path) const
429 FileStream out(toUTF8(path), FileStream::trunc);
430 XMLWriter writer(out, XMLWriter::WRITE_XML_DECLARATION | XMLWriter::PRETTY_PRINT);
431 writer.startDocument();
432 writer.startElement("", "", Root_element_name);
434 for (auto& item : m_items)
436 writer.startElement("", "", Paths_element_name);
438 if (!item.m_paths.GetLeft().empty())
439 writeElement(writer, Left_element_name, toUTF8(item.m_paths.GetLeft()));
440 if (!item.m_paths.GetMiddle().empty())
441 writeElement(writer, Middle_element_name, toUTF8(item.m_paths.GetMiddle()));
442 if (!item.m_paths.GetRight().empty())
443 writeElement(writer, Right_element_name, toUTF8(item.m_paths.GetRight()));
444 if (!item.m_leftDesc.empty())
445 writeElement(writer, Left_desc_element_name, toUTF8(item.m_leftDesc));
446 if (!item.m_middleDesc.empty())
447 writeElement(writer, Middle_desc_element_name, toUTF8(item.m_middleDesc));
448 if (!item.m_rightDesc.empty())
449 writeElement(writer, Right_desc_element_name, toUTF8(item.m_rightDesc));
450 if (item.m_bSaveFilter && !item.m_filter.empty())
451 writeElement(writer, Filter_element_name, toUTF8(item.m_filter));
452 if (item.m_bSaveSubfolders)
453 writeElement(writer, Subfolders_element_name, item.m_subfolders != 0 ? "1" : "0");
454 writeElement(writer, Left_ro_element_name, item.m_bLeftReadOnly ? "1" : "0");
455 if (!item.m_paths.GetMiddle().empty())
456 writeElement(writer, Middle_ro_element_name, item.m_bMiddleReadOnly ? "1" : "0");
457 writeElement(writer, Right_ro_element_name, item.m_bRightReadOnly ? "1" : "0");
458 if (item.m_bSaveUnpacker && !item.m_unpacker.empty())
459 writeElement(writer, Unpacker_element_name, toUTF8(item.m_unpacker));
460 if (item.m_bSavePrediffer && !item.m_prediffer.empty())
461 writeElement(writer, Prediffer_element_name, toUTF8(item.m_prediffer));
462 if (item.m_nWindowType != -1)
463 writeElement(writer, Window_type_element_name, std::to_string(item.m_nWindowType));
464 if (item.m_nWindowType == 2 /* table */)
466 writeElement(writer, Table_delimiter_element_name, toUTF8(String(&item.m_cTableDelimiter, 1)));
467 writeElement(writer, Table_quote_element_name, toUTF8(String(&item.m_cTableQuote, 1)));
468 writeElement(writer, Table_allownewlinesinquotes_element_name, item.m_bTableAllowNewLinesInQuotes ? "1" : "0");
470 if (item.m_bSaveIgnoreWhite)
471 writeElement(writer, White_spaces_element_name, std::to_string(item.m_nIgnoreWhite));
472 if (item.m_bSaveIgnoreBlankLines)
473 writeElement(writer, Ignore_blank_lines_element_name, item.m_bIgnoreBlankLines ? "1" : "0");
474 if (item.m_bSaveIgnoreCase)
475 writeElement(writer, Ignore_case_element_name, item.m_bIgnoreCase ? "1" : "0");
476 if (item.m_bSaveIgnoreEol)
477 writeElement(writer, Ignore_cr_diff_element_name, item.m_bIgnoreEol ? "1" : "0");
478 if (item.m_bSaveIgnoreNumbers)
479 writeElement(writer, Ignore_numbers_element_name, item.m_bIgnoreNumbers ? "1" : "0");
480 if (item.m_bSaveIgnoreCodepage)
481 writeElement(writer, Ignore_codepage_diff_element_name, item.m_bIgnoreCodepage ? "1" : "0");
482 if (item.m_bSaveFilterCommentsLines)
483 writeElement(writer, Ignore_comment_diff_element_name, item.m_bFilterCommentsLines ? "1" : "0");
484 if (item.m_bSaveCompareMethod)
485 writeElement(writer, Compare_method_element_name, std::to_string(item.m_nCompareMethod));
486 if (item.m_bSaveHiddenItems && item.m_vSavedHiddenItems.size() > 0)
487 saveHiddenItems(writer, item.m_vSavedHiddenItems);
489 writer.endElement("", "", Paths_element_name);
492 writer.endElement("", "", Root_element_name);
493 writer.endDocument();