OSDN Git Service

e51b558fb228f13d6c3bf7a3f615f70c46eedd43
[winmerge-jp/winmerge-jp.git] / Src / DirCmpReport.cpp
1 /** 
2  * @file  DirCmpReport.cpp
3  *
4  * @brief Implementation file for DirCmpReport
5  *
6  */
7
8 #include "stdafx.h"
9 #include <ctime>
10 #include <cassert>
11 #include <sstream>
12 #include <algorithm>
13 #include <Poco/Base64Encoder.h>
14 #include "locality.h"
15 #include "DirCmpReport.h"
16 #include "paths.h"
17 #include "unicoder.h"
18 #include "markdown.h"
19 #include "CompareStats.h"
20 #include "DiffItem.h"
21 #include "DiffThread.h"
22 #include "IAbortable.h"
23
24 UINT CF_HTML = RegisterClipboardFormat(_T("HTML Format"));
25
26 /**
27  * @brief Return current time as string.
28  * @return Current time as String.
29  */
30 static String GetCurrentTimeString()
31 {
32         time_t nTime = 0;
33         time(&nTime);
34         _int64 nTime64 = nTime;
35         String str = locality::TimeString(&nTime64);
36         return str;
37 }
38
39 /**
40  * @brief Format string as beginning tag.
41  * @param [in] elName String to format as beginning tag.
42  * @return String formatted as beginning tag.
43  */
44 static String BeginEl(const String& elName, const String& attr = _T(""))
45 {
46         if (attr.empty())
47                 return strutils::format(_T("<%s>"), elName);
48         else
49                 return strutils::format(_T("<%s %s>"), elName, attr);
50 }
51
52 /**
53  * @brief Format string as ending tag.
54  * @param [in] elName String to format as ending tag.
55  * @return String formatted as ending tag.
56  */
57 static String EndEl(const String& elName)
58 {
59         return strutils::format(_T("</%s>"), elName);
60 }
61
62 /**
63  * @brief Constructor.
64  */
65 DirCmpReport::DirCmpReport(const std::vector<String> & colRegKeys)
66 : m_pList(nullptr)
67 , m_pFile(nullptr)
68 , m_nColumns(0)
69 , m_colRegKeys(colRegKeys)
70 , m_sSeparator(_T(","))
71 , m_pFileCmpReport(nullptr)
72 , m_bIncludeFileCmpReport(false)
73 , m_bOutputUTF8(false)
74 , m_myStruct(nullptr)
75 , m_bCopyToClipboard(false)
76 , m_nReportType(REPORT_TYPE_COMMALIST)
77 {
78 }
79
80 /**
81  * @brief Set UI-list pointer.
82  */
83 void DirCmpReport::SetList(IListCtrl *pList)
84 {
85         m_pList.reset(pList);
86 }
87
88 /**
89  * @brief Set root-paths of current compare so we can add them to report.
90  * @param [in] paths Root path information for the directory for which the report is generated.
91  */
92 void DirCmpReport::SetRootPaths(const PathContext &paths)
93 {
94         if (paths.GetSize() < 3)
95         {
96                 m_rootPaths.SetLeft(paths.GetLeft());
97                 m_rootPaths.SetRight(paths.GetRight());
98                 m_sTitle = strutils::format_string2(_("Compare %1 with %2"),
99                         m_rootPaths.GetLeft(), m_rootPaths.GetRight());
100         }
101         else {
102                 m_rootPaths.SetLeft(paths.GetLeft());
103                 m_rootPaths.SetMiddle(paths.GetMiddle());
104                 m_rootPaths.SetRight(paths.GetRight());
105                 m_sTitle = strutils::format_string3(_("Compare %1 with %2 and %3"),
106                         m_rootPaths.GetLeft(), m_rootPaths.GetMiddle(), m_rootPaths.GetRight());
107         }
108 }
109
110 /**
111  * @brief Set column-count.
112  */
113 void DirCmpReport::SetColumns(int columns)
114 {
115         m_nColumns = columns;
116 }
117
118 /**
119  * @brief Set file compare reporter functor
120  */
121 void DirCmpReport::SetFileCmpReport(IFileCmpReport *pFileCmpReport)
122 {
123         m_pFileCmpReport.reset(pFileCmpReport);
124 }
125
126 static ULONG GetLength32(CFile const &f)
127 {
128         ULONGLONG length = f.GetLength();
129         if (length > ULONG_MAX)
130                 length = ULONG_MAX;
131         return static_cast<ULONG>(length);
132 }
133
134 static HGLOBAL ConvertToUTF16ForClipboard(HGLOBAL hMem, int codepage)
135 {
136         size_t len = GlobalSize(hMem);
137         HGLOBAL hMemW = GlobalAlloc(GMEM_DDESHARE|GMEM_MOVEABLE|GMEM_ZEROINIT, (len + 1) * sizeof(wchar_t));
138         if (hMemW == nullptr)
139                 return nullptr;
140         LPCSTR pstr = reinterpret_cast<LPCSTR>(GlobalLock(hMem));
141         LPWSTR pwstr = reinterpret_cast<LPWSTR>(GlobalLock(hMemW));
142         if (pstr == nullptr || pwstr == nullptr)
143                 return nullptr;
144         int wlen = MultiByteToWideChar(codepage, 0, pstr, static_cast<int>(len), pwstr, static_cast<int>(len + 1));
145         if (len > 0 && pstr[len - 1] != '\0')
146         {
147                 pwstr[wlen] = 0;
148                 ++wlen;
149         }
150         GlobalUnlock(hMemW);
151         hMemW = GlobalReAlloc(hMemW, wlen * sizeof(wchar_t), 0);
152         GlobalUnlock(hMem);
153         return hMemW;
154 }
155
156 /**
157  * @brief Generate report and save it to file.
158  * @param [out] errStr Empty if succeeded, otherwise contains error message.
159  * @return `true` if report was created, `false` if user canceled report.
160  */
161 bool DirCmpReport::GenerateReport(String &errStr)
162 {
163         assert(m_pList != nullptr);
164         assert(m_pFile == nullptr);
165         bool bRet = false;
166         try
167         {
168                 if (m_bCopyToClipboard)
169                 {
170                         if (!OpenClipboard(NULL))
171                                 return false;
172                         if (!EmptyClipboard())
173                                 return false;
174                         CSharedFile file(GMEM_DDESHARE|GMEM_MOVEABLE|GMEM_ZEROINIT);
175                         m_pFile = &file;
176                         bool savedIncludeFileCmpReport = m_bIncludeFileCmpReport;
177                         m_bIncludeFileCmpReport = false;
178                         GenerateReport(m_nReportType);
179                         HGLOBAL hMem = file.Detach();
180                         SetClipboardData(CF_UNICODETEXT, ConvertToUTF16ForClipboard(hMem, m_bOutputUTF8 ? CP_UTF8 : CP_THREAD_ACP));
181                         GlobalFree(hMem);
182                         // If report type is HTML, render CF_HTML format as well
183                         if (m_nReportType == REPORT_TYPE_SIMPLEHTML)
184                         {
185                                 // Reconstruct the CSharedFile object
186                                 file.~CSharedFile();
187                                 file.CSharedFile::CSharedFile(GMEM_DDESHARE|GMEM_MOVEABLE|GMEM_ZEROINIT);
188                                 // Write preliminary CF_HTML header with all offsets zero
189                                 static const char header[] =
190                                         "Version:0.9\n"
191                                         "StartHTML:%09d\n"
192                                         "EndHTML:%09d\n"
193                                         "StartFragment:%09d\n"
194                                         "EndFragment:%09d\n";
195                                 static const char start[] = "<html><body>\n<!--StartFragment -->";
196                                 static const char end[] = "\n<!--EndFragment -->\n</body>\n</html>\n";
197                                 char buffer[MAX_PATH_FULL];
198                                 int cbHeader = wsprintfA(buffer, header, 0, 0, 0, 0);
199                                 file.Write(buffer, cbHeader);
200                                 file.Write(start, sizeof start - 1);
201                                 GenerateHTMLHeaderBodyPortion();
202                                 GenerateXmlHtmlContent(false);
203                                 file.Write(end, sizeof end); // include terminating zero
204                                 DWORD size = GetLength32(file);
205                                 // Rewrite CF_HTML header with valid offsets
206                                 file.SeekToBegin();
207                                 wsprintfA(buffer, header, cbHeader, 
208                                         static_cast<int>(size - 1),
209                                         static_cast<int>(cbHeader + sizeof start - 1),
210                                         static_cast<int>(size - sizeof end + 1));
211                                 file.Write(buffer, cbHeader);
212                                 SetClipboardData(CF_HTML, GlobalReAlloc(file.Detach(), size, 0));
213                         }
214                         CloseClipboard();
215                         m_bIncludeFileCmpReport = savedIncludeFileCmpReport;
216                 }
217                 if (!m_sReportFile.empty())
218                 {
219                         String path;
220                         paths::SplitFilename(m_sReportFile, &path, nullptr, nullptr);
221                         if (!paths::CreateIfNeeded(path))
222                         {
223                                 errStr = _("Folder does not exist.");
224                                 return false;
225                         }
226                         CFile file(m_sReportFile.c_str(),
227                                 CFile::modeWrite|CFile::modeCreate|CFile::shareDenyWrite);
228                         m_pFile = &file;
229                         GenerateReport(m_nReportType);
230                 }
231                 bRet = true;
232         }
233         catch (CException *e)
234         {
235                 e->ReportError(MB_ICONSTOP);
236                 e->Delete();
237         }
238         m_pFile = nullptr;
239         return bRet;
240 }
241
242 /**
243  * @brief Generate report of given type.
244  * @param [in] nReportType Type of report.
245  */
246 void DirCmpReport::GenerateReport(REPORT_TYPE nReportType)
247 {
248         switch (nReportType)
249         {
250         case REPORT_TYPE_SIMPLEHTML:
251                 m_bOutputUTF8 = true;
252                 GenerateHTMLHeader();
253                 GenerateXmlHtmlContent(false);
254                 GenerateHTMLFooter();
255                 break;
256         case REPORT_TYPE_SIMPLEXML:
257                 m_bOutputUTF8 = true;
258                 GenerateXmlHeader();
259                 GenerateXmlHtmlContent(true);
260                 GenerateXmlFooter();
261                 break;
262         case REPORT_TYPE_COMMALIST:
263                 m_bOutputUTF8 = false;
264                 m_sSeparator = _T(",");
265                 GenerateHeader();
266                 GenerateContent();
267                 break;
268         case REPORT_TYPE_TABLIST:
269                 m_bOutputUTF8 = false;
270                 m_sSeparator = _T("\t");
271                 GenerateHeader();
272                 GenerateContent();
273                 break;
274         }
275 }
276
277 /**
278  * @brief Write text to report file.
279  * @param [in] pszText Text to write to report file.
280  */
281 void DirCmpReport::WriteString(const String& sText)
282 {
283         std::string sOctets(m_bOutputUTF8 ? ucr::toUTF8(sText) : ucr::toThreadCP(sText));
284         const char *pchOctets = sOctets.c_str();
285         size_t cchAhead = sOctets.length();
286         while (const char *pchAhead = (const char *)memchr(pchOctets, '\n', cchAhead))
287         {
288                 size_t cchLine = pchAhead - pchOctets;
289                 m_pFile->Write(pchOctets, static_cast<unsigned>(cchLine));
290                 static const char eol[] = { '\r', '\n' };
291                 m_pFile->Write(eol, sizeof eol);
292                 ++cchLine;
293                 pchOctets += cchLine;
294                 cchAhead -= cchLine;
295         }
296         m_pFile->Write(pchOctets, static_cast<unsigned>(cchAhead));
297
298 }
299
300 /**
301  * @brief Write text to report file while turning special chars to entities.
302  * @param [in] sText Text to write to report file.
303  */
304 void DirCmpReport::WriteStringEntityAware(const String& sText)
305 {
306         WriteString(ucr::toTString(CMarkdown::Entities(ucr::toUTF8(sText))));
307 }
308
309 /**
310  * @brief Generate header-data for report.
311  */
312 void DirCmpReport::GenerateHeader()
313 {
314         WriteString(m_sTitle);
315         WriteString(_T("\n"));
316         WriteString(GetCurrentTimeString());
317         WriteString(_T("\n"));
318         for (int currCol = 0; currCol < m_nColumns; currCol++)
319         {
320                 WriteString(m_pList->GetColumnName(currCol));
321                 // Add col-separator, but not after last column
322                 if (currCol < m_nColumns - 1)
323                         WriteString(m_sSeparator);
324         }
325 }
326
327 /**
328  * @brief Generate report content (compared items).
329  */
330 void DirCmpReport::GenerateContent()
331 {
332         int nRows = m_pList->GetRowCount();
333
334         // Report:Detail. All currently displayed columns will be added
335         for (int currRow = 0; currRow < nRows; currRow++)
336         {
337                 if (m_myStruct && m_myStruct->context->GetAbortable()->ShouldAbort())
338                         break;
339                 WriteString(_T("\n"));
340                 DIFFITEM* pdi = reinterpret_cast<DIFFITEM*>(m_pList->GetItemData(currRow));
341                 if (reinterpret_cast<uintptr_t>(pdi) == -1)
342                         continue;
343                 if (m_myStruct)
344                         m_myStruct->context->m_pCompareStats->BeginCompare(pdi, 0);
345                 for (int currCol = 0; currCol < m_nColumns; currCol++)
346                 {
347                         String value = m_pList->GetItemText(currRow, currCol);
348                         if (value.find(m_sSeparator) != String::npos) {
349                                 WriteString(_T("\""));
350                                 WriteString(value);
351                                 WriteString(_T("\""));
352                         }
353                         else
354                                 WriteString(value);
355
356                         // Add col-separator, but not after last column
357                         if (currCol < m_nColumns - 1)
358                                 WriteString(m_sSeparator);
359                 }
360                 if (m_myStruct)
361                         m_myStruct->context->m_pCompareStats->AddItem(-1);
362         }
363
364 }
365
366 /**
367  * @brief Generate simple html report header.
368  */
369 void DirCmpReport::GenerateHTMLHeader()
370 {
371         WriteString(_T("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"\n")
372                 _T("\t\"http://www.w3.org/TR/html4/loose.dtd\">\n")
373                 _T("<html>\n<head>\n")
374                 _T("\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n")
375                 _T("\t<title>"));
376         WriteStringEntityAware(m_sTitle);
377         WriteString(_T("</title>\n"));
378         WriteString(_T("\t<style type=\"text/css\">\n\t<!--\n"));
379         WriteString(_T("\t\tth {\n"));
380         WriteString(_T("\t\t\tposition: sticky; top: 0;\n"));
381         WriteString(_T("\t\t}\n"));
382         WriteString(_T("\t\tbody {\n"));
383         WriteString(_T("\t\t\tfont-family: sans-serif;\n"));
384         WriteString(_T("\t\t\tfont-size: smaller;\n"));
385         WriteString(_T("\t\t}\n"));
386         WriteString(_T("\t\ttable {\n"));
387         WriteString(_T("\t\t\tborder-collapse: collapse;\n"));
388         WriteString(_T("\t\t\tborder: 1px solid gray;\n"));
389         WriteString(_T("\t\t}\n"));
390         WriteString(_T("\t\tth,td {\n"));
391         WriteString(_T("\t\t\tpadding: 3px;\n"));
392         WriteString(_T("\t\t\ttext-align: left;\n"));
393         WriteString(_T("\t\t\tvertical-align: middle;\n"));
394         WriteString(_T("\t\t\tborder: 1px solid gray;\n"));
395         WriteString(_T("\t\t}\n"));
396         WriteString(_T("\t\tth {\n"));
397         WriteString(_T("\t\t\tcolor: white;\n"));
398         WriteString(_T("\t\t\tbackground: blue;\n"));
399         WriteString(_T("\t\t\tpadding: 4px 4px;\n"));
400         WriteString(_T("\t\t\tbackground: linear-gradient(mediumblue, darkblue);\n"));
401         WriteString(_T("\t\t}\n"));
402
403         std::vector<bool> usedIcon(m_pList->GetIconCount());
404         int maxIndent = 0;
405         for (int i = 0; i < m_pList->GetRowCount(); ++i)
406         {
407                 usedIcon[m_pList->GetIconIndex(i)] = true;
408                 maxIndent = (std::max)(m_pList->GetIndent(i), maxIndent);
409         }
410         for (int i = 0; i < m_pList->GetIconCount(); ++i)
411         {
412                 if (usedIcon[i])
413                 {
414                         std::ostringstream stream;
415                         Poco::Base64Encoder enc(stream);
416                         enc.rdbuf()->setLineLength(0);
417                         enc << m_pList->GetIconPNGData(i);
418                         enc.close();
419                         WriteString(strutils::format(_T("\t\t.icon%d { background-image: url('data:image/png;base64,%s'); background-repeat: no-repeat; background-size: 16px 16px; }\n"), i, ucr::toTString(stream.str())));
420                 }
421         }
422         for (int i = 0; i < maxIndent + 1; ++i)
423                 WriteString(strutils::format(_T("\t\t.indent%d { padding-left: %dpx; background-position: %dpx center; }\n"), i, 2 * 2 + 16 + 8 * i, 2 + 8 * i));
424         WriteString(_T("\t-->\n\t</style>\n"));
425         WriteString(_T("</head>\n<body>\n"));
426         GenerateHTMLHeaderBodyPortion();
427 }
428
429 /**
430  * @brief Generate body portion of simple html report header (w/o body tag).
431  */
432 void DirCmpReport::GenerateHTMLHeaderBodyPortion()
433 {
434         WriteString(_T("<h2>"));
435         WriteString(m_sTitle);
436         WriteString(_T("</h2>\n<p>"));
437         WriteString(GetCurrentTimeString());
438         WriteString(_T("</p>\n"));
439         WriteString(_T("<table border=\"1\">\n<tr>\n"));
440
441         for (int currCol = 0; currCol < m_nColumns; currCol++)
442         {
443                 WriteString(_T("<th>"));
444                 WriteStringEntityAware(m_pList->GetColumnName(currCol));
445                 WriteString(_T("</th>"));
446         }
447         WriteString(_T("</tr>\n"));
448 }
449
450 /**
451  * @brief Generate simple xml report header.
452  */
453 void DirCmpReport::GenerateXmlHeader()
454 {
455         WriteString(_T("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
456                                 _T("<WinMergeDiffReport version=\"2\">\n")
457                                 _T("<left>"));
458         WriteStringEntityAware(m_rootPaths.GetLeft());
459         WriteString(_T("</left>\n"));
460         if (m_rootPaths.GetSize() == 3)
461         {
462                 WriteString(_T("<middle>"));
463                 WriteStringEntityAware(m_rootPaths.GetMiddle());
464                 WriteString(_T("</middle>\n"));
465         }
466         WriteString(_T("<right>"));
467         WriteStringEntityAware(m_rootPaths.GetRight());
468         WriteString(_T("</right>\n")
469                                 _T("<time>"));
470         WriteStringEntityAware(GetCurrentTimeString());
471         WriteString(_T("</time>\n"));
472
473         // Add column headers
474         const String rowEl = _T("column_name");
475         WriteString(BeginEl(rowEl));
476         for (int currCol = 0; currCol < m_nColumns; currCol++)
477         {
478                 const String colEl = m_colRegKeys[currCol];
479                 WriteString(BeginEl(colEl));
480                 WriteStringEntityAware(m_pList->GetColumnName(currCol));
481                 WriteString(EndEl(colEl));
482         }
483         WriteString(EndEl(rowEl) + _T("\n"));
484 }
485
486 /**
487  * @brief Generate simple html or xml report content.
488  */
489 void DirCmpReport::GenerateXmlHtmlContent(bool xml)
490 {
491         String sFileName, sParentDir;
492         paths::SplitFilename((const tchar_t *)m_pFile->GetFilePath(), &sParentDir, &sFileName, nullptr);
493         String sRelDestDir = sFileName.substr(0, sFileName.find_last_of(_T('.'))) + _T(".files");
494         String sDestDir = paths::ConcatPath(sParentDir, sRelDestDir);
495         if (!xml && m_bIncludeFileCmpReport && m_pFileCmpReport != nullptr)
496                 paths::CreateIfNeeded(sDestDir);
497
498         int nRows = m_pList->GetRowCount();
499
500         // Report:Detail. All currently displayed columns will be added
501         for (int currRow = 0; currRow < nRows; currRow++)
502         {
503                 if (m_myStruct && m_myStruct->context->GetAbortable()->ShouldAbort())
504                         break;
505                 DIFFITEM* pdi = reinterpret_cast<DIFFITEM*>(m_pList->GetItemData(currRow));
506                 if (reinterpret_cast<uintptr_t>(pdi) == -1)
507                         continue;
508                 String sLinkPath;
509                 if (m_myStruct)
510                         m_myStruct->context->m_pCompareStats->BeginCompare(pdi, 0);
511                 if (!xml && m_bIncludeFileCmpReport && m_pFileCmpReport != nullptr)
512                         (*m_pFileCmpReport.get())(REPORT_TYPE_SIMPLEHTML, m_pList.get(), currRow, sDestDir, sLinkPath);
513
514                 strutils::replace(sLinkPath, _T("%"), _T("%25"));
515                 strutils::replace(sLinkPath, _T("#"), _T("%23"));
516
517                 String rowEl = _T("tr");
518                 if (xml)
519                 {
520                         rowEl = _T("filediff");
521                         WriteString(BeginEl(rowEl));
522                 }
523                 else
524                 {
525                         COLORREF backcolor = m_pList->GetBackColor(currRow);
526                         COLORREF textcolor = m_pList->GetTextColor(currRow);
527                         String attr = strutils::format(_T("style='%sbackground-color: #%02x%02x%02x'"),
528                                 textcolor == 0 ? _T("") : strutils::format(_T("color: #%02x%02x%02x; "),
529                                                 GetRValue(textcolor), GetGValue(textcolor), GetBValue(textcolor)).c_str(),
530                                 GetRValue(backcolor), GetGValue(backcolor), GetBValue(backcolor));
531                         WriteString(BeginEl(rowEl, attr));
532                 }
533                 for (int currCol = 0; currCol < m_nColumns; currCol++)
534                 {
535                         String colEl = _T("td");
536                         if (xml)
537                         {
538                                 colEl = m_colRegKeys[currCol];
539                                 WriteString(BeginEl(colEl));
540                         }
541                         else
542                         {
543                                 if (currCol == 0)
544                                         WriteString(BeginEl(colEl, strutils::format(_T("class=\"icon%d indent%d\""), m_pList->GetIconIndex(currRow), m_pList->GetIndent(currRow))));
545                                 else
546                                         WriteString(BeginEl(colEl));
547                         }
548                         if (currCol == 0 && !sLinkPath.empty())
549                         {
550                                 WriteString(_T("<a href=\""));
551                                 WriteString(sRelDestDir);
552                                 WriteString(_T("/"));
553                                 WriteString(sLinkPath);
554                                 WriteString(_T("\">"));
555                                 WriteStringEntityAware(m_pList->GetItemText(currRow, currCol));
556                                 WriteString(_T("</a>"));
557                         }
558                         else
559                         {
560                                 WriteStringEntityAware(m_pList->GetItemText(currRow, currCol));
561                         }
562                         WriteString(EndEl(colEl));
563                 }
564                 WriteString(EndEl(rowEl) + _T("\n"));
565                 if (m_myStruct)
566                         m_myStruct->context->m_pCompareStats->AddItem(-1);
567         }
568         if (!xml)
569                 WriteString(_T("</table>\n"));
570 }
571
572 /**
573  * @brief Generate simple html report footer.
574  */
575 void DirCmpReport::GenerateHTMLFooter()
576 {
577         WriteString(_T("</body>\n</html>\n"));
578 }
579
580 /**
581  * @brief Generate simple xml report header.
582  */
583 void DirCmpReport::GenerateXmlFooter()
584 {
585         WriteString(_T("</WinMergeDiffReport>\n"));
586 }
587