Warning to Fake Blog Followers

I have received a number of suspect email follower signups with Outlook addresses but have not received page views for them.

Upon further investigation, it appears I am not the only one getting fake followers.

Please, if you have no interest in my blog, do not sign up for notifications. I will remove the emails of anyone I consider suspect.

Thank you for your attention to this matter.

Advertisements

Adding a Duplicate Related List to Leads

cropped-apex-logo-copy1.pngIn this post I will show you how to add a duplicate related list to the Lead object. The code can be modified to find duplicates on most objects.

These will be the tabs you create and for illustrative purposes, I have put the tabs next to the Lead tab.

tabs

Find Dups is a Visual Force tab for the Visual Force page Find Duplicates.

VF

This Visual Force page has two buttons. The Delete All Records button only deletes records created in the Unique and Dup tables. It will not delete your leadsThe Find Duplicates button, will look for exact matches in the Lead object using Company Name, Street, City, State and ZIP. You can modify the APEX code to customize which fields are compared.

When the batch APEX has processed, click on the Unique tab to see a list of unique leads together with the number of duplicates found for each unique record.

Unique

 

If you click on the hyperlinked Unique Record ID, it will pull up the detail page and show the related Dups list.

 

Unique1.JPG

Unique2.JPG

Now click on one of the Duplicate Record IDs to pull up the detail page of the Dup record:

 

Dup

To see the actual Lead record this dup is related to, click the Lead Look Up field:

 

Under the Dups related list on the Lead detail page are the matching dups for the lead you just saw earlier…

Lead

LeadDups

 

Here is the Schema Builder of the objects:

Schema

 

To create this app:

First create two custom objects called Unique and Dup by following the instructions below. Dup is a child of Unqiue so you will need to create a Master-Detail relationship between these two objects. Dup also has a lookup relationship to Lead. This creates the related list Dup on the Lead detail page.

Create an object called Unique with API Name Unique__c. 

When creating the object, add related lists, notes and attachments and create a tab.

The object name should be Unique Record ID and formatted as Auto Number with the display format as U-{00000000} and beginning at 1.

Custom Fields

Lead Name, Lead_Name__c, Text (255)
Number of Duplicates, Number_of_Duplicates__c, Roll-Up Summary (COUNT Dup)
Unique Lead ID, Unique_Lead_ID__c, Text (18)
Unique Record, Unique_Record__c, Text (255) (Unique Case Insensitive)
Create an object called Dup with API Name Dup__c

When creating the object, add related lists, notes and attachments and create a tab.

The object name should be Duplicate Record ID and formatted as Auto Number with the display format as D-{00000000} and beginning at 1.

Custom Fields

Duplicate Lead ID, Duplicate_Lead_ID__c, Text (18)
Duplicate Match, Duplicate_Match__c, Text (255)
Lead Look Up, LeadName__c, Lookup (Lead)
Unique Lead ID, Unique_Lead_ID__c, Text (18)
Unique Record ID, Unique_Record_ID__c, Master-Detail (Unique)

 

For the Unique object List View,

Select the following fields in the order they are displayed: Record ID, Unique Record ID, Number of Duplicates, Unique Lead ID, Lead Name, Unique Record

For the Dup object List View:

Select the following fields in the order they are displayed: Record ID, Unique Record ID, Duplicate Record ID, Unique Lead ID, Duplicate Lead ID, Lead Look Up, Duplicate Match

Edit the Lead page layout and modify the Dup related list to show the fields in the following order: Record ID, Unique Record ID, Unique Lead ID, Duplicate Record ID, Duplicate Lead ID and Duplicate Match.

Create the following APEX classes:

public class FindDup {

//this class has web service call outs from the Visual Force page Find Dups

public FindDup(ApexPages.StandardController controller) {

}


 public void FindDup(){
 
 BatchFindDups2 obj = new BatchFindDups2();
 
 Database.executebatch(obj,200); //use 1,000 as batchable chunk avoids 'Apex CPU time limit exceeded' error
 
 system.debug(obj);
 }


 public void DeleteAllRecords(){
 
 //this code also delete the child records in the Dup object
 
 BatchMassDeleteUnique objU = new BatchMassDeleteUnique();
 
 Database.executebatch(objU,2000); 
 
 system.debug(objU);
 
 
 }
 
}

 

global class BatchMassDeleteUnique implements Database.Batchable<sObject>,Database.stateful{
 
//this is a batch class that deletes the Unique and Dup records

 global final string query;
 
 global BatchMassDeleteUnique(){
 
 query = 'SELECT ID FROM Unique__c';

}
 
 global Database.QueryLocator start(Database.BatchableContext BC){
 
 return Database.getQueryLocator(query);
 }
 
 global void execute(Database.BatchableContext BC,List<sObject> scope){
 
 Database.delete(scope,false);
 } 
 
 global void finish(Database.BatchableContext BC){
 
 
 }

}

 

