/*CatalogCopyRetriever.cpp * CatalogCopyRetriever.cpp * * AJO 4/12/07 Modified to accept a "copy selector" code in the input file * to specify whether to use short, medium, or long copy. If the specified * copy type is not available, default to long, but issue an error message. * If the copy selector code is invalid, issue an error message. * * AJO 5/3/07 Some substantial changes were made to replace all DOD lines * with a single "Streaming Video" line, which can be excluded with a command- * line parameter. * * AJO 5/9/07 Redefined IS_LIBRARY as both including ISBN (which is now not on * its own line) and excluding streaming video. * * AJO 6/15/07 Huge revision to comply with new FOF design */ #include #include #include #include #include #include #include "osslib/odbc.h" #include "osslib/string.h" using namespace std; using namespace odbc; using namespace OssLib; class ErrorLog { string log; char delimiter; ostream &os; public: // os is the output file ErrorLog(char delimiterInit, ostream &osInit) : delimiter(delimiterInit), os(osInit) { } void Add(const string &entry) { log += entry + '\n'; os << delimiter << delimiter << "@comment:" << entry << delimiter << delimiter; } const string &GetLog() const { return(log); } }; // Stores the data for a title needed throughout the program struct TitleData { string extTitleId, nameHtml, longDescriptionHtml, mediumDescriptionHtml, shortDescriptionHtml; bool hasCanadianRights, isDiscontinued; bool hasVideoClip, closedCaptioned; int copyrightYear; // AJO 2/7/06 Added. Note that 0 = unavailable int runtime; // AJO 3/22/06 Added. 0 = unavailable bool isVideoFormat; // AJO 3/22/06 Added. string licensing; // AJO 6/4/07 Added. bool hasCorrelations; // AJO 6/15/07 Added. bool isPastDoNotPromoteAfterDate; // AJO 6/18/07 Added. bool isPosterFormat; // AJO 6/19/07 bool hasReview; // AJO 6/25/07 TitleData(const string &extTitleIdInit, const string &nameHtmlInit, const string &longDescriptionHtmlInit, const string &mediumDescriptionHtmlInit, const string &shortDescriptionHtmlInit, bool hasCanadianRightsInit, bool isDiscontinuedInit, bool hasVideoClipInit, bool closedCaptionedInit, int copyrightYearInit, int runtimeInit, bool isVideoFormatInit, const string &licensingInit, bool hasCorrelationsInit, bool isPastDoNotPromoteAfterDateInit, bool isPosterFormatInit, bool hasReviewInit) : extTitleId(extTitleIdInit), nameHtml(nameHtmlInit), longDescriptionHtml(longDescriptionHtmlInit), mediumDescriptionHtml(mediumDescriptionHtmlInit), shortDescriptionHtml(shortDescriptionHtmlInit), hasCanadianRights(hasCanadianRightsInit), isDiscontinued(isDiscontinuedInit), hasVideoClip(hasVideoClipInit), closedCaptioned(closedCaptionedInit), copyrightYear(copyrightYearInit), runtime(runtimeInit), isVideoFormat(isVideoFormatInit), licensing(licensingInit), hasCorrelations(hasCorrelationsInit), isPastDoNotPromoteAfterDate(isPastDoNotPromoteAfterDateInit), isPosterFormat(isPosterFormatInit), hasReview(hasReviewInit) { } }; // Subclasses choose which copy to use from the TitleData provided. class CopySelector { public: virtual ~CopySelector() { } virtual string Select(const TitleData &, ErrorLog &errorLog) const = 0; }; class LongCopySelector : public CopySelector { public: virtual string Select(const TitleData &td, ErrorLog &errorLog) const { if (td.longDescriptionHtml.empty()) { errorLog.Add("Long copy selected for title ID " + td.extTitleId + " but none is available."); } return(td.longDescriptionHtml); } }; class MediumCopySelector : public CopySelector { public: virtual string Select(const TitleData &td, ErrorLog &errorLog) const { if (td.mediumDescriptionHtml.empty()) { if (td.longDescriptionHtml.empty()) { errorLog.Add("Medium copy selected for title ID " + td.extTitleId + " but neither medium copy " "nor long copy is available."); } else { errorLog.Add("Medium copy selected for title ID " + td.extTitleId + ", but none is available. Using " "long copy."); } return(td.longDescriptionHtml); } return(td.mediumDescriptionHtml); } }; class ShortCopySelector : public CopySelector { public: virtual string Select(const TitleData &td, ErrorLog &errorLog) const { if (td.shortDescriptionHtml.empty()) { if (td.longDescriptionHtml.empty()) { errorLog.Add("Short copy selected for title ID " + td.extTitleId + " but neither short copy " "nor long copy is available."); } else { errorLog.Add("Short copy selected for title ID " + td.extTitleId + ", but none is available. Using " "long copy."); } return(td.longDescriptionHtml); } return(td.shortDescriptionHtml); } }; // base class; derived know how to set the bin tag class ItemLineRenderer { public: virtual ~ItemLineRenderer() { } virtual string GetBinTag() const = 0; }; class SeriesItemLineRenderer : public ItemLineRenderer { public: virtual string GetBinTag() const { return("@series bin:"); } }; class NonseriesItemLineRenderer : public ItemLineRenderer { public: virtual string GetBinTag() const { return("@bin:"); } }; struct InputData { string extTitleId, comment; HeapHolder copySelector; InputData(const string &extTitleIdInit, const string &commentInit, const HeapHolder ©SelectorInit) : extTitleId(extTitleIdInit), comment(commentInit), copySelector(copySelectorInit) { } }; typedef vector InputDataList; struct SeriesComponentData { string extTitleId; int listPriceCents; string formatCode; SeriesComponentData(const string &extTitleIdInit, int listPriceCentsInit, const string &formatCodeInit) : extTitleId(extTitleIdInit), listPriceCents(listPriceCentsInit), formatCode(formatCodeInit) { } }; class SeriesComponentTitleFinder : public unary_function { string extTitleId; public: SeriesComponentTitleFinder(const string &extTitleIdInit) : extTitleId(extTitleIdInit) { } bool operator()(const SeriesComponentData &data) { return(data.extTitleId == extTitleId); } }; typedef vector SeriesComponentList; struct ItemData { string formatDescription, formatCode, isbn, adamsItemCode; int listPriceCents; ItemData(const string &formatDescriptionInit, const string &formatCodeInit, int listPriceCentsInit, const string &isbnInit, const string &adamsItemCodeInit) : formatDescription(formatDescriptionInit), formatCode(formatCodeInit), listPriceCents(listPriceCentsInit), isbn(isbnInit), adamsItemCode(adamsItemCodeInit) { } // We want to use ISBN for items that are not 3P and are video, CD-ROM, // or DVD-ROM bool ShouldUseIsbn(const TitleData &titleData) const { return(titleData.licensing != "3P" && (titleData.isVideoFormat || formatCode == "R" || formatCode == "KROM")); } // Get either an ISBN or an item code string GetItemCodeToDisplay(const TitleData &titleData) const { if (!isbn.empty() && ShouldUseIsbn(titleData)) return("ISBN " + isbn); return(titleData.extTitleId + '-' + formatCode); } }; typedef vector ItemDataList; class HTMLTagsAndEntitiesRemover { map entityToMacCharMap; vector > replacements; public: HTMLTagsAndEntitiesRemover(); string Remove(const string &s); }; HTMLTagsAndEntitiesRemover::HTMLTagsAndEntitiesRemover() { // This file contains tab-separated HTML entity number // and the corresponding Mac character set number. // It was generated by copying HTML tables out of // http://www.siber-sonic.com/mac/charsetstuff/Soniccharset.html // pasting it into Excel and eventually saving as text. ifstream is("HTMLEntityNumberToMacCharacter.txt"); string line; while (getline(is, line).good()) { vector cols(ParseColumns(line)); int entity = atoi(cols.at(0).c_str()); int mac = atoi(cols.at(1).c_str()); if (!entityToMacCharMap.insert(make_pair(entity, mac)).second) throw runtime_error("HTML entity to Mac character set " "input file contains more than one entry for entity " + cols.at(0));; } if (!is.eof()) throw runtime_error("Error occurred reading HTML entity " "to Mac character set input file"); // A list of original values and replacement values // More provided by Ed 11/14/06, confirmed at // http://www.prefab.com/dev/macHTMLEntities.html // Now stored in a file for easier editing. ifstream is2("HTMLEntityToXpressTag.txt"); while (getline(is2, line).good()) { vector cols(ParseColumns(line)); if (cols.size() != 2) throw runtime_error("invalid HTML entity to Xpress " "tag input file line '" + line + "'"); string entity(cols.at(0)); string tag(cols.at(1)); replacements.push_back(make_pair(entity, tag)); } if (!is2.eof()) throw runtime_error("Error occurred reading HTML Entity " "to Xpress Tag input file"); } string HTMLTagsAndEntitiesRemover::Remove(const string &s) { string w(s); unsigned p; // Replace all instances of any of the to-be-replaced strings from // the input file with the corresponding desired string for (vector >::iterator i = replacements.begin(); i != replacements.end(); ++i) { string original(i->first); string replacement(i->second); while ((p = w.find(original)) != string::npos) { w = w.substr(0, p) + replacement + w.substr(p + original.length()); } } // replace all HTML character entities &#NNN; with <\#MMM> // where NNN will be replaced with MMM if found in the // HTML entity to Mac character map read in during construction for (p = 0; p + 1 < w.length(); ++p) { if (w.at(p) == '&' && w.at(p + 1) == '#') { string nnn; bool shouldReplace = true; unsigned q = p + 2; while (q < w.length() && w.at(q) != ';' && shouldReplace) { char c = w.at(q); // If it's a digit, add it to NNN if (c >= '0' && c <= '9') nnn += c; // Otherwise, it's not an entity else shouldReplace = false; ++q; } // Didn't find a terminating semicolon if (q >= w.length()) shouldReplace = false; if (shouldReplace && !nnn.empty()) { int n = atoi(nnn.c_str()); map::iterator i = entityToMacCharMap.find(n); int m; // If not found in the map, just use the entity number if (i == entityToMacCharMap.end()) m = n; else m = i->second; ostringstream os; os << w.substr(0, p) << "<\\#" << m << ">" << w.substr(q + 1); w = os.str(); } } } return(w); } class CatalogCopyRetriever { char delimiter; // line delimiter bool isMaster; bool includeCorrelations; string separator; // between elements of product info lines double seriesDiscount; // percentage, from 0 - 100 ostream &outputFile; ostream &errorFile; ostream &neededIsbnFile; ostream *os; // convenience variable for outputFile HTMLTagsAndEntitiesRemover remover; ErrorLog errorLog; HeapHolder GetTitle( PreparedStatement &getTitleStmt, PreparedStatement &getPrimaryItemStmt, const string &extTitleId); HeapHolder GetSeries( PreparedStatement &getSeriesComponentsStmt, const string &extTitleId); HeapHolder GetItemDataList( PreparedStatement &getItemsStmt, const string &extTitleId); bool GetsSeriesDiscount(const TitleData &titleData, const SeriesComponentList &seriesComponentList) const; void GenerateIcons(const TitleData &titleData); void GenerateProductInfoLine(const ItemData &itemData, const TitleData &titleData, const ItemLineRenderer &itemLineRenderer); void GenerateSeriesHeader( const TitleData &titleData, const string &comment, const CopySelector ©Selector); void GenerateSeriesFooter(const TitleData &titleData, const SeriesComponentList &componentList, const ItemDataList &titleRenderData); void GenerateItemPriceLine(const ItemData &itemData, const TitleData &titleData, const ItemLineRenderer &itemLineRenderer); void GenerateStreamingVideoLine(const TitleData &titleData, const ItemLineRenderer &itemLineRenderer); void GenerateTitleOutput(const TitleData &titleData, const ItemDataList &titleDataRender, const string &comment, const CopySelector ©Selector); string GetMissingComponentsList( const SeriesComponentList &seriesComponentList, const vector &seriesComponentsFound); public: CatalogCopyRetriever(char delimiterInit, ostream &outputFileInit, ostream &errorFileInit, ostream &neededIsbnFileInit, bool isMasterInit, bool includeCorrelationsInit) : delimiter(delimiterInit), errorLog(delimiterInit, outputFileInit), outputFile(outputFileInit), errorFile(errorFileInit), neededIsbnFile(neededIsbnFileInit), isMaster(isMasterInit), includeCorrelations(includeCorrelationsInit), separator(isMasterInit ? "<\\#9>" : " <\\#165> "), // Masters get space; others get bullet seriesDiscount(isMasterInit ? 10.0 : 15.0) // Master cat gets lower discount { os = &outputFile; // setting the convenience variable } void Retrieve(const InputDataList &); }; HeapHolder CatalogCopyRetriever::GetTitle( PreparedStatement &getTitleStmt, PreparedStatement &getPrimaryItemStmt, const string &extTitleId) { HeapHolder titleData; getTitleStmt.setString(1, extTitleId); SafeResultSet rs(getTitleStmt.executeQuery()); if (!rs.next()) { errorLog.Add("Title " + extTitleId + " DOES NOT EXIST. " "Skipping this entry."); } else { getPrimaryItemStmt.setString(1, extTitleId); SafeResultSet itemRs(getPrimaryItemStmt.executeQuery()); if (!itemRs.next()) { errorLog.Add("Title " + extTitleId + " is NOT PROPERLY " "CONFIGURED WITH A PRIMARY ITEM. Skipping this title."); } else { string name(remover.Remove(rs.getString("TitleFullNameHTML"))); string longDescription(remover.Remove(rs.getString("FullDescriptionHTML"))); string mediumDescription(remover.Remove(rs.getString("MediumCopyHTML"))); string shortDescription(remover.Remove(rs.getString("ShortCopyHTML"))); if (name.empty()) errorLog.Add("Missing name from website " "for title " + extTitleId + " (probably not loaded " "on website)"); titleData.reset(new TitleData( extTitleId, name, longDescription, mediumDescription, shortDescription, rs.getInt("HasCanadianRights"), itemRs.getInt("IsDiscontinued"), rs.getInt("HasVideoClip"), rs.getInt("ClosedCaptioned"), rs.getInt("CopyrightYear"), rs.getInt("Runtime"), itemRs.getInt("IsVideoFormat"), rs.getString("Licensing"), rs.getInt("HasCorrelations"), rs.getInt("IsPastDoNotPromoteAfterDate"), itemRs.getInt("IsPosterFormat"), rs.getInt("HasReview"))); } } return(titleData); } HeapHolder CatalogCopyRetriever::GetSeries( PreparedStatement &getSeriesComponentsStmt, const string &extTitleId) { HeapHolder seriesComponentList( new SeriesComponentList); getSeriesComponentsStmt.setString(1, extTitleId); SafeResultSet rs(getSeriesComponentsStmt.executeQuery()); while (rs.next()) { seriesComponentList->push_back(SeriesComponentData( rs.getString("ExtTitleId"), rs.getDouble("ListPriceCents"), rs.getString("PrimaryFormatCode"))); } return(seriesComponentList); } HeapHolder CatalogCopyRetriever::GetItemDataList( PreparedStatement &getItemsStmt, const string &extTitleId) { HeapHolder itemDataList(new ItemDataList); // Display VHS after any other formats, so hold on to it here HeapHolder vhsItemData; getItemsStmt.setString(1, extTitleId); SafeResultSet rs(getItemsStmt.executeQuery()); while (rs.next()) { string formatCode(rs.getString("FormatCode")); string formatSubtype(rs.getString("FormatSubType")); int listPriceCents(rs.getDouble("ListPriceCents")); string isbn(rs.getString("ISBN")); string adamsItemCode(rs.getString("ExtItemID")); string formatDesc; bool isOkayToSkip = false; if (formatCode == "A" && formatSubtype == "NT") formatDesc = "VHS"; else if (formatCode == "K" && formatSubtype.empty()) formatDesc = "DVD"; else if (formatCode == "KROM" && formatSubtype.empty()) formatDesc = "DVD-ROM"; else if (formatCode == "KS" && formatSubtype.empty()) formatDesc = "DVD"; else if (formatCode == "L" && formatSubtype.empty()) formatDesc = "Audio CD"; else if (formatCode == "MM" && formatSubtype.empty()) formatDesc = "Multimedia"; else if (formatCode == "R" && formatSubtype.empty()) formatDesc = "CD-ROM"; else if (formatCode == "W" && formatSubtype.empty()) formatDesc = "Workbook"; else if (formatCode == "BK" && formatSubtype.empty()) formatDesc = "Book"; else if (formatCode == "PO" && formatSubtype == "LA") formatDesc = "Poster"; else if (formatCode == "TR" && formatSubtype.empty()) formatDesc = "Transparencies"; else if (formatCode == "SG" && formatSubtype.empty()) formatDesc = "Study Guide"; else if (formatCode == "DOD" || formatCode == "DR" || formatCode == "SV" || formatCode == "A" || formatCode == "M" || formatCode == "N" || formatCode == "B" || (formatCode == "KROM" && (formatSubtype == "M1" || formatSubtype == "M2" || formatSubtype == "M4" || formatSubtype == "WM" || formatSubtype == "QT")) || (formatCode == "R" && (formatSubtype == "M1" || formatSubtype == "M2")) || formatCode == "X" || formatCode == "C" || formatCode == "D" || formatCode == "E" || formatCode == "DP" || formatCode == "PO" || formatCode == "SL") isOkayToSkip = true; if (formatDesc.empty() && !isOkayToSkip) { errorLog.Add("Unexpected format " + formatCode + " and format subtype " + formatSubtype + " for title " + extTitleId); } else if (!formatDesc.empty()) { // Check the list price if this is an item we want // to include in the output. if (listPriceCents <= 0) { errorLog.Add("Item with format " + formatCode + " and format subtype " + formatSubtype + " for title " + extTitleId + " has a zero or negative price"); } else if (formatDesc != "VHS") { // Put the non-VHS item in the list itemDataList->push_back(ItemData( formatDesc, formatCode, listPriceCents, isbn, adamsItemCode)); } else // Keep this VHS item for later if necessary vhsItemData.reset(new ItemData( formatDesc, formatCode, listPriceCents, isbn, adamsItemCode)); } } // If we found a VHS item, add the VHS item to the list if (!IsNull(vhsItemData)) itemDataList->push_back(*vhsItemData); if (itemDataList->empty()) errorLog.Add("No valid items for title " + extTitleId); return(itemDataList); } // These icons appear after copy void CatalogCopyRetriever::GenerateIcons(const TitleData &titleData) { if (titleData.copyrightYear != 0) *os << " <\\#169><\\!s>" << titleData.copyrightYear; // It's possible that the <\!s> guys are rendering as a space themselves if (titleData.closedCaptioned) *os << "<\\!s><@cc>.<@$p>"; if (titleData.hasVideoClip) *os << "<\\!s><@vcg><\\#169><@vcm>M<@$p>"; if (!titleData.hasCanadianRights) *os << "<\\!s><\\#224>"; if (includeCorrelations && titleData.hasCorrelations) *os << "<\\!s><@corr>o<@$p>"; // This is sort-of an icon... if (titleData.hasReview) { *os << delimiter << "@review:Reviews Available"; } *os << delimiter; } // Certain comment values should translate into tags to prefix copy with. // Returns empty string if not a special comment. string SpecialCommentTag(const string &comment) { // We'll identify a special comment by extracting and uppercasing letters // to make it easier to identify, in case of punctuation or capitalization // variations. string extract; for (string::const_iterator i = comment.begin(); i != comment.end(); ++i) { if (isalpha(*i)) extract += static_cast(toupper(*i)); } static map tags; if (tags.empty()) { tags.insert(make_pair("NEW", "<@New!>New!<@$p> ")); tags.insert(make_pair("BESTSELLER", "<@bs>Best Seller!<@$p> ")); tags.insert(make_pair("EDITORSCHOICE", "<@ec>Editor's Choice!<@$p> ")); tags.insert(make_pair("UPDATED", "<@ud>Updated!<@$p> ")); tags.insert(make_pair("REDUCEDPRICES", "<@rp>Reduced Prices!<@$p> ")); tags.insert(make_pair("INSTRUCTORSCHOICE", "<@ic>Instructor's Choice!<@$p> ")); tags.insert(make_pair("NEWSERIES", "<@ns>New Series!<@$p> ")); } map::iterator i = tags.find(extract); string retval; if (i != tags.end()) retval = i->second; return(retval); } void CatalogCopyRetriever::GenerateSeriesHeader( const TitleData &titleData, const string &comment, const CopySelector ©Selector) { string commentTag(SpecialCommentTag(comment)); if (commentTag.empty() && !comment.empty()) *os << "@comment:" << comment << delimiter; *os << "@series title:" << commentTag << titleData.nameHtml << delimiter << "@intros:" << copySelector.Select(titleData, errorLog); GenerateIcons(titleData); } // Does not include generation of the price at the end of the line, // or the delimiter void CatalogCopyRetriever::GenerateProductInfoLine(const ItemData &itemData, const TitleData &titleData, const ItemLineRenderer &itemLineRenderer) { *os << itemLineRenderer.GetBinTag() << "<@format>" << itemData.formatDescription << "<@$p>" << separator << "<@ISBN>" << itemData.GetItemCodeToDisplay(titleData) << "<@$p>"; // If no ISBN is available for an item that needs one, warn! // We will want one for this item. if (itemData.isbn.empty() && itemData.ShouldUseIsbn(titleData)) { ostringstream err; err << itemData.formatDescription << " item for title " << titleData.extTitleId << " does not have an ISBN"; string errString(err.str()); errorLog.Add(errString); neededIsbnFile << itemData.adamsItemCode << endl; } } string CentsAsMoney(int totalCents) { ostringstream centsStream; int centsInt = totalCents % 100; centsStream << centsInt; string cents(centsStream.str()); ostringstream dollarsStream; int dollarsInt = totalCents / 100; dollarsStream << dollarsInt; string dollars(dollarsStream.str()); // First generate the cents and the decimal point string money(cents); if (money.size() == 1) money = '0' + money; money = '.' + money; // Now add the dollar digits one by one, adding commas as necessary for (unsigned i = 0; i < dollars.size(); ++i) { if (i % 3 == 0 && i != 0) money = ',' + money; money = dollars.at(dollars.size() - (i + 1)) + money; } // And a final dollar sign. money = '$' + money; return(money); } // Takes a price in cents, removes the discount, and rounds up to the nearest // 5 cents; int DiscountPriceOf(int cents, double discount) { int discountedPrice = floor( static_cast(cents) * (1.0 - (discount / 100.0))); int roundedDiscountedPrice = discountedPrice / 5; roundedDiscountedPrice += (discountedPrice % 5 == 0 ? 0 : 1); roundedDiscountedPrice *= 5; return(roundedDiscountedPrice); } // Gets discount if not poster, not 3P, and has only one format bool CatalogCopyRetriever::GetsSeriesDiscount(const TitleData &titleData, const SeriesComponentList &componentList) const { bool getsSeriesDiscount = true; if (titleData.isPosterFormat || titleData.licensing == "3P") getsSeriesDiscount = false; else { string firstFormatCode; for (SeriesComponentList::const_iterator i = componentList.begin(); i != componentList.end() && getsSeriesDiscount; ++i) { if (firstFormatCode.empty()) firstFormatCode = i->formatCode; else if (firstFormatCode != i->formatCode) getsSeriesDiscount = false; } } return(getsSeriesDiscount); } void CatalogCopyRetriever::GenerateSeriesFooter( const TitleData &titleData, const SeriesComponentList &componentList, const ItemDataList &itemDataList) { int componentPriceSumCents = 0; for (SeriesComponentList::const_iterator i = componentList.begin(); i != componentList.end(); ++i) componentPriceSumCents += i->listPriceCents; if (GetsSeriesDiscount(titleData, componentList)) { *os << "@save series:SAVE " << seriesDiscount << "%!" << delimiter << "@series when:Purchase the " << componentList.size() << "-part series today!" << delimiter << "@series list price:List Price: " << CentsAsMoney(componentPriceSumCents) << delimiter << "@series discount price:Special Discount Price: " << CentsAsMoney(DiscountPriceOf(componentPriceSumCents, seriesDiscount)) << delimiter; } else { // Shouldn't be empty at this point. Paranoia, fear destroya. if (itemDataList.size() == 0) throw runtime_error("No items available for title ID " + titleData.extTitleId + " from which to determine " "the list price."); *os << "@save series:SAVE!" << delimiter << "@series when:when you purchase the " << componentList.size() << "-part series" << delimiter << "@series list price:List Price: " << CentsAsMoney(itemDataList.at(0).listPriceCents) << delimiter; } for (ItemDataList::const_iterator i = itemDataList.begin(); i != itemDataList.end(); ++i) { GenerateProductInfoLine(*i, titleData, SeriesItemLineRenderer()); *os << delimiter; } } void CatalogCopyRetriever::GenerateTitleOutput( const TitleData &titleData, const ItemDataList &itemDataList, const string &comment, const CopySelector ©Selector) { string commentTag(SpecialCommentTag(comment)); if (commentTag.empty() && !comment.empty()) *os << "@comment:" << comment << delimiter; *os << "@program titles:" << commentTag << titleData.nameHtml << delimiter << "@body:" << copySelector.Select(titleData, errorLog); GenerateIcons(titleData); for (ItemDataList::const_iterator i = itemDataList.begin(); i != itemDataList.end(); ++i) { GenerateProductInfoLine(*i, titleData, NonseriesItemLineRenderer()); *os << separator << CentsAsMoney(i->listPriceCents) << delimiter; } if (itemDataList.empty()) { ostringstream err; err << "No items found for title " << titleData.extTitleId; errorLog.Add(err.str()); } } string CatalogCopyRetriever::GetMissingComponentsList( const SeriesComponentList &seriesComponentList, const vector &seriesComponentsFound) { string list; for (SeriesComponentList::const_iterator i = seriesComponentList.begin(); i != seriesComponentList.end(); ++i) { if (find(seriesComponentsFound.begin(), seriesComponentsFound.end(), i->extTitleId) == seriesComponentsFound.end()) { if (!list.empty()) list += ", "; list += i->extTitleId; } } return(list); } void CatalogCopyRetriever::Retrieve(const InputDataList &inputDataList) { HeapHolder con(GetSqlServerConnection( "ffh-njfs02.films.com", "MK3", "sa", "ffhmk$09", "CatalogCopyRetriever")); // I'm not excluding non-positive list price items here, // because I want the end user to see that a zero price item // has been requested. HeapHolder getItemsStmt(con->prepareStatement( "select Item.FormatCode, " "coalesce(Item.FormatSubType, '') as FormatSubType, " "cast((Item.ListPrice * 100) as int) as ListPriceCents, " "coalesce(Item.ISBN, '') as ISBN, " "Item.ExtItemID " "from Item " "join Title on Title.ID = Item.TitleID " "join ItemFormat on ItemFormat.FormatCode = Item.FormatCode " "where " "Title.ExtTitleID = ? and " "(Item.DiscontinuedAsOfDate is null or " "Item.DiscontinuedAsOfDate >= current_timestamp) " "order by ItemFormat.Name ")); HeapHolder getTitleStmt(con->prepareStatement( "select " "cast(coalesce(Title.TitleFullNameHTML, '') as varchar(8000)) as TitleFullNameHTML, " "cast(coalesce(Title.FullDescriptionHTML, '') as varchar(8000)) as FullDescriptionHTML, " "cast(coalesce(Title.MediumCopyHTML, '') as varchar(8000)) as MediumCopyHTML, " "cast(coalesce(Title.ShortCopyHTML, '') as varchar(8000)) as ShortCopyHTML, " "case " " when Title.Review is not null or Title.ReviewHTML is not null then 1 " " else 0 " "end as HasReview, " "case " " when exists (select 1 from Title_ProductRight tpr where " " tpr.TitleID = Title.ID and tpr.ProductRightID = 3) then 1 " "else 0 " "end as HasCanadianRights, " "case " " when exists (select 1 from Title_ProductRight tpr where " " tpr.TitleID = Title.ID and tpr.ProductRightID = 12) then 1 " " else 0 " "end as HasVideoClip, " "ClosedCaptioned, " "coalesce(year(CopyrightDate), 0) as CopyrightYear, " "coalesce(Runtime, 0) as Runtime, " "Licensing.ExtLicenseCode as Licensing, " "case " " when exists (select 1 from LearningObject_Standard los " " join LearningObject lo on lo.ID = los.LearningObjectID " " where lo.TitleID = Title.ID) then 1 " " else 0 " "end as HasCorrelations, " "case " " when current_timestamp > Title.DoNotPromoteAfterDate then 1 " " else 0 " "end as IsPastDoNotPromoteAfterDate " "from Title " "join Licensing on Licensing.ID = Title.LicensingID " "where " "Title.ExtTitleID = ?")); HeapHolder getPrimaryItemStmt(con->prepareStatement( "select " "case " " when Item.FormatCode in ('A','K','KS') then 1 " " else 0 " "end as IsVideoFormat, " "case " " when Item.DiscontinuedAsOfDate <= current_timestamp then 1 " " else 0 " "end as IsDiscontinued, " "cast((Item.ListPrice * 100) as int) as ListPriceCents, " "case " " when Item.FormatCode = 'PO' then 1 " " else 0 " "end as IsPosterFormat " "from Item " "join Title on Title.ID = Item.TitleID " "where " "Title.ExtTitleID = ? and " "Item.IsPrimaryFormat = 1 ")); HeapHolder getSeriesComponentsStmt(con->prepareStatement( "select compTitle.ExtTitleID, " "cast((Item.ListPrice * 100) as int) as ListPriceCents, " "Item.FormatCode as PrimaryFormatCode " "from Title compTitle " "join Series on Series.ComponentTitleID = compTitle.ID " "join Title mainTitle on mainTitle.ID = Series.MainTitleID " "left outer join Item on Item.TitleID = compTitle.ID and " " Item.IsPrimaryFormat = 1 " "where " "mainTitle.ExtTitleID = ?")); // Some data required by Quark *os << "" << delimiter; string currentSeriesTitle; HeapHolder currentSeriesTitleData; HeapHolder currentSeriesComponents; HeapHolder > seriesComponentsFound; bool previousLineWasBlank = false; for (InputDataList::const_iterator i = inputDataList.begin(); i != inputDataList.end(); ++i) { string extTitleId(i->extTitleId); string comment(i->comment); HeapHolder copySelector(i->copySelector); bool isBlank(extTitleId.empty()); if (isBlank) { // Comments without a title ID is unexpected, but // dump it out. if (!comment.empty()) { *os << "@comment:" << comment << delimiter; } // Looks like we're in the middle of a series if (!currentSeriesTitle.empty()) { if (seriesComponentsFound->size() != currentSeriesComponents->size()) errorLog.Add("Blank input in series mode " "for series " + currentSeriesTitle + "; still missing title(s) " + GetMissingComponentsList( *currentSeriesComponents, *seriesComponentsFound)); HeapHolder seriesItemDataList( GetItemDataList(*getItemsStmt, currentSeriesTitle)); GenerateSeriesFooter(*currentSeriesTitleData, *currentSeriesComponents, *seriesItemDataList); // Leave series mode (even if not all components were found) currentSeriesTitle = string(); } if (!previousLineWasBlank) *os << "<\\b>" << delimiter; previousLineWasBlank = true; } else { previousLineWasBlank = false; if (IsNull(copySelector)) { errorLog.Add("No copy selector code was present " "for title " + extTitleId + ". Using long copy."); copySelector.reset(new LongCopySelector()); } HeapHolder titleData(GetTitle( *getTitleStmt, *getPrimaryItemStmt, extTitleId)); if (!IsNull(titleData)) { if (titleData->isDiscontinued) { errorLog.Add("Title " + extTitleId + " is DISCONTINUED. " "Skipping this title."); } else if (titleData->isPastDoNotPromoteAfterDate) { errorLog.Add("It is past the " "DO NOT PROMOTE AFTER DATE " "for title " + extTitleId + ". Skipping this title."); } else { // AJO 6/18/07 Per a conversation // with Jeanne, no longer check // for missing series titles, // to ensure that all titles // are not skipped. Of course, // we still need to detect the // end of a series so the footer // can be generated, but otherwise // create an entry. If the // intervening title is a series // itself, finish the previous // series and start the new one; if it's // not in the series, finish the // previous series anyway. HeapHolder seriesComponentList(GetSeries( *getSeriesComponentsStmt, extTitleId)); bool shouldEndSeriesBefore = false; bool shouldEndSeriesAfter = false; // In a series if (!currentSeriesTitle.empty()) { // The title is in the series if (find_if(currentSeriesComponents->begin(), currentSeriesComponents->end(), SeriesComponentTitleFinder(extTitleId)) != currentSeriesComponents->end()) { // Ouch, series-in-series if (!seriesComponentList->empty()) { errorLog.Add("Series title " + currentSeriesTitle + " is " "misconfigured; it contains another series title " + extTitleId); shouldEndSeriesBefore = true; } seriesComponentsFound->push_back(extTitleId); // If we've got all the components, end the series after the // last component is output if (seriesComponentsFound->size() == currentSeriesComponents->size()) { shouldEndSeriesAfter = true; } } else // the title is not in the series { errorLog.Add("Title " + extTitleId + " is " "not in series title " + currentSeriesTitle + "; still missing title(s) " + GetMissingComponentsList( *currentSeriesComponents, *seriesComponentsFound)); shouldEndSeriesBefore = true; } } if (shouldEndSeriesBefore) { HeapHolder seriesItemDataList( GetItemDataList( *getItemsStmt, currentSeriesTitle)); GenerateSeriesFooter( *currentSeriesTitleData, *currentSeriesComponents, *seriesItemDataList); currentSeriesTitle = string(); } // Is a series, so dump header if (!seriesComponentList->empty()) { GenerateSeriesHeader( *titleData, comment, *copySelector); currentSeriesTitle = extTitleId; currentSeriesTitleData = titleData; currentSeriesComponents = seriesComponentList; seriesComponentsFound.reset( new vector()); } // Normal non-series title else { HeapHolder itemDataList( GetItemDataList( *getItemsStmt, extTitleId)); GenerateTitleOutput(*titleData, *itemDataList, comment, *copySelector); } if (shouldEndSeriesAfter) { HeapHolder seriesItemDataList( GetItemDataList( *getItemsStmt, currentSeriesTitle)); GenerateSeriesFooter( *currentSeriesTitleData, *currentSeriesComponents, *seriesItemDataList); currentSeriesTitle = string(); } } } } } if (!errorLog.GetLog().empty()) errorFile << errorLog.GetLog(); } string Clean(const string &s) { string w(s), lastw; while (lastw != w) { lastw = w; w = StrippedOf(w, ' ', BOTH_ENDS); w = StrippedOf(w, '\n', BOTH_ENDS); if (!w.empty() && w.at(0) == '"' && w.at(w.length() - 1) == '"') w = StrippedOf(w, '"', BOTH_ENDS); } return(w); } HeapHolder GetInputDataList(istream &is, char delimiter) { HeapHolder inputDataList(new InputDataList); string line; // Skip the first header line if (!getline(is, line, delimiter).good()) throw runtime_error("Error reading header line of input file"); while (getline(is, line, delimiter).good()) { vector cols(ParseColumns(line)); string titleId; string comment; HeapHolder copySelector; if (cols.size() >= 1) comment = Clean(cols.at(0)); if (cols.size() >= 3) titleId = Clean(cols.at(2)); if (cols.size() >= 4) { string copySelectorCode(Clean(cols.at(3))); if (copySelectorCode == "L" || copySelectorCode == "l") copySelector.reset(new LongCopySelector()); else if (copySelectorCode == "M" || copySelectorCode == "m") copySelector.reset(new MediumCopySelector()); else if (copySelectorCode == "S" || copySelectorCode == "s") copySelector.reset(new ShortCopySelector()); // If an invalid copy selector, we leave it null } inputDataList->push_back(InputData( titleId, comment, copySelector)); } return(inputDataList); } void Run(const string &inputFileName, const string &outputFileName, const string &errorFileName, const string &neededIsbnFileName, bool isMaster, bool includeCorrelations) { static const char delimiter('\r'); ifstream is(inputFileName.c_str(), ios::in | ios::binary); HeapHolder inputDataList(GetInputDataList(is, delimiter)); // Output file needs to be binary for Mac compatibility ofstream outputFile(outputFileName.c_str(), ios::out | ios::binary); ofstream errorFile(errorFileName.c_str()); ofstream neededIsbnFile(neededIsbnFileName.c_str()); CatalogCopyRetriever retriever(delimiter, outputFile, errorFile, neededIsbnFile, isMaster, includeCorrelations); retriever.Retrieve(*inputDataList); } int main(int argc, char *argv[]) { try { string usageDescription( "usage: CatalogCopyRetriever " "[inputFile] [outputFile] [errorFile] [neededIsbnFile] " "{MASTER_CATALOG} {INCLUDE_CORRELATIONS}"); if (argc < 4 || argc > 6) throw runtime_error(usageDescription); string inputFile(argv[1]); string outputFile(argv[2]); string errorFile(argv[3]); string neededIsbnFile(argv[4]); bool isMaster = false; bool includeCorrelations = false; for (int i = 5; i < argc; ++i) { if (strcmp(argv[i], "MASTER_CATALOG") == 0) isMaster = true; else if (strcmp(argv[i], "INCLUDE_CORRELATIONS") == 0) includeCorrelations = true; else throw runtime_error(usageDescription); } Run(inputFile, outputFile, errorFile, neededIsbnFile, isMaster, includeCorrelations); } catch (const exception &e) { cerr << e.what() << endl; } catch (...) { cerr << "unexpected error" << endl; } return(0); }