package gw.solr.utils uses gw.api.database.Query uses gw.plugin.solr.SolrSearchStore uses gw.solr.mapper.BuildDate uses org.apache.solr.client.solrj.response.QueryResponse uses org.apache.solr.common.SolrDocument uses org.apache.solr.common.SolrDocumentList uses org.apache.solr.common.util.NamedList uses org.json.simple.JSONArray uses org.json.simple.JSONObject uses java.lang.Exception uses java.lang.Float uses java.lang.Integer uses java.lang.Math uses java.util.ArrayList uses java.util.Date uses java.util.Map uses java.util.Set uses org.slf4j.Logger uses java.util.List uses gw.api.upgrade.Coercions uses gw.api.extension.ExtensionInterfaces uses gw.solr.ISolrPolicyResultDocument uses gw.solr.request.AbstractIndexDocument class SolrConsistencyChecker { protected static final var LOGGER : Logger = ExtensionInterfaces.IPCSolrUtilsExtensions.Logger var _client : SolrConsistencyQueryHandler var _trackProcessedUrns : boolean as TrackProcessedUrns var _pageSize = 10 var _maxFailures = 25 var _restrictToPeriods : List as RestrictToPeriods = {} // If populated, pretend the local DB only has these periods. Useful for unit testing construct(queryHandler : SolrConsistencyQueryHandler, maxFailures : int){ _maxFailures = maxFailures _client = queryHandler } construct(maxFailures : int){ _maxFailures = maxFailures _client = new SolrConsistencyQueryHandler() } function check() : SolrConsistencyReport{ var report = new SolrConsistencyReport() processSolrDatabase(report) if (shouldContinue(report)) { processPolicyCenterDatabase(report) } return report } // -------- Loop through Solr Index documents and compare them to PolicyCenter policy periods -------- private function processSolrDatabase(report : SolrConsistencyReport){ LOGGER.debug("Processing solr database documents") var response = _client.standardQuery("*:*", 0, _pageSize) var result = response.Results processSolrQueryResult(response, report) // process additional result pages var numFound = result.NumFound var numPages = Math.ceil((numFound * 1.0) / _pageSize) as Integer if (numPages > 0){ for (i in 1..|numPages) { LOGGER.debug("Processing solr query result page ${i + 1}") response = _client.standardQuery("*:*", i * _pageSize, _pageSize) processSolrQueryResult(response, report) } } } private function processSolrQueryResult(response : QueryResponse, report : SolrConsistencyReport){ var docs = response.Results for (solrDoc in docs){ testSolrIndexDocument(solrDoc, report) if (!shouldContinue(report)) { return } } } private function testSolrIndexDocument(solrDoc : SolrDocument, report : SolrConsistencyReport){ report.DocumentsProcessed = report.DocumentsProcessed + 1 var parsedSolrDoc : ISolrPolicyResultDocument // Ensure the document can be processed try{ var searchRequest = ExtensionInterfaces.ISolrPolicySearchRequestFactory.newInstance(ExtensionInterfaces.ISolrPolicySearchCriteriaFactory.newInstance()) parsedSolrDoc = searchRequest.processQueryResponse(wrapInResponseSolrDocument(solrDoc), 0, SolrSearchStore.ACTIVE).single() } catch (ex : Exception){ addFailure(report, CouldNotParseResult, null, null, parsedSolrDoc, solrDoc, ex.Message) return } if ( _trackProcessedUrns ){ report.ProcessedUrns.add(parsedSolrDoc.URN) } // Fetch the policy period from the database var query = makeDBQuery().compare(PolicyPeriod#PublicID, Equals, parsedSolrDoc.PeriodID).select() // Ensure the period exists if (query.Count == 0){ addFailure(report, InvalidPeriod, parsedSolrDoc.PeriodID, null, parsedSolrDoc, solrDoc, null) return } // Ensure the period should be indexed var period = query.single() if ( !ExtensionInterfaces.IPCSolrUtilsExtensions.shouldIndexPeriod(period) ){ addFailure(report, ShouldNotIndexPeriod, period.PublicID, null, parsedSolrDoc, solrDoc, null) return } // Ensure the slice date exists if (! ExtensionInterfaces.ISolrPolicyIndexRequestExtensions.effDatesForPeriod(period).contains(parsedSolrDoc.SliceDate)){ addFailure(report, InvalidSlice, period.PublicID, parsedSolrDoc.SliceDate, parsedSolrDoc, solrDoc, null) return } period = period.getSlice(parsedSolrDoc.SliceDate) var expectedData = ExtensionInterfaces.ISolrPolicyIndexDocumentExtensions.createDocument(period, period.PNIContactDenorm) compareSolrDataToExpectedData(solrDoc, parsedSolrDoc, expectedData, report) } private function compareSolrDataToExpectedData(solrDoc : SolrDocument, parsedSolrDoc : ISolrPolicyResultDocument, expectedData : AbstractIndexDocument, report : SolrConsistencyReport){ var json = expectedData.asJSON() var expected = createMapFromJSON(json) var actual = createMapFromSolrDocument(solrDoc) // Remove version and timestamp fields actual.remove("_version_") actual.remove("timestamp") expected.eachKeyAndValue(\ k, v -> { // Ensure the solr document contains the field if (!actual.containsKey(k) and v.HasElements) { LOGGER.debug("for key " +k+ " on policy " +parsedSolrDoc.PeriodID+ " the XML contains no element, but expected contains " +v) addFailure(report, InvalidIndexData, parsedSolrDoc.PeriodID, parsedSolrDoc.SliceDate, parsedSolrDoc, solrDoc, "Index document is missing field '${k}'") return } // Ensure the solr document's field is correct var indexValues = actual.get(k) ?: {} var expectedValues = expected.get(k) ?: {} var disjunction = indexValues.disjunction(expectedValues) if (disjunction.HasElements){ addFailure(report, InvalidIndexData, parsedSolrDoc.PeriodID, parsedSolrDoc.SliceDate, parsedSolrDoc, solrDoc, "Field '${k}' value(s) wrong. Expected '${expectedValues}' but got '${indexValues}'") return } }) actual.eachKeyAndValue(\ k, v -> { if (!expected.containsKey(k) and v.HasElements) { addFailure(report, InvalidIndexData, parsedSolrDoc.PeriodID, parsedSolrDoc.SliceDate, parsedSolrDoc, solrDoc, "Index document has extra field '${k}' with value '${v}'") return } }) } // -------- Loop through PolicyCenter periods and ensure the corresponding index documents exist in SOLR -------- private function processPolicyCenterDatabase(report : SolrConsistencyReport){ //noinspection SelectWithLimitingFunctions We intentionally allow customers to post-filter here var periods = makeDBQuery() .select() .where(\ p -> ExtensionInterfaces.IPCSolrUtilsExtensions.shouldIndexPeriod(p)) for (period in periods){ processPeriod(period, report) if (!shouldContinue(report)) { return } } } private function processPeriod(period : PolicyPeriod, report : SolrConsistencyReport){ // Ensure that the indices exist in Solr for the period var response = _client.standardQuery("periodID:\"${period.PublicID}\"", 0, 100) var searchRequest = ExtensionInterfaces.ISolrPolicySearchRequestFactory.newInstance(ExtensionInterfaces.ISolrPolicySearchCriteriaFactory.newInstance()) var solrResult = searchRequest.processQueryResponse(response, 0, SolrSearchStore.ACTIVE) if (!solrResult.HasElements) { if (!laterSliceExistsInSolrWithSameURN(period, period.EditEffectiveDate)) { addFailure(report, MissingSolrDocument, period.PublicID, null, null, null, "Missing solr document(s) for period ${period.PublicID}") } return } var effectiveDates = period.Status == TC_BOUND ? period.AllEffectiveDates : {period.EditEffectiveDate} effectiveDates.each(\ d -> { var solrDoc = solrResult.firstWhere(\ s -> s.SliceDate == d) if (solrDoc == null) { if (!laterSliceExistsInSolrWithSameURN(period, d)) { addFailure(report, MissingSolrDocument, period.PublicID, d, null, null, "Missing solr document for period ${period.PublicID}, slice date ${d.XmlDateTime}") if (!shouldContinue(report)) { return } } } }) } private function laterSliceExistsInSolrWithSameURN(period : PolicyPeriod, date : Date) : boolean{ // Perhaps rewrite this to compare against later slices/periods in the db rather than in solr var q = "urn:\"policyPublicID="+ period.Policy.PublicID + "&solrUniqueKey=" + period.getSlice(date).SolrUniqueKey + "\"" var response = _client.standardQuery(q, 0, 1) var result = response.Results var numFound = result.NumFound if (numFound == 0) { return false } var doc = result.single() var indexedPeriodPublicID = doc.getFieldValue("periodID") as String if (indexedPeriodPublicID == period.PublicID) { var indexedSliceDate : Date if (_client.Integrated) { indexedSliceDate = Coercions.makeDateFrom(doc.getFieldValue("sliceDate")) } else { indexedSliceDate = BuildDate.DATE_PARSER.get().parse(doc.getFieldValue("sliceDate") as String) } return indexedSliceDate > date } var indexedPeriod = Query.make(PolicyPeriod).compare(PolicyPeriod#PublicID, Equals, indexedPeriodPublicID).select().first() if (indexedPeriod == null) { return false } return isLaterPeriod(indexedPeriod, period) } // --------- Helper methods ---------- private function shouldContinue(report : SolrConsistencyReport) : boolean{ return report.Failures.size() <= _maxFailures } private function createMapFromJSON(j : JSONObject) : Map>{ var removeSet : Set = {"$BeanVersions$"} // we don't want this one in our map var mapped = j.Keys.mapToKeyAndValue( \ k : Object -> { return k as String }, \ k : Object -> { var o = j.get(k) if (o typeis JSONArray) { if (o.Empty or o.first() typeis String) { return o.map(\ e : Object -> {return e as String}) } else { // don't blow up here...but we could have problems down the line! var m = o.map(\ e : Object -> {return e.toString()}) if (not removeSet.contains(k as String)) { print("createMapFromJSON: non-string array for key " +k) print(" -> " + m) } return m } } else if(o typeis Date) { return { BuildDate.DATE_FORMATTER.format(o) } } return {j.get(k) as String} }) removeSet.each(\ k -> mapped.remove(k)) return mapped } private function createMapFromSolrDocument(doc : SolrDocument) : Map>{ var map : Map> = {} doc.eachKeyAndValue(\ key, value -> { if("score" == key) { // skip score field } else if(value typeis List) { var nestedList = new ArrayList() value.each( \ x -> nestedList.add(x?.toString()) ) map.put(key, nestedList) } else if (value typeis Date and _client.Integrated) { map.put(key, { BuildDate.DATE_FORMATTER.format(value) }) } else { map.put(key, { value?.toString() }) } }) return map } private function wrapInResponseSolrDocument(solrDoc : SolrDocument) : QueryResponse { var response = new QueryResponse() var result = new NamedList() var header = new NamedList() header.add("name", "response") header.add("start", "0") header.add("maxScore", Float.valueOf(1.0)) result.add("responseHeader", header) var solrDocs = new SolrDocumentList() solrDocs.add(solrDoc) solrDocs.NumFound = 1 result.add("response", solrDocs) response.Response = result return response } private function addFailure( report : SolrConsistencyReport, type : SolrConsistencyFailure.FailureType, periodPublicID : String, sliceDate : Date, parsedDoc : ISolrPolicyResultDocument, resultDoc : SolrDocument, message : String ){ report.Failures.add( new SolrConsistencyFailure() { :Type = type, :PeriodPublicID = periodPublicID, :SliceDate = sliceDate, :Document = parsedDoc, :SolrDocument = resultDoc, :Message = message }) } private function makeDBQuery() : Query{ var query = Query.make(PolicyPeriod) if (_restrictToPeriods.HasElements) { query.compareIn(PolicyPeriod#PublicID, _restrictToPeriods.toArray()) } return query } private function isLaterPeriod(a : PolicyPeriod, b : PolicyPeriod) : boolean{ if (a.TermNumber == b.TermNumber) { return (a.ModelNumber ?: 0) > (b.ModelNumber ?: 0) } return a.TermNumber > b.TermNumber } }