global class BatchFindDups2 implements Database.Batchable<sObject>,Database.Stateful{

//this is a batch class that looks for unique records and duplicate records
//and puts them in objects

global final string query;
 
global Map<String, Id> uniqueRecord;

global BatchFindDups2(){
 
 uniqueRecord = new Map<String, ID>();
 
 query = 'SELECT ID, Name, Company, Street, City, State, PostalCode FROM Lead ORDER BY Company';


}


global Database.QueryLocator start(Database.BatchableContext BC){ 
 
 return Database.getQueryLocator(query);
 
}


global void execute(Database.BatchableContext BC,List<Lead> scope){

 String fields;
 
 ID uniqueID;
 
 Map<Id, Id> duplicateRecord = new Map<Id, Id>();

 list<Unique__c> uniqueInsert = new list<Unique__c>();
 list<Dup__c> duplicateInsert = new list<Dup__c>();
 
 
 try{
 
 for(Lead l:scope){ 
 
 fields = l.Company + '|' + l.Street + '|' + l.City + '|' + l.State + '|' + l.PostalCode; //concatenate fields
 
 if(uniqueRecord.containsKey(fields)){ //if field already exists in uniqueRecord map then add to duplicateRecord map

 duplicateRecord.put(l.ID,uniqueRecord.get(fields));
 
 }
 else{
 
 uniqueRecord.put(l.Company + '|' + l.Street + '|' + l.City + '|' + l.State + '|' + l.PostalCode, l.ID); //add to uniqueRecord map
 
 Unique__c uni = New Unique__c(); //add to uniqueRecord object
 
 uni.Unique_Lead_ID__c = l.ID;
 
 uni.Unique_Record__c = l.Company + '|' + l.Street + '|' + l.City + '|' + l.State + '|' + l.PostalCode; //Combine fields with pipe | separator
 
 uni.Lead_Name__c = l.name;
 
 uniqueInsert.add(uni);
 
 }
 
 }
 
 List<Database.SaveResult> updateResults = Database.insert(uniqueInsert, false); //false allows for partial inserts


//Get a list of newly created IDs
//Database.SaveResult only gives record Ids for new records so you must query them to pull back the complete object record

List<Id> listOfIds = new List<Id>();
 
 for (Database.SaveResult sr : updateResults) {
 
 if (sr.isSuccess()) {
 
 listOfIds.add(sr.getId());

}
 }
 
 
 //Lead_Name__c must be included in newUniqueRecords query - it makes the correct lookup from Dup to Lead possible. I have no idea how.
 
 List<Unique__c> newUniqueRecords = [Select Id, Lead_Name__c, Unique_Lead_ID__c, Unique_Record__c from Unique__c where Id in :listOfIds]; 

 
 for (ID dupID : duplicateRecord.keySet())
 {

 uniqueID = duplicateRecord.get(dupID);
 
 
 for(Unique__c u: newUniqueRecords){
 
 If (uniqueID == u.Unique_Lead_ID__c){

 
 Dup__c dup = New Dup__c();
 
 dup.Unique_Record_ID__c = u.Id;
 dup.LeadName__c = uniqueID; //somehow the Lookup Lead ID is resolved to the Lead name
 dup.Unique_Lead_ID__c = uniqueID;
 dup.Duplicate_Lead_ID__c = dupID;
 dup.Duplicate_Match__c = u.Unique_Record__c;
 
 duplicateInsert.add(dup);
 
 break; //exit out of loop if match found
 
 }

} 
 }
 
 insert duplicateInsert;
 
 }
 
 
 catch (Exception e) { 
 
 
 }
 
} 


global void finish(Database.BatchableContext BC){
 

}
}

 

Create a Visual Force page called FindDup and add a tab for it:

<apex:page standardController="Lead" extensions="FindDup" sidebar="false" showHeader="true" > 
 
 <apex:form >
 
 <p>Find Duplicates in Leads Object</p>
 <br></br>
 <p>Combine Company Name and Address</p>
 <br></br>
 
 
 <apex:commandButton action="{!DeleteAllRecords}" value="Delete All Records" id="deleteAll"/>
 
 <apex:commandButton action="{!FindDup}" value="Find Duplicates" id="findDup"/>
 
 </apex:form> 
 
</apex:page>

 

Create a custom report type called Dup based off the custom object Dup and save it under the Lead category.

 

So how does it work? The Visual Force page Find Dups has web service call outs which are activated by the two buttons, Delete All Records and Find Duplicates. The APEX class for this call out is called FindDup. When deleting records from the Unique and Dup tables, the maximum batch size of 2,000 records is used.

To find duplicates, the Find Dups class calls the BatchFindDups2 global batch class. The statement Database.Stateful is added as we need this batch to remember certain variables for each batch processed.

To minimize heap, stack and APEX time out errors, batch size is limited to 200 records. The global batch class queries the Lead object with the following SOQL:

query = ‘SELECT ID, Name, Company, Street, City, State, PostalCode FROM Lead ORDER BY Company’;

This query gets 200 records at a time from the Lead object and orders them by Company Name. If you wanted to change the fields to match on you would need to change this SOQL.

The global Database.QueryLocator start(Database.BatchableContext BC) allows Salesforce to query up to 50 million records.

The execute method of this batch concatenates the lead fields, separated by pipes, i.e., Tower Records|210 First Ave|New York City|NY 10021, and puts them into the uniqueRecord map as the unique key and the map value as the unique Lead ID. The map keeps its collection of records for each batch because we set this class to Database.Stateful and the map gets progressively larger as the system works its way through the queried record set.

Records in the uniqueRecord map are inserted into the Unique table and matching lead records are inserted into the duplicateRecord map with the matching Lead ID as the map’s unique key and the map value as the as the unique Lead ID. This is how the app matches leads with their duplicates. The duplicateRecord map is inserted into the Dup table.

To illustrate:

uniqueRecord map: Unique Key: Tower Records|210 First Ave|New York City|NY 10021; Value: 00167ujK891245ghUv (Unique Lead ID)

duplicateRecord map: Unique Key: 00189ujK8916Q5ghUv (Duplicate Lead ID); Value: 00167ujK891245ghUv (Unqiue Lead ID)

To connect the Dup records to the Lead object as a related list, we need to know the created record IDs of the Unique records that were previously inserted, as they contain the unique Lead ID. A query retrieves these records and the unique Lead ID is compared to the Lead ID in the duplicate record. If they match, then the unique Lead ID is saved in the Lead Lookup field of the Dup table.

The end result is the Unique table only contains unique leads and the duplicate table contains duplicate leads. This is a parent child relationship. The Dup table also has a lookup relationship to the Lead object. Neither of these objects contains the entire lead record except for the concatenated matching fields and Lead IDs.

The Unique table has a roll up summary field so it can count the number of child duplicate “leads.”

Remember the custom report type Dup you created earlier? You can use this to create a cross object report of all Leads with Dups. You will need to add a filter “Dup ID not equal to “null”. This is shown below:

report

 

I have not load tested this app. If you try it on large record sets and it causes APEX time out errors, try reducing the batch size to 150 or even 100.

This is not a fast method to find duplicates compared to some apps out there but it is simple enough that anyone can create it and modify it for their own needs. Whether you merge or delete the dups is entirely up to you – this app just shows you how to find them and add them as a related list.

 

 

 

 

Deduping in Salesforce

cropped-apex-logo-copy1.pngThis is a simple project that anyone with some APEX programming experience can achieve and there is lots of room for expanding it. The app is designed to find exact matches on the Lead object but could just as easily be changed to find matches on any object.

The app pulls records from the Lead object, analyzes them and puts unique leads in the Unique object and duplicate leads in the Duplicate object. It does not make any changes to the Lead object. You will still have to manually check the leads against records in both objects and they can be sorted alphabetically. You could make the Unique and Duplicate Lead IDs into URLs to easily pull up both records from Salesforce. You could also add a Unique checkbox on the Lead object which is ticked by the same APEX batch method that processes unique and duplicate records.

This is how it works:

A VisualForce page called FindDuplicates has two buttons, Delete All Records and Find Duplicates. The first button calls a custom webservice which clear the records from both the Unique and Duplicate custom objects.

FindDuplicates

VF

The second button calls a custom webservice through the APEX class FindDup. This in turn calls a batch APEX class BatchFindDups and queries the entire Lead object for all records. The class loops through each lead record combining the Company Name, Street, City, State and ZIP into one field separated by pipes ‘|’.  This field and its Lead ID are inserted into the Unique object. The Unique Match field on this object is set to unique so it cannot have more than one of the same record. If a duplicate record is inserted, this violates the unique constraints of the field and the record is “kicked out” and put into the Duplicate object via an insert method.

Example:

The app encounters the first instance of the following record: Abbott Insurance, VA (no street, city or state listed). The fields are combined with a pipe ‘|’ separating each field: Abbott Insurance|null|null|VA|null. This field and its Lead ID are put into the Unqiue object as a Unique record. If a second lead of this record is found, the unique constraints of the Unique Record will prevent the lead from being saved twice. It’s Lead ID is put into the Duplicate object.

Original Lead: Abbott Insurance, VA (no street, city or state listed)

When running the app, the unique records are put into the Unique object…

Unqiue

and the duplicate matches are put into the Duplicate object. In this example, there are two duplicate records of Abbott Insurance.

Duplicate

Ingredients:

Two custom objects: Unique and Duplicate
Two custom record types: Unique and Duplicate
Two reports using the record types: Unique and Duplicate
Modified List Views of both Unique and Duplicate objects showing all fields
One Visual Force page: FindDuplicate
Four APEX classes: FindDup, BatchMassDeleteUnique, BatchMassDeleteDuplicate, BatchFindDups


Recipe:

To make the app, create the following objects, Unique and Duplicate, with tabs and using the information provided below:

Unique (custom object)

Singular Label: Unique
Plural Label: Unique
Object Name: Unique
API Name: Unique__c

Enable Reports: Yes
Allow Bulk API Access: Yes
Deployment Status: Deployed

Standard Field:

Record Name: Unique Record ID (label), Auto Number U {000000}, starts at 1

Custom Fields:

Unique Lead ID, Unique_Lead_ID__c, Text (18)
Unique Record, Unique_Record__c, Text (255) (Unique Case Insensitive)

Change the list view to see the following:

Record ID, Unique Record ID, Unique Lead ID, Unique Record

 

Duplicate (custom object)

Singular Label: Duplicate
Plural Label: Duplicates
Object Name: Duplicate
API Name: Duplicate__c

Enable Reports: Yes
Allow Bulk API Access: Yes
Deployment Status: Deployed

Standard Field:

Record Name: Duplicate Record ID (label), Auto Number D {000000}, starts at 1

Custom Fields:

Duplicate Lead ID, Duplciate_Lead_ID__c, Text (18)
Duplicate Match, Duplicate_Match__c, Text (255)

Change the list view to see the following:

Record ID, Duplicate Record ID, Duplicate Lead ID, Duplicate Match

 

Create two record types and call them Unique and Duplicate and save them under the Lead report folder. Create a report for each record type and add all of the fields.

Create an APEX class called FindDup. This will extend the Standard Lead Object Controller of the Visual Force page FindDuplicates and is a web service call out for the buttons that will be placed on the Visual Force page.

public class FindDup {

public FindDup(ApexPages.StandardController controller) {

}


 public void FindDup(){
 
 BatchFindDups obj = new BatchFindDups();
 
 Database.executebatch(obj,1500); //use 1,500 as batchable chunk avoids 'Apex CPU time limit exceeded' error
 
 system.debug(obj);
 
 }


 public void DeleteAllRecords(){
 
 BatchMassDeleteUnique objU = new BatchMassDeleteUnique();
 
 Database.executebatch(objU,2000); 
 
 system.debug(objU);
 
 
 BatchMassDeleteDuplicate objD = new BatchMassDeleteDuplicate();
 
 Database.executebatch(objD,2000); 
 
 system.debug(objD);
 
 }

}

 

Create an APEX class called BatchMassDeleteUnique. This will delete records from the Unique table and is called from the FindDup class.

global class BatchMassDeleteUnique implements Database.Batchable<sObject>,Database.stateful{
 
 global final string query;
 
 global BatchMassDeleteUnique(){
 
 query = 'SELECT ID FROM Unique__c';

}
 
 global Database.QueryLocator start(Database.BatchableContext BC){
 
 return Database.getQueryLocator(query);
 }
 
 global void execute(Database.BatchableContext BC,List<sObject> scope){
 
 Database.delete(scope,false);
 } 
 
 global void finish(Database.BatchableContext BC){
 
 
 }
}

 

Create an APEX class called BatchMassDeleteDuplicate. This will delete records from the Duplicate table and is called from the FindDup class.

global class BatchMassDeleteDuplicate implements Database.Batchable<sObject>,Database.stateful{
 
 global final string query;
 
 global BatchMassDeleteDuplicate(){
 
 query = 'SELECT ID FROM Duplicate__c';

}
 
 global Database.QueryLocator start(Database.BatchableContext BC){
 
 return Database.getQueryLocator(query);
 }
 
 global void execute(Database.BatchableContext BC,List<sObject> scope){
 
 Database.delete(scope,false);
 } 
 
 global void finish(Database.BatchableContext BC){
 
 
 }
}

 

Create an APEX class called BatchFindDups. This class is called by the FindDup method in the FindDup class and the batch chunk is set to 1,500 to avoid APEX time out or heap errors. To avoid governor limits, the Database.QueryLocator method is used which returns up to 50 million records. Hopefully you won’t have this many records to work through!

The results of the query are checked one by one and the Lead name and address fields are concatenated with a pipe ‘|’ separator. All the processed records are inserted into the Unique object; any duplicates are kicked out and added to the map which stores the duplicate Lead IDs and the concatenated matching field for each duplicate record. The map is then inserted into the Duplicate object. When the process is complete, an email is sent to the user notifying them.

global class BatchFindDups implements Database.Batchable<sObject>{
 
 global final string query;
 
 global list<String>duplicateID = new List<String>();
 global list<String>duplicateFields = new List<String>();
 
 global list<Unique__c> uniqueInsert = new list<Unique__c>();
 global list<Duplicate__c> duplicateInsert = new list<Duplicate__c>();
 global List<string> exception_List;
 
 
 Map<Id,String> duplicateField = new Map<Id,String>();
 
 
 global BatchFindDups(){
 
 query = 'SELECT ID, Company, Street, City, State, PostalCode FROM Lead ORDER BY Company LIMIT 25000'; //ORDER BY CreatedDate
 
 }
 
 
 global Database.QueryLocator start(Database.BatchableContext BC){ 
 
 return Database.getQueryLocator(query);
 
 }
 
 
 global void execute(Database.BatchableContext BC,List<Lead> scope){
 
 system.debug(scope.size());
 
 
 try{
 
 for(Lead l:scope){ 
 
 Unique__c uni = New Unique__c();
 
 uni.Unique_Lead_ID__c = l.ID;
 
 uni.Unique_Record__c = l.Company + '|' + l.Street + '|' + l.City + '|' + l.State + '|' + l.PostalCode; //Combine fields with pipe | separator
 
 uniqueInsert.add(uni);
 
 }
 
 
 //Update List 
 
 List<Database.SaveResult> updateResults = Database.insert(uniqueInsert, false); //false allows for partial inserts
 
 
 duplicateField.clear();
 
 
 for(Integer i=0;i<updateResults.size();i++){
 
 if (updateResults.get(i).isSuccess()){
 
 updateResults.get(i).getId();


}else if (!updateResults.get(i).isSuccess()){
 
 uniqueInsert.get(i);//failed record from the list
 
 //system.debug('Duplicate ID '+ uniqueInsert.get(i).LeadID__c);
 
 duplicateField.put(uniqueInsert.get(i).Unique_Lead_ID__c, uniqueInsert.get(i).Unique_Record__c);
 
 }

} 
 
 
 system.debug(duplicatefield.size());
 
 
 duplicateinsert.clear();
 
 for (Id id : duplicateField.keySet())
 {
 Duplicate__c dups = New Duplicate__c();
 
 System.debug(id);
 System.debug(duplicateField.get(id));
 
 Dups.Duplicate_Lead_ID__c = id; //string.valueOf(l.ID).substring(0,15);
 
 dups.Duplicate_Match__c = duplicateField.get(id);
 
 duplicateInsert.add(dups);
 
 }
 
 insert duplicateinsert;
 
 }
 
 
 catch (Exception e) { 
 
 }
 
 } 
 
 
 global void finish(Database.BatchableContext BC){
 
 
 //Send an email to the User after your batch completes
 
 Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();

String[] toAddresses = new String[] {'youremail@xxx.com'};

mail.setToAddresses(toAddresses);

mail.setSubject('Batch Apex Job');

mail.setPlainTextBody('Batch Apex Job Complete');

Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });

}
}

 

Create a Visual Force page called FindDuplicates

