Whilst trying to deploy an Apex class and test to our production org, I get this error without any context. The class runs fine in sandbox and the test passes with 92% coverage.
✘ Deploying Metadata 1m 8.71s ▸ Components: 1/1 (100%)
Test Results Summary
Passing: 0
Failing: 0
Total: 0
Error (10): stringWidth is not a function
This 'stringWidth' variable or method is not used or referred to anywhere in this class or any of the other classes being called, or even in our entire repo for that matter.
I'm wondering if it's part of the Regex or something that I've used to manipulate strings in some places, however I've done some testing by commenting out these blocks of code and it still gives the same error.
So I'm stumped. Any ideas greatly appreciated.
Here's the class:
global class HSEDataScraperBatch implements Database.Batchable<String>, Database.AllowsCallouts {
global List<String> allCaseNumbers = new List<String>();
global List<String> companyMatchers = new List<String>{
'Limited', 'Ltd', 'Council', 'Association', 'Trust', 'Society','University', 'College', 'School','Institute',
'Academy', 'Centre','Group', 'Partnership', 'Services','Service', 'Company', 'Corporation','Contractor', '('
};
global Iterable<String> start(Database.BatchableContext BC) {
String url = 'https://resources.hse.gov.uk/convictions/breach/breach_list.asp?PN=1&ST=B&SO=DHD';
String htmlBody = makeHttpCall(url);
if (String.isNotBlank(htmlBody)) {
Integer totalPages = getTotalPages(htmlBody);
for (Integer page = 1; page <= totalPages; page++) {
String paginatedUrl = url.replace('PN=1', 'PN=' + page);
String paginatedHtml = makeHttpCall(paginatedUrl);
if (String.isNotBlank(paginatedHtml)) {
allCaseNumbers.addAll(extractFirstColumnValues(paginatedHtml));
}
}
}
if(Test.isRunningTest()) {
Integer maxCasesInTest = 20;
List<String> limitedCaseNumbers = new List<String>();
for (Integer i = 0; i < Math.min(maxCasesInTest, allCaseNumbers.size()); i++) {
limitedCaseNumbers.add(allCaseNumbers[i]);
}
return limitedCaseNumbers;
}
return allCaseNumbers;
}
global void execute(Database.BatchableContext BC, List<String> caseNumbers) {
Set<String> existingCaseNumbers = new Set<String>();
Set<String> newCaseNumbers = new Set<String>();
List<Lead> leadsToEnrich = new List<Lead>();
List<Lead> newLeads = new List<Lead>();
for (Lead lead : [SELECT HSE_Case_No__c FROM Lead WHERE HSE_Case_No__c != null]) {
existingCaseNumbers.add(lead.HSE_Case_No__c);
}
for (String caseNo : caseNumbers) {
String simplifiedCaseNo = caseNo.substringBefore('/');
if (!newCaseNumbers.contains(simplifiedCaseNo) && !existingCaseNumbers.contains(simplifiedCaseNo)) {
String detailsUrl = 'https://resources.hse.gov.uk/convictions/breach/breach_details.asp?SF=BID&SV=' + caseNo.replace('/', '');
String detailsHtml = makeHttpCall(detailsUrl);
newCaseNumbers.add(simplifiedCaseNo);
if (String.isNotBlank(detailsHtml)) {
CaseDetails caseDetails = extractCaseDetails(detailsHtml, simplifiedCaseNo);
Lead newLead = (createLead(caseDetails));
// Seperating the leads with and without postcode/company, then merging them for insert. To avoid modifying the enrichment class, as other classes use it.
if(newLead.PostalCode != null && newLead.Company != null) {
leadsToEnrich.add(newLead);
}
else {
newLeads.add(newLead);
}
}
}
}
if (!leadsToEnrich.isEmpty()) {
List<Lead> enrichedLeads = GooglePlacesLeadEnrichment.googlePlacesSearch(leadsToEnrich);
newLeads.addAll(enrichedLeads);
insert newLeads;
}
else if (!newLeads.isEmpty()) {
insert newLeads;
}
}
global void finish(Database.BatchableContext BC) {
System.debug('Batch job completed.');
}
private String makeHttpCall(String url) {
try {
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint(url);
request.setMethod('GET');
HttpResponse response = http.send(request);
if (response.getStatusCode() == 200) {
return response.getBody();
} else {
System.debug('HTTP call failed with status code: ' + response.getStatusCode());
}
} catch (Exception e) {
System.debug('Exception during HTTP call: ' + e.getMessage());
}
return null;
}
private Integer getTotalPages(String html) {
Integer totalPages = 0;
String pageTextStart = 'Showing Page ';
String pageTextEnd = ', results';
Integer startIdx = html.indexOf(pageTextStart);
Integer endIdx = html.indexOf(pageTextEnd, startIdx);
if (startIdx != -1 && endIdx != -1) {
String pageText = html.substring(startIdx, endIdx);
Integer ofIdx = pageText.indexOf('of ');
if (ofIdx != -1) {
String totalPagesText = pageText.substring(ofIdx + 3).trim();
totalPages = Integer.valueOf(totalPagesText);
}
}
return totalPages;
}
private List<String> extractFirstColumnValues(String htmlBody) {
List<String> firstColumnValues = new List<String>();
Integer startIdx = htmlBody.indexOf('<table');
Integer endIdx = htmlBody.indexOf('</table>', startIdx) + 8;
if (startIdx != -1 && endIdx != -1) {
String tableHtml = htmlBody.substring(startIdx, endIdx);
List<String> rows = tableHtml.split('<tr>');
for (String row : rows) {
if (row.contains('<td>')) {
Integer tdStart = row.indexOf('<td>');
Integer tdEnd = row.indexOf('</td>', tdStart);
if (tdStart != -1 && tdEnd != -1) {
String cellData = row.substring(tdStart + 4, tdEnd).replaceAll('<.*?>', '').trim();
if (!cellData.contains('&')) {
firstColumnValues.add(cellData);
}
}
}
}
}
return firstColumnValues;
}
private CaseDetails extractCaseDetails(String html, String caseNo) {
CaseDetails caseDetails = new CaseDetails();
caseDetails.caseNumber = caseNo;
caseDetails.defendant = extractValue(html, '<td><strong>Defendant</strong></td>\\s*<td[^>]*>([\\s\\S]*?)</td>');
caseDetails.courtName = extractValue(html, '<td><strong>Court Name</strong></td>\\s*<td[^>]*>([\\s\\S]*?)</td>');
caseDetails.courtLevel = extractValue(html, '<td><strong>Court Level</strong></td>\\s*<td[^>]*>([\\s\\S]*?)</td>');
caseDetails.act = extractValue(html, '<td><strong>Act</strong></td>\\s*<td[^>]*>([\\s\\S]*?)</td>');
caseDetails.regulation = extractValue(html, '<td><strong>Regulation</strong></td>\\s*<td[^>]*>([\\s\\S]*?)</td>');
caseDetails.dateOfHearing = extractValue(html, '<td><strong>Date of Hearing</strong></td>\\s*<td[^>]*>([\\s\\S]*?)</td>');
caseDetails.result = extractValue(html, '<td><strong>Result</strong></td>\\s*<td[^>]*>([\\s\\S]*?)</td>');
caseDetails.fine = extractValue(html, '<td><strong>Fine</strong></td>\\s*<td[^>]*>([\\s\\S]*?)</td>');
caseDetails.industry = extractValue(html, '<td><strong>Industry</strong></td>\\s*<td[^>]*>([\\s\\S]*?)</td>');
caseDetails.address = extractValue(html, '<td rowspan="5"><strong>Address</strong></td>\\s*<td rowspan="5">([\\s\\S]*?)</td>').replace('<BR>', ', ');
return caseDetails;
}
private Lead createLead(CaseDetails caseDetails) {
Lead newLead = new Lead();
if (companyMatchers.contains(caseDetails.defendant)
) {
newLead.LastName = 'Not Known';
} else {
List<String> nameParts = caseDetails.defendant.split(' ');
if (nameParts.size() > 1) {
newLead.FirstName = nameParts[0];
newLead.LastName = nameParts[nameParts.size() - 1];
} else {
newLead.LastName = caseDetails.defendant;
}
}
if (caseDetails.address != null) {
newLead.PostalCode = extractValue(caseDetails.address, '([A-Z]{1,2}[0-9R][0-9A-Z]? [0-9][A-Z]{2})');
}
newLead.Company = caseDetails.defendant.replace('&', '&');
newLead.HSE_Case_No__c = caseDetails.caseNumber;
newLead.LeadSource = 'Health & Safety Executive';
newLead.Industry = caseDetails.industry != null ? caseDetails.industry : '';
newLead.Description = 'Case No: ' + caseDetails.caseNumber + '\n' +
'Court Name: ' + (caseDetails.courtName != null ? caseDetails.courtName : '') + '\n' +
'Court Level: ' + (caseDetails.courtLevel != null ? caseDetails.courtLevel : '') + '\n' +
'Act/Regulation: ' + (caseDetails.act != null ? caseDetails.act : caseDetails.regulation != null ? caseDetails.regulation : '') + '\n' +
'Date of Hearing: ' + caseDetails.dateOfHearing + '\n' +
'Result: ' + caseDetails.result + '\n' +
'Fine: ' + caseDetails.fine + '\n';
return newLead;
}
private String extractValue(String html, String regex) {
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(html);
if (matcher.find()) {
return matcher.group(1).trim().replaceAll('<.*?>', '');
}
return null;
}
global class CaseDetails {
public String caseNumber;
public String defendant;
public String courtName;
public String courtLevel;
public String act;
public String regulation;
public String dateOfHearing;
public String result;
public String fine;
public String industry;
public String address;
}
}