/* Copyright (C) 2007 J.F.Dockes * 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; either version 2 of the License, or * (at your option) any later version. * * 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, write to the * Free Software Foundation, Inc., * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #ifdef HAVE_CONFIG_H #include "autoconfig.h" #endif #include #include #include #include #include #include using std::ostringstream; using std::endl; using std::list; #include "cstr.h" #include "reslistpager.h" #include "log.h" #include "rclconfig.h" #include "smallut.h" #include "rclutil.h" #include "plaintorich.h" #include "mimehandler.h" // Default highlighter. No need for locking, this is query-only. static const string cstr_hlfontcolor(""); static const string cstr_hlendfont(""); class PlainToRichHtReslist : public PlainToRich { public: virtual string startMatch(unsigned int) { return cstr_hlfontcolor; } virtual string endMatch() { return cstr_hlendfont; } }; static PlainToRichHtReslist g_hiliter; ResListPager::ResListPager(int pagesize) : m_pagesize(pagesize), m_newpagesize(pagesize), m_resultsInCurrentPage(0), m_winfirst(-1), m_hasNext(true), m_hiliter(&g_hiliter) { } void ResListPager::resultPageNext() { if (!m_docSource) { LOGDEB("ResListPager::resultPageNext: null source\n" ); return; } int resCnt = m_docSource->getResCnt(); LOGDEB("ResListPager::resultPageNext: rescnt " << (resCnt) << ", winfirst " << (m_winfirst) << "\n" ); if (m_winfirst < 0) { m_winfirst = 0; } else { m_winfirst += int(m_respage.size()); } // Get the next page of results. Note that we look ahead by one to // determine if there is actually a next page vector npage; int pagelen = m_docSource->getSeqSlice(m_winfirst, m_pagesize + 1, npage); // If page was truncated, there is no next m_hasNext = (pagelen == m_pagesize + 1); // Get rid of the possible excess result if (pagelen == m_pagesize + 1) { npage.resize(m_pagesize); pagelen--; } if (pagelen <= 0) { // No results ? This can only happen on the first page or if the // actual result list size is a multiple of the page pref (else // there would have been no Next on the last page) if (m_winfirst > 0) { // Have already results. Let them show, just disable the // Next button. We'd need to remove the Next link from the page // too. // Restore the m_winfirst value, let the current result vector alone m_winfirst -= int(m_respage.size()); } else { // No results at all (on first page) m_winfirst = -1; } return; } m_resultsInCurrentPage = pagelen; m_respage = npage; } static string maybeEscapeHtml(const string& fld) { if (fld.compare(0, cstr_fldhtm.size(), cstr_fldhtm)) return escapeHtml(fld); else return fld.substr(cstr_fldhtm.size()); } void ResListPager::resultPageFor(int docnum) { if (!m_docSource) { LOGDEB("ResListPager::resultPageFor: null source\n" ); return; } int resCnt = m_docSource->getResCnt(); LOGDEB("ResListPager::resultPageFor(" << (docnum) << "): rescnt " << (resCnt) << ", winfirst " << (m_winfirst) << "\n" ); m_winfirst = (docnum / m_pagesize) * m_pagesize; // Get the next page of results. vector npage; int pagelen = m_docSource->getSeqSlice(m_winfirst, m_pagesize, npage); // If page was truncated, there is no next m_hasNext = (pagelen == m_pagesize); if (pagelen <= 0) { m_winfirst = -1; return; } m_respage = npage; } void ResListPager::displayDoc(RclConfig *config, int i, Rcl::Doc& doc, const HighlightData& hdata, const string& sh) { ostringstream chunk; // Determine icon to display if any string iconurl = iconUrl(config, doc); // Printable url: either utf-8 if transcoding succeeds, or url-encoded string url; printableUrl(config->getDefCharset(), doc.url, url); // Same as url, but with file:// possibly stripped. output by %u instead // of %U. string urlOrLocal; urlOrLocal = fileurltolocalpath(url); if (urlOrLocal.empty()) urlOrLocal = url; // Make title out of file name if none yet string titleOrFilename; string utf8fn; doc.getmeta(Rcl::Doc::keytt, &titleOrFilename); doc.getmeta(Rcl::Doc::keyfn, &utf8fn); if (utf8fn.empty()) { utf8fn = path_getsimple(url); } if (titleOrFilename.empty()) { titleOrFilename = utf8fn; } // Url for the parent directory. We strip the file:// part for local // paths string parenturl = url_parentfolder(url); { string localpath = fileurltolocalpath(parenturl); if (!localpath.empty()) parenturl = localpath; } // Result number char numbuf[20]; int docnumforlinks = m_winfirst + 1 + i; sprintf(numbuf, "%d", docnumforlinks); // Document date: either doc or file modification time char datebuf[100]; datebuf[0] = 0; if (!doc.dmtime.empty() || !doc.fmtime.empty()) { time_t mtime = doc.dmtime.empty() ? atoll(doc.fmtime.c_str()) : atoll(doc.dmtime.c_str()); struct tm *tm = localtime(&mtime); strftime(datebuf, 99, dateFormat().c_str(), tm); } // Size information. We print both doc and file if they differ a lot off_t fsize = -1, dsize = -1; if (!doc.dbytes.empty()) dsize = static_cast(atoll(doc.dbytes.c_str())); if (!doc.fbytes.empty()) fsize = static_cast(atoll(doc.fbytes.c_str())); string sizebuf; if (dsize > 0) { sizebuf = displayableBytes(dsize); if (fsize > 10 * dsize && fsize - dsize > 1000) sizebuf += string(" / ") + displayableBytes(fsize); } else if (fsize >= 0) { sizebuf = displayableBytes(fsize); } string richabst; bool needabstract = parFormat().find("%A") != string::npos; if (needabstract && m_docSource) { vector vabs; m_docSource->getAbstract(doc, vabs); m_hiliter->set_inputhtml(false); for (vector::const_iterator it = vabs.begin(); it != vabs.end(); it++) { if (!it->empty()) { // No need to call escapeHtml(), plaintorich handles it list lr; // There may be data like page numbers before the snippet text. // will be in brackets. string::size_type bckt = it->find("]"); if (bckt == string::npos) { m_hiliter->plaintorich(*it, lr, hdata); } else { m_hiliter->plaintorich(it->substr(bckt), lr, hdata); lr.front() = it->substr(0, bckt) + lr.front(); } richabst += lr.front(); richabst += absSep(); } } } // Links; ostringstream linksbuf; if (canIntern(doc.mimetype, config)) { linksbuf << "" << trans("Preview") << "  "; } string apptag; doc.getmeta(Rcl::Doc::keyapptg, &apptag); if (!config->getMimeViewerDef(doc.mimetype, apptag, false).empty()) { linksbuf << "" << trans("Open") << ""; } ostringstream snipsbuf; if (doc.haspages) { snipsbuf << "" << trans("Snippets") << "  "; linksbuf << "  " << snipsbuf.str(); } string collapscnt; if (doc.getmeta(Rcl::Doc::keycc, &collapscnt) && !collapscnt.empty()) { ostringstream collpsbuf; int clc = atoi(collapscnt.c_str()) + 1; collpsbuf << "" << trans("Dups") << "(" << clc << ")" << "  "; linksbuf << "  " << collpsbuf.str(); } // Build the result list paragraph: // Subheader: this is used by history if (!sh.empty()) chunk << "

" << sh << "

\n

"; else chunk << "

"; char xdocidbuf[100]; sprintf(xdocidbuf, "%lu", doc.xdocid); // Configurable stuff map subs; subs["A"] = !richabst.empty() ? richabst : ""; subs["D"] = datebuf; subs["E"] = snipsbuf.str(); subs["I"] = iconurl; subs["i"] = doc.ipath; subs["K"] = !doc.meta[Rcl::Doc::keykw].empty() ? string("[") + maybeEscapeHtml(doc.meta[Rcl::Doc::keykw]) + "]" : ""; subs["L"] = linksbuf.str(); subs["N"] = numbuf; subs["M"] = doc.mimetype; subs["P"] = parenturl; subs["R"] = doc.meta[Rcl::Doc::keyrr]; subs["S"] = sizebuf; subs["T"] = maybeEscapeHtml(titleOrFilename); subs["t"] = maybeEscapeHtml(doc.meta[Rcl::Doc::keytt]); subs["U"] = url; subs["u"] = urlOrLocal; subs["x"] = xdocidbuf; // Let %(xx) access all metadata. HTML-neuter everything: for (map::iterator it = doc.meta.begin(); it != doc.meta.end(); it++) { if (!it->first.empty()) subs[it->first] = maybeEscapeHtml(it->second); } string formatted; pcSubst(parFormat(), formatted, subs); chunk << formatted; chunk << "

" << endl; // This was to force qt 4.x to clear the margins (which it should do // anyway because of the paragraph's style), but we finally took // the table approach for 1.15 for now (in guiutils.cpp) // chunk << "
" << endl; LOGDEB2("Chunk: [" << ((const char *)chunk.rdbuf()->str()) << "]\n" ); append(chunk.rdbuf()->str(), i, doc); } bool ResListPager::getDoc(int num, Rcl::Doc& doc) { if (m_winfirst < 0 || m_respage.size() == 0) return false; if (num < m_winfirst || num >= m_winfirst + int(m_respage.size())) return false; doc = m_respage[num-m_winfirst].doc; return true; } void ResListPager::displayPage(RclConfig *config) { LOGDEB("ResListPager::displayPage\n" ); if (!m_docSource) { LOGDEB("ResListPager::displayPage: null source\n" ); return; } if (m_winfirst < 0 && !pageEmpty()) { LOGDEB("ResListPager::displayPage: sequence error: winfirst < 0\n" ); return; } ostringstream chunk; // Display list header // We could use a but the textedit doesnt display // it prominently // Note: have to append text in chunks that make sense // html-wise. If we break things up too much, the editor // gets confused. Hence the use of the 'chunk' text // accumulator // Also note that there can be results beyond the estimated resCnt. chunk << "<html><head>" << endl << "<meta http-equiv=\"content-type\"" << " content=\"text/html; charset=utf-8\">" << endl << headerContent() << "</head><body>" << endl << pageTop() << "<p><span style=\"font-size:110%;\"><b>" << m_docSource->title() << "</b></span>   "; if (pageEmpty()) { chunk << trans("<p><b>No results found</b><br>"); string reason = m_docSource->getReason(); if (!reason.empty()) { chunk << "<blockquote>" << escapeHtml(reason) << "</blockquote></p>"; } else { HighlightData hldata; m_docSource->getTerms(hldata); vector<string> uterms(hldata.uterms.begin(), hldata.uterms.end()); if (!uterms.empty()) { map<string, vector<string> > spellings; suggest(uterms, spellings); if (!spellings.empty()) { if (o_index_stripchars) { chunk << trans("<p><i>Alternate spellings (accents suppressed): </i>") << "<br /><blockquote>"; } else { chunk << trans("<p><i>Alternate spellings: </i>") << "<br /><blockquote>"; } for (map<string, vector<string> >::const_iterator it0 = spellings.begin(); it0 != spellings.end(); it0++) { chunk << "<b>" << it0->first << "</b> : "; for (vector<string>::const_iterator it = it0->second.begin(); it != it0->second.end(); it++) { chunk << *it << " "; } chunk << "<br />"; } chunk << "</blockquote></p>"; } } } } else { unsigned int resCnt = m_docSource->getResCnt(); if (m_winfirst + m_respage.size() < resCnt) { chunk << trans("Documents") << " <b>" << m_winfirst + 1 << "-" << m_winfirst + m_respage.size() << "</b> " << trans("out of at least") << " " << resCnt << " " << trans("for") << " " ; } else { chunk << trans("Documents") << " <b>" << m_winfirst + 1 << "-" << m_winfirst + m_respage.size() << "</b> " << trans("for") << " "; } } chunk << detailsLink(); if (hasPrev() || hasNext()) { chunk << "  "; if (hasPrev()) { chunk << "<a href=\"" + prevUrl() + "\"><b>" << trans("Previous") << "</b></a>   "; } if (hasNext()) { chunk << "<a href=\""+ nextUrl() + "\"><b>" << trans("Next") << "</b></a>"; } } chunk << "</p>" << endl; append(chunk.rdbuf()->str()); chunk.rdbuf()->str(""); if (pageEmpty()) return; HighlightData hdata; m_docSource->getTerms(hdata); // Emit data for result entry paragraph. Do it in chunks that make sense // html-wise, else our client may get confused for (int i = 0; i < (int)m_respage.size(); i++) { Rcl::Doc& doc(m_respage[i].doc); string& sh(m_respage[i].subHeader); displayDoc(config, i, doc, hdata, sh); } // Footer chunk << "<p align=\"center\">"; if (hasPrev() || hasNext()) { if (hasPrev()) { chunk << "<a href=\"" + prevUrl() + "\"><b>" << trans("Previous") << "</b></a>   "; } if (hasNext()) { chunk << "<a href=\""+ nextUrl() + "\"><b>" << trans("Next") << "</b></a>"; } } chunk << "</p>" << endl; chunk << "</body></html>" << endl; append(chunk.rdbuf()->str()); } // Default implementations for things that should be implemented by // specializations string ResListPager::nextUrl() { return "n-1"; } string ResListPager::prevUrl() { return "p-1"; } string ResListPager::iconUrl(RclConfig *config, Rcl::Doc& doc) { string apptag; doc.getmeta(Rcl::Doc::keyapptg, &apptag); return path_pathtofileurl(config->getMimeIconPath(doc.mimetype, apptag)); } bool ResListPager::append(const string& data) { fprintf(stderr, "%s", data.c_str()); return true; } string ResListPager::trans(const string& in) { return in; } string ResListPager::detailsLink() { string chunk = "<a href=\"H-1\">"; chunk += trans("(show query)") + "</a>"; return chunk; } const string &ResListPager::parFormat() { static const string cstr_format("<img src=\"%I\" align=\"left\">" "%R %S %L   <b>%T</b><br>" "%M %D   <i>%U</i><br>" "%A %K"); return cstr_format; } const string &ResListPager::dateFormat() { static const string cstr_format(" %Y-%m-%d %H:%M:%S %z"); return cstr_format; }