<apex:page standardController="Lead" extensions="FindDup" sidebar="false" showHeader="true" > 
 
 <apex:form >
 
 <p>Find Duplicates in Leads Object</p>
 <br></br>
 <p>Combine Company Name and Address</p>
 <br></br>
 
 
 <apex:commandButton action="{!DeleteAllRecords}" value="Delete All Records" id="deleteAll"/>
 
 <apex:commandButton action="{!FindDup}" value="Find Duplicates" id="findDup"/>
 
 
 </apex:form> 
 
</apex:page>

 

Hopefully this example will show what can be done using APEX and a Visual Force page to find duplicates.

 

Tip for Refreshing Detail Page From an Inline Visual Force Page

cloudThis code assumes you’re using an inline Visual Force page in an object detail page, the Standard Controller with/without an extension and using the oncomplete attribute of an actionSupport tag to refresh the parent detail page.

I can’t guarantee this will work on all browsers, but if you’re having trouble refreshing an object detail page from code executed on an inline Visual Force page try the following:

Add a line to your Visual Force page to declare a variable which will hold the ID of your parent detail page. You’ll need to convert the parent record ID from 18 to 15 digits.

<apex:variable var=”a” value=”{!URLENCODE(LEFT(Account.id,15))}” />

Replace your oncomplete attribute line with the following:

oncomplete=”javascript:window.top.location.href=’/{!a}'” />

Avoid using the formula function in the oncomplete attribute as this will cause the Visual Force page to refresh itself with the parent detail page.

Hope this helps!

 

 

Automatically Refresh an APEX Job Page and an Object Detail Page Using a Custom Button – Part 2

cropped-apex-logo-copy1.pngcloudPlease visit part 1 of this blog post. I have extended the functionality of the automatic refresh of the APEX Job Status page so that when a user closes this window the Object detail page is also refreshed. This is useful when their are calculated fields on the detail page that will only update properly once an APEX batch job has completed. I have added three additional lines of code to the setInterval function:

setInterval(function(){if(apexjobstatus.closed){alert(‘Refreshing Screen’);document.location.reload(true);}apexjobstatus.history.go(0);}, 5000);

Let’s break this code down:

setInterval(function(){

        if(apexjobstatus.closed){

               alert('Refreshing Screen');

               document.location.reload(true);

           }

           apexjobstatus.history.go(0);

}, 5000);

The function setInterval checks to see if the apexjobstatus window is closed (refer to part 1 of this blog post). If the APEX window is open then this code never runs and the window will automatically refresh until the user has closed the window. Once this happens the If statement is true and an alert message pops up to let the user know the Object window is about to refresh. Once the user clicks OK, the Object window refereshes and the JavaScript code stops running.

Automatically Refresh an APEX Job Page Using a Custom Button

cropped-apex-logo-copy1.pngcloud
This post assumes you have a custom button that calls a web service to perform operations using batch APEX.

You have a custom button on a detail page that runs a large APEX batch job and you have custom fields on the detail page that only show data when the batch job has finished. It’s going to take a while for the job to finish and the only way to see the batch status is to go to to Setup–>Monitoring–>Apex Jobs and manually refresh the page every so often.

Wouldn’t it be nice if your custom button opened up the Apex Jobs page in a pop-up window and automatically refreshed it every few seconds, allowing you to do something else in the meantime?

To do this, you will need to add two lines of code to your  function success code block which is called after a successful APEX Web Services call out. (Your custom button’s behavior should already be Execute JavaScript and the Content Source should be OnClick JavaScript).

apexjobstatus = window.open("/apexpages/setup/listAsyncApexJobs.apexp", "MyWinName", "height=500,width=1000")

setInterval(function(){apexjobstatus.history.go(0);}, 5000);

The first line of JavaScript declares variable apexjobstatus as the pointer to the APEX Jobs page URL and opens up the page in a pop up window. You can change the height and width variables of the pop-up window dimensions to suit your needs. The second line uses the setInterval function which continually refreshes the page every 5 seconds. The value of the time interval is in milliseconds, so a 3 second delay would be entered as 3000. Refreshing the detail page will stop the pop-up window from being refreshed.

Capture

Web Miner Part 1: Adding and Deleting Remote Site URLS programmatically

cropped-apex-logo-copy1.pngImagine being able to go into Salesforce and add leads directly from a website without having to copy and paste them into an Excel sheet and import them. This app will allow you to do that but it will only work with websites where the data is not embedded in Flash pages.

In this post I show you how to programmatically add Remote Sites using APEX and a Visual Force page. This is important because Salesforce will not permit the platform to access external sites without manually adding them and this is inconvenient for what we want to do.

When you have finished all four steps of this project, go to the Web Miner Visual Force page. You will see three menu options: Add Remote Site; View Remote Site; Get HTML data. Ignore the last option as in this post we’re focused on adding and removing Remote Site URLs.

Here is what you should see:

Pt1-001

 
To see a list of Remote Site URLs, click View Remote Sites. Click the Delete button to remove a site. You can click the site link to open the web site in a new window.

Pt1-002

 
To add a remote site, click Add Remote Site. Enter the Remote Site name and the URL and click Save Remote Site. The app will flash an error screen if you have a space in the Remote Site name or the URL does not conform. The app uses rendering tags to only refresh the View Remote Site list.

In the example below, we will add the SFDC99 blog to our list of Remote Sites.

Pt1-003

Pt1-004

 

And here is the newly added Remote Site site:

Pt1-005

