OSDN Git Service

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