To create this app, follow the directions below:

  1. Visit https://github.com/financialforcedev/apex-mdapi/blob/master/apex-mdapi/src/classes/MetadataService.cls and install the MetadataService apex class.
  2. Add the following endpoints to your Remote Site settings. This is required to call out the web service. Replace ‘org’ in the URLs below with your Salesforce instance, i.e. na35.
  3. Create an APEX class called WebMiner and copy and paste the code below into your developer console
    public class WebMiner {
               
        public string RemoteSiteURL { get; set; }
        public List<String> RemoteSites {get; set;}
        public string HTMLBody;
       
       
        //variables used in VisualForce
       
        public string iHTMLBody { get; set; }
        public string iRemoteSiteName { get; set; } // iRemoteSiteName value from vf page
        public string iRemoteSiteURL { get; set; } // iRemoteSiteURL value from vf page
        public string iURL { get; set; } // iURL value from vf page
       
       
        //declare and initalize variables to avoid returning a null value and crashing the code
              
        Public Map<String,String> mapRemoteSiteURL { get {
                                                   
            if(mapRemoteSiteURL == null) mapRemoteSiteURL = new Map<String,String> { };
    
                return mapRemoteSiteURL;
                                                       
            }set;}
        
        
        
        Public Map<String,String> mapRemoteSiteID { get {
                                                   
            if(mapRemoteSiteID == null) mapRemoteSiteID = new Map<String,String> { };
    
                return mapRemoteSiteID;
                                                       
            }set;}
    
    
        
        Public Map<String,String> mapRemoteSiteConfToken { get {
                                                   
            if(mapRemoteSiteConfToken == null) mapRemoteSiteConfToken = new Map<String,String> { };
    
                return mapRemoteSiteConfToken;
                                                       
            }set;}    
        
       
        
        public string RemoteSiteStatus { get {
           
            if (RemoteSiteStatus==null) RemoteSiteStatus='';
    
                    return RemoteSiteStatus;
    
            }set; }
      
       
        // constructor
       
        public WebMiner(ApexPages.StandardController controller){
           
           
        }
       
           
       
        public static MetadataService.MetadataPort createService(){
           
            MetadataService.MetadataPort service = new MetadataService.MetadataPort();
            service.SessionHeader = new MetadataService.SessionHeader_element();
            service.SessionHeader.sessionId = UserInfo.getSessionId();
            return service;
           
        }
       
       
          
        //public static void loadRemoteSites() {  //remove static
       
        public void loadRemoteSites() {
            
            string ConfirmationToken;
            
            //comment the variables out if running anon APEX
           
            //string HTMLBody;
            //string RemoteSiteURL;
            //Map<String,String> mapRemoteSiteURL = new Map<string,string>();
           
       
            //Get a list of Remote Site URLs
           
            MetadataService.MetadataPort service = createService();
           
            List<MetadataService.ListMetadataQuery> queries = new List<MetadataService.ListMetadataQuery>();
           
            MetadataService.ListMetadataQuery queryLayout = new MetadataService.ListMetadataQuery();
           
            queryLayout.folder = '';
           
            queryLayout.type_x = 'RemoteSiteSetting';
           
            queries.add(queryLayout);   
           
            MetadataService.FileProperties[] fileProperties = service.listMetadata(queries, 38);
           
           
            for(MetadataService.FileProperties fileProperty : fileProperties){
               
                           
                String requestUrl = '/' + fileProperty.ID;
                   
           
                // get the Remote Site page using the fileProperty.ID and scrape the Remote Site URL
           
                  if(!test.isRunningTest()){
               
                    PageReference pg = new PageReference( requestUrl );
               
                    HTMLBody = pg.getContent().toString();
               
                  }
           
           
                if (HTMLBody.Contains('Remote Site URL')){
                           
                    if(!test.isRunningTest()){
                        
                        if (HTMLBody.Contains('CONFIRMATIONTOKEN=')){
                            
                            ConfirmationToken = HTMLBody.mid(HTMLBody.indexOf('CONFIRMATIONTOKEN='),HTMLBody.length());
                        
                            ConfirmationToken = ConfirmationToken.left(110); //includes length of word 'CONFIRMATIONTOKEN='
                            
                            mapRemoteSiteConfToken.put(fileProperty.fullName, ConfirmationToken);
                            
                            System.debug(ConfirmationToken);
                        }
                                            
                        RemoteSiteURL = HTMLBody.mid(HTMLBody.indexOf('Remote Site URL'),HTMLBody.length());
                   
                        RemoteSiteURL = RemoteSiteURL.mid(RemoteSiteURL.indexOf('http'), RemoteSiteURL.length());
                   
                        RemoteSiteURL = RemoteSiteURL.left(RemoteSiteURL.indexof('<'));
                                           
                        mapRemoteSiteURL.put(fileProperty.fullName, RemoteSiteURL);
                        
                        mapRemoteSiteID.put(fileProperty.fullName, fileProperty.ID.left(15)); //must be 15 digit ID or won't work!
                                                       
                        System.debug(fileProperty.fullName + ' ' + fileProperty.ID + ' ' + RemoteSiteURL);
                                            
                      }
           
                }
                          
            }
           
           
            //If map size greater than 0
           
            If (mapRemoteSiteURL.size()>0){
           
                //for (String e : mapRemoteSiteURL.keySet()) {System.debug('Map results: ' + mapRemoteSiteURL.get(e));}
           
                //Sort the map
           
                RemoteSites = new List<String>();
                RemoteSites.addAll(mapRemoteSiteURL.keySet());
                RemoteSites.sort();
               
            }
           
        }
       
       
        public void saveRemoteSite()  {
                   
            system.debug('iRemoteSiteName: ' + iRemoteSiteName + ' iRemoteSiteURL: ' + iRemoteSiteURL);
                   
            boolean exists = false;
           
            //compare iRemoteSiteName to existing list of RemoteSiteNames
           
            If (mapRemoteSiteURL.size()>0){
          
                for (String e : mapRemoteSiteURL.keySet()) {
                             
                    System.debug('Map results: ' + e + ' ' + mapRemoteSiteURL.get(e));
                   
                    if (iRemoteSiteName == e){
                   
                        exists = true;
                   
                        RemoteSiteStatus = 'Remote Site ' + iRemoteSiteName + ' already exists';
                                   
                        System.debug('Exists: iRemoteSiteName: ' + iRemoteSiteName + ' mapRemoteSiteName: ' + e);
                   
                    }
                   
                }
                                 
            }
                                  
                   
            if (exists == false){
           
                //http://technome2.blogspot.com/2017/05/creating-remote-site-settings.html
           
                MetadataService.MetadataPort service = createService();
                MetadataService.RemoteSiteSetting remoteSiteSettings = new MetadataService.RemoteSiteSetting();
                remoteSiteSettings.fullName = iRemoteSiteName;
                remoteSiteSettings.url = iRemoteSiteURL;
                remoteSiteSettings.isActive=true;
                remoteSiteSettings.disableProtocolSecurity=false;
                service.createMetadata(new List<MetadataService.Metadata> { remoteSiteSettings });
               
                RemoteSiteStatus = 'Remote Site ' + iRemoteSiteName + ' saved';
               
                loadRemoteSites(); //this line must be here as it reloads the Remote Sites
                                      
            }       
                           
        }
       
      
      
        public void getHTMLBody() {
           
            string baseURL;
           
            boolean exists = false;
           
           
            system.debug(iURL);
           
           
           
            //get base root of iURL
           
            system.debug(baseURL);
                                       
                   
            //compare baseURL to existing list of RemoteSiteNames
           
            If (mapRemoteSiteURL.size()>0){
          
                for (String e : mapRemoteSiteURL.keySet()) {
                             
                    System.debug('Map results: ' + e + ' ' + mapRemoteSiteURL.get(e));
                   
                    if (baseURL == mapRemoteSiteURL.get(e)){
                   
                        exists = true;
                   
                        RemoteSiteStatus = 'URL ' + iURL + ' exists';
                                   
                        System.debug('Exists: iRemoteSiteName: ' + iRemoteSiteName + ' mapRemoteSiteName: ' + e);
                   
                    }
                   
                }
                                 
            }
           
           
            if (exists == false){
               
                RemoteSiteStatus = 'Website ' + iURL + ' does not exist in Remote Site table';
               
                System.debug('Website ' + iURL + ' does not exist in Remote Site table');
                           
            } 
           
           
            if (exists == true){
               
                RemoteSiteStatus = 'Website ' + iURL + ' exists in Remote Site table';
               
                System.debug('Site exists');
               
               
                //Get HTML body from website
               
                Http endpoint = new Http();
    
                HttpRequest req = new HttpRequest();
    
                req.setEndpoint(iURL);
    
                req.setMethod('GET');
    
                HttpResponse res = endpoint.send(req);
    
                HTMLBody = res.getBody();
           
                iHTMLBody = HTMLBody;
               
            }     
           
           
           
         
        }
          
    }
  4. Create a Visual Force page called WebMiner and copy and paste the code below into your developer screen.
    <apex:page standardcontroller="Lead" extensions="WebMiner" action="{!loadRemoteSites}">
    
    <!-- <apex:page standardcontroller="Lead" extensions="WebMiner"> <!--  action="{!getHTMLBody}"> -->
    
    <!-- <apex:outputText escape="false" value="{!iHTMLBody}"/> -->
    
    
     
    
        // JavaScript function to check the value entered in the inputField
    
        function ValidateInput() {
                   
            var iRemoteSiteName = document.getElementById('{!$Component.block1.section1.theForm.RemoteSiteName}').value;
            var iRemoteSiteURL = document.getElementById('{!$Component.block1.section1.theForm.RemoteSiteURL}').value;
            var regexQuery = "^(https?://)?(www\\.)?([-a-z0-9]{1,63}\\.)*?[a-z0-9][-a-z0-9]{0,61}[a-z0-9]\\.[a-z]{2,6}(/[-\\w@\\+\\.~#\\?&/=%]*)?$";
            var url = new RegExp(regexQuery,"i");
            var strError = ''
            var ErrorAlert = 'False';
         
    
            if (iRemoteSiteName == '') {
         
                ErrorAlert = 'True';
             
                strError = 'Please enter a Remote Site Name'
             
            }
         
         
            if (/\s/.test(iRemoteSiteName)){
              
                ErrorAlert = 'True';
             
                strError = strError + '\nRemote Site Name must not contain spaces'
              
            }
                       
                                   
         
            if (iRemoteSiteURL == '') {
         
                ErrorAlert = 'True';
             
                strError = strError + '\nPlease enter a Remote Site URL'
             
            }
         
            else {
                                   
                if (url.test(iRemoteSiteURL)) {
             
                    
             
                }
         
                else {
             
                    ErrorAlert = 'True';
             
                    strError = strError + '\nInvalid url: ' + iRemoteSiteURL;
         
                }
         
            }
                     
         
            if (ErrorAlert == 'False') {
         
                SaveRemoteSite();
                                                                                                         
            }
         
            else alert(strError);
                     
       
        }
       
       
        // JavaScript function to validate url
    
        function ValidateURL() {
           
            var iURL = document.getElementById('{!$Component.block3.section1.theForm.myURL}').value;
            var regexQuery = "^(https?://)?(www\\.)?([-a-z0-9]{1,63}\\.)*?[a-z0-9][-a-z0-9]{0,61}[a-z0-9]\\.[a-z]{2,6}(/[-\\w@\\+\\.~#\\?&/=%]*)?$";
            var url = new RegExp(regexQuery,"i");
            var strError = ''
            var ErrorAlert = 'False';
                                                
             
            if (iURL == '') {
         
                ErrorAlert = 'True';
             
                strError = 'Please enter a URL'
             
            }
           
            else {
           
                if (url.test(iURL)) {
             
                    
             
                }
         
                else {
             
                    ErrorAlert = 'True';
             
                    strError = strError + '\nInvalid url: ' + iURL;
         
                }
           
            }
           
           
            if (ErrorAlert == 'False') {
         
                
                                                                                                         
            }
         
            else alert(strError);     
       
        }
             
    
    
    
    
    
          
    <apex:pageBlock id="block1">
    
      <apex:pageBlockSection id="section1" collapsible="true" title="Add Remote Site">
       
          <apex:form id="theForm">
                         
              <apex:outputText value="Remote Site Name: " />
              <apex:inputText value="{!iRemoteSiteName}" id="RemoteSiteName" style="text-align:left" required="true"/>
           
              &nbsp; &nbsp;
                         
              <apex:outputText value="Remote Site URL: " />
              <apex:inputText value="{!iRemoteSiteURL}" id="RemoteSiteURL" style="text-align:left" required="true"/>
           
              &nbsp; &nbsp;
           
              <input type="button" value="Save Remote Site" onclick="ValidateInput();"/> <!-- call JS function to check integrity of values passed -->
           
           
              <!-- JS calls actionFunction "SaveRemoteSite" -->
              <apex:actionFunction name="SaveRemoteSite" action="{!saveRemoteSite}" rerender="thePanelWrapper" />
                                                                  
          </apex:form>
      
     </apex:pageBlockSection>
    
    </apex:pageBlock>
    
    
    
    
          
    <apex:pageBlock id="block2">
                   
       <apex:pageBlockSection id="section1" collapsible="true" title="View Remote Sites">
    
           <apex:form id="theForm">
          
               <apex:outputPanel id="thePanelWrapper">
        
                   <apex:pageBlock id="thePanel" > <!-- rendered="{! rend}" > -->
                                                           
                       <apex:pageBlockTable value="{!RemoteSites}" var="mapRemoteSiteKey" style="width:50%">
         
                           <!-- first column contains Remote Site Name -->
                           <apex:column value="{!mapRemoteSiteKey}" />
       
       
                           <!-- second column contains Remote Site URL -->
                           <apex:column >
                               
                               <!-- the line below opens the URL in a new window -->
                               <apex:outputLink value="{!mapRemoteSiteURL[mapRemoteSiteKey]}" target="_blank">{!mapRemoteSiteURL[mapRemoteSiteKey]}</apex:outputLink>
                             
                          </apex:column>
                     
                                              
                          <!-- third column contains Delete button which inc Remote Site ID and Confirmation Token -->
                          <apex:column >
                          
                              <input class="btn" value = "Delete" name="del" title = "Delete" onclick="if ((Modal.confirm && Modal.confirm('Are you sure?')) || (!Modal.confirm && window.confirm('Are you sure?'))) navigateToUrl('/setup/own/deleteredirect.jsp?delID={!mapRemoteSiteID[mapRemoteSiteKey]}&retURL=%2Fapex%2FWebMiner&_{!mapRemoteSiteConfToken[mapRemoteSiteKey]}','DETAIL','del');"/>  
                          
                          </apex:column>
                        
                        
    
                       </apex:pageBlockTable>
                  
                  
                       
                           
                            var res = "{!RemoteSiteStatus}";
                           
                            if (res != "" ){
                           
                                alert("{!RemoteSiteStatus}");
                           
                            }
                           
                       
                       
                   </apex:pageBlock>
                
               </apex:outputPanel>
        
          </apex:form>
                                                 
       </apex:pageBlockSection>
                                                  
    </apex:pageBlock>
    
    
    
    
    
    <apex:pageBlock id="block3">
    
      <apex:pageBlockSection id="section1" collapsible="true" title="Get HTML data">
       
          <apex:form id="theForm">
                         
              <apex:outputText value="URL: " />
              <apex:inputText value="{!iURL}" id="myURL" style="text-align:left" required="true"/>
                       
              <input type="button" value="Get URL" onclick="ValidateURL();"/> <!-- call JS function to check integrity of values passed -->
           
           
              <!-- JS calls actionFunction "GetHTMLBody" -->
              <!--  <apex:actionFunction name="GetHTMLBody" action="{!getHTMLBody}" rerender="thePanelWrapper" /> -->
                                                                  
          </apex:form>
                     
     </apex:pageBlockSection>
    
    </apex:pageBlock>
    
    
    
    
    
    
    <!-- these scripts automatically close the pageblock sections -->        
     twistSection(document.getElementById('{!$Component.block1.section1}').getElementsByTagName('img')[0]) 
     twistSection(document.getElementById('{!$Component.block2.section1}').getElementsByTagName('img')[0]) 
     twistSection(document.getElementById('{!$Component.block3.section1}').getElementsByTagName('img')[0]) 
    
    
    </apex:page>