Instantly share code, notes, and snippets.

Embed
What would you like to do?
Custom Metadata POC for DLRS
/**
* Copyright (c) 2013, Andrew Fawcett
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the Andrew Fawcett, nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**/
/**
* Wraps the Apex Metadata API to provide create, update and delete operations around Custom Metadata SObject's
*
* NOTE: Upsert is currently not supported by the Metadata API
*
* TODO: Support bulk requests
* TODO: Support All Or Nothing (new for Metadata API v34.0)
**/
public class CustomMetadataService {
/**
* Insert the given Custom Metadata records into the orgs config
**/
public static void createMetadata(List<SObject> records) {
// Call Metadata API and handle response
MetadataService.MetadataPort service = createService();
List<MetadataService.SaveResult> results =
service.createMetadata(new List<MetadataService.Metadata> { toCustomMetadata(records[0]) });
handleSaveResults(results[0]);
}
/**
* Update the given Custom Metadata records in the orgs config
**/
public static void updateMetadata(List<SObject> records) {
// Call Metadata API and handle response
MetadataService.MetadataPort service = createService();
List<MetadataService.SaveResult> results =
service.updateMetadata(new List<MetadataService.Metadata> { toCustomMetadata(records[0]) });
handleSaveResults(results[0]);
}
/**
* Delete the given Custom Metadata records from the orgs config
**/
public static void deleteMetadata(SObjectType qualifiedMetadataType, List<String> customMetadataFullNames) {
MetadataService.MetadataPort service = createService();
List<String> qualifiedFullNames = new List<String>();
for(String customMetadataFullName : customMetadataFullNames)
qualifiedFullNames.add(qualifiedMetadataType.getDescribe().getName() + '.' + customMetadataFullName);
List<MetadataService.DeleteResult> results =
service.deleteMetadata('CustomMetadata', qualifiedFullNames);
handleDeleteResults(results[0]);
}
public class CustomMetadataServiceException extends Exception {}
/**
* Takes the SObject instance of the Custom Metadata Type and translates to a Metadata API Custmo Metadata Type
**/
private static MetadataService.CustomMetadata toCustomMetadata(SObject customMetadataRecord) {
MetadataService.CustomMetadata cm = new MetadataService.CustomMetadata();
cm.values = new List<MetadataService.CustomMetadataValue>();
SObjectType recordType = customMetadataRecord.getSObjectType();
cm.fullName = recordType.getDescribe().getName().replace('__mdt', '') + '.' + customMetadataRecord.get('DeveloperName');
cm.label = (String) customMetadataRecord.get('Label');
for(SObjectField sObjectField : recordType.getDescribe().fields.getMap().values()) {
DescribeFieldResult dsr = sObjectField.getDescribe();
if(!dsr.isCustom())
continue;
Object fieldValue = customMetadataRecord.get(sObjectField);
if(fieldValue == null)
continue;
MetadataService.CustomMetadataValue cmdv = new MetadataService.CustomMetadataValue();
cmdv.field = dsr.getName();
cmdv.value = fieldValue+''; // TODO: More work here, type conversion
cm.values.add(cmdv);
}
return cm;
}
/**
* Connect to the Metadata API
**/
private static MetadataService.MetadataPort createService()
{
MetadataService.MetadataPort service = new MetadataService.MetadataPort();
service.SessionHeader = new MetadataService.SessionHeader_element();
service.SessionHeader.sessionId = UserInfo.getSessionId();
return service;
}
/**
* Example helper method to interpret a SaveResult, throws an exception if errors are found
**/
private static void handleSaveResults(MetadataService.SaveResult saveResult)
{
// Nothing to see?
if(saveResult==null || saveResult.success)
return;
// Construct error message and throw an exception
if(saveResult.errors!=null)
{
List<String> messages = new List<String>();
messages.add(
(saveResult.errors.size()==1 ? 'Error ' : 'Errors ') +
'occured processing component ' + saveResult.fullName + '.');
for(MetadataService.Error error : saveResult.errors)
messages.add(
error.message + ' (' + error.statusCode + ').' +
( error.fields!=null && error.fields.size()>0 ?
' Fields ' + String.join(error.fields, ',') + '.' : '' ) );
if(messages.size()>0)
throw new CustomMetadataServiceException(String.join(messages, ' '));
}
if(!saveResult.success)
throw new CustomMetadataServiceException('Request failed with no specified error.');
}
/**
* Example helper method to interpret a SaveResult, throws an exception if errors are found
**/
private static void handleDeleteResults(MetadataService.DeleteResult deleteResult)
{
// Nothing to see?
if(deleteResult==null || deleteResult.success)
return;
// Construct error message and throw an exception
if(deleteResult.errors!=null)
{
List<String> messages = new List<String>();
messages.add(
(deleteResult.errors.size()==1 ? 'Error ' : 'Errors ') +
'occured processing component ' + deleteResult.fullName + '.');
for(MetadataService.Error error : deleteResult.errors)
messages.add(
error.message + ' (' + error.statusCode + ').' +
( error.fields!=null && error.fields.size()>0 ?
' Fields ' + String.join(error.fields, ',') + '.' : '' ) );
if(messages.size()>0)
throw new CustomMetadataServiceException(String.join(messages, ' '));
}
if(!deleteResult.success)
throw new CustomMetadataServiceException('Request failed with no specified error.');
}
}
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<fields>
<fullName>Active__c</fullName>
<defaultValue>false</defaultValue>
<deprecated>false</deprecated>
<externalId>false</externalId>
<inlineHelpText>For Realtime rollups can only be set when the Child Apex Trigger has been deployed.</inlineHelpText>
<label>Active</label>
<trackTrending>false</trackTrending>
<type>Checkbox</type>
</fields>
<fields>
<fullName>AggregateOperation__c</fullName>
<deprecated>false</deprecated>
<externalId>false</externalId>
<inlineHelpText>Rollup operation.</inlineHelpText>
<label>Aggregate Operation</label>
<trackTrending>false</trackTrending>
<type>Text</type>
<length>32</length>
</fields>
<fields>
<fullName>AggregateResultField__c</fullName>
<deprecated>false</deprecated>
<externalId>false</externalId>
<inlineHelpText>API name of the field that will store the result of the rollup on the Parent Object, e.g. AnnualRevenue</inlineHelpText>
<label>Aggregate Result Field</label>
<length>80</length>
<required>true</required>
<trackTrending>false</trackTrending>
<type>Text</type>
<unique>false</unique>
</fields>
<fields>
<fullName>CalculateJobId__c</fullName>
<deprecated>false</deprecated>
<externalId>false</externalId>
<inlineHelpText>This field is used by the system when using the Calculate button to track if a calculation job is already running. Clear this field if the system reports the calculate job is already running and you known this is not the case.</inlineHelpText>
<label>Calculate Job Id</label>
<length>18</length>
<required>false</required>
<trackTrending>false</trackTrending>
<type>Text</type>
<unique>false</unique>
</fields>
<fields>
<fullName>CalculationMode__c</fullName>
<deprecated>false</deprecated>
<externalId>false</externalId>
<inlineHelpText>Realtime mode requires an Apex Trigger to be deployed for the Child Object. Click Manage Child Trigger button to deploy.</inlineHelpText>
<label>Calculation Mode</label>
<trackTrending>false</trackTrending>
<type>Text</type>
<length>32</length>
</fields>
<fields>
<fullName>CalculationSharingMode__c</fullName>
<externalId>false</externalId>
<inlineHelpText>Determines if the Sharing Rules defined on the Child Object are considered when calculating the rollup. Default is User.</inlineHelpText>
<label>Calculation Sharing Mode</label>
<trackTrending>false</trackTrending>
<type>Text</type>
<length>32</length>
</fields>
<fields>
<fullName>ChildObject__c</fullName>
<deprecated>false</deprecated>
<externalId>false</externalId>
<inlineHelpText>API name of the Child Object, e.g. Opportunity</inlineHelpText>
<label>Child Object</label>
<length>80</length>
<required>true</required>
<trackTrending>false</trackTrending>
<type>Text</type>
<unique>false</unique>
</fields>
<fields>
<fullName>ConcatenateDelimiter__c</fullName>
<deprecated>false</deprecated>
<externalId>false</externalId>
<inlineHelpText>Enter the character or characters to delimit values in the Field to Aggregate when rolling up text values into the Aggregate Result Field, enter BR() for new line. Only applies when using Concatenate operation.</inlineHelpText>
<label>Concatenate Delimiter</label>
<length>32</length>
<required>false</required>
<trackTrending>false</trackTrending>
<type>Text</type>
<unique>false</unique>
</fields>
<fields>
<fullName>Description__c</fullName>
<deprecated>false</deprecated>
<externalId>false</externalId>
<label>Description</label>
<length>255</length>
<trackTrending>false</trackTrending>
<type>Text</type>
</fields>
<fields>
<fullName>FieldToAggregate__c</fullName>
<deprecated>false</deprecated>
<externalId>false</externalId>
<inlineHelpText>API name of the field on the Child Object that contains the value to rollup, e.g. Amount</inlineHelpText>
<label>Field to Aggregate</label>
<length>80</length>
<required>true</required>
<trackTrending>false</trackTrending>
<type>Text</type>
<unique>false</unique>
</fields>
<fields>
<fullName>FieldToOrderBy__c</fullName>
<deprecated>false</deprecated>
<externalId>false</externalId>
<inlineHelpText>Only applicable when using the Concatenate, Concatenate Distinct, Last and First aggregate operations. Defaults to the field given in Field to Aggregate.</inlineHelpText>
<label>Field to Order By</label>
<length>80</length>
<required>false</required>
<trackTrending>false</trackTrending>
<type>Text</type>
<unique>false</unique>
</fields>
<fields>
<fullName>ParentObject__c</fullName>
<deprecated>false</deprecated>
<externalId>false</externalId>
<inlineHelpText>API name of the Parent Object, e.g. Account</inlineHelpText>
<label>Parent Object</label>
<length>80</length>
<required>true</required>
<trackTrending>false</trackTrending>
<type>Text</type>
<unique>false</unique>
</fields>
<fields>
<fullName>RelationshipCriteriaFields__c</fullName>
<deprecated>false</deprecated>
<externalId>false</externalId>
<inlineHelpText>If you have specified a relationship criteria, you must confirm the fields referenced by it here on separate lines, for example for criteria StageName = &apos;Won&apos; list StageName in this field. You do not need to specify the Field to Aggregate field however.</inlineHelpText>
<label>Relationship Criteria Fields</label>
<required>false</required>
<trackTrending>false</trackTrending>
<type>TextArea</type>
</fields>
<fields>
<fullName>RelationshipCriteria__c</fullName>
<deprecated>false</deprecated>
<externalId>false</externalId>
<inlineHelpText>SOQL WHERE clause applied when querying Child Object records, e.g. Amount &gt; 200</inlineHelpText>
<label>Relationship Criteria</label>
<length>255</length>
<required>false</required>
<trackTrending>false</trackTrending>
<type>Text</type>
<unique>false</unique>
</fields>
<fields>
<fullName>RelationshipField__c</fullName>
<deprecated>false</deprecated>
<externalId>false</externalId>
<inlineHelpText>API name of the Lookup field on the Child Object relating to the Parent Object, e.g. AccountId</inlineHelpText>
<label>Relationship Field</label>
<length>80</length>
<required>true</required>
<trackTrending>false</trackTrending>
<type>Text</type>
<unique>false</unique>
</fields>
<label>Lookup Rollup Summary</label>
<pluralLabel>Lookup Rollup Summaries</pluralLabel>
<visibility>Public</visibility>
</CustomObject>
<apex:page controller="ManageLookupRollupSummariesController" showHeader="true" sidebar="true" action="{!init}">
<apex:form>
<apex:sectionHeader title="Manage Lookup Rollup Summaries"/>
<apex:outputLabel value="Select Lookup Rollup Summary:" />&nbsp;
<apex:selectList value="{!SelectedLookup}" size="1">
<apex:actionSupport event="onchange" action="{!load}" reRender="rollupDetailView"/>
<apex:selectOptions value="{!Lookups}"/>
</apex:selectList>
<p/>
<apex:pageMessages/>
<apex:pageBlock mode="edit" id="rollupDetailView">
<apex:pageBlockButtons>
<apex:commandButton value="Save" action="{!save}"/>
<apex:commandButton value="Delete" action="{!deleteX}" disabled="{!LookupRollupSummary.Id==null}"/>
</apex:pageBlockButtons>
<apex:pageBlockSection title="Information" columns="2">
<apex:inputText value="{!LookupRollupSummary.Label}"/>
<apex:inputText value="{!LookupRollupSummary.DeveloperName}" disabled="{!LookupRollupSummary.Id!=null}"/>
</apex:pageBlockSection>
<apex:pageBlockSection title="Lookup Relationship" columns="2">
<apex:inputText value="{!LookupRollupSummary.ParentObject__c}"/>
<apex:inputText value="{!LookupRollupSummary.RelationshipField__c}"/>
<apex:inputText value="{!LookupRollupSummary.ChildObject__c}"/>
<apex:inputText value="{!LookupRollupSummary.RelationshipCriteria__c}"/>
<apex:inputText value="{!LookupRollupSummary.RelationshipCriteriaFields__c}"/>
</apex:pageBlockSection>
<apex:pageBlockSection title="Rollup Details" columns="2">
<apex:inputText value="{!LookupRollupSummary.FieldToAggregate__c}"/>
<apex:inputCheckbox value="{!LookupRollupSummary.Active__c}"/>
<apex:inputText value="{!LookupRollupSummary.FieldToOrderBy__c}"/>
<apex:inputText value="{!LookupRollupSummary.CalculationMode__c}"/>
<apex:inputText value="{!LookupRollupSummary.AggregateOperation__c}"/>
<apex:inputText value="{!LookupRollupSummary.CalculationSharingMode__c}"/>
<apex:inputText value="{!LookupRollupSummary.AggregateResultField__c}"/>
</apex:pageBlockSection>
<apex:pageBlockSection title="Text Lookups" columns="2">
<apex:inputText value="{!LookupRollupSummary.ConcatenateDelimiter__c}"/>
</apex:pageBlockSection>
<apex:pageBlockSection title="Description" columns="2">
<apex:inputText value="{!LookupRollupSummary.Description__c}"/>
</apex:pageBlockSection>
</apex:pageBlock>
</apex:form>
</apex:page>
/**
* Copyright (c) 2013, Andrew Fawcett
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the Andrew Fawcett, nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**/
public with sharing class ManageLookupRollupSummariesController {
public LookupRollupSummary__mdt LookupRollupSummary {get;set;}
public String selectedLookup {get;set;}
public ManageLookupRollupSummariesController() {
LookupRollupSummary = new LookupRollupSummary__mdt();
}
public List<SelectOption> getLookups() {
// List current rollup custom metadata configs
List<SelectOption> options = new List<SelectOption>();
options.add(new SelectOption('[new]','Create new...'));
for(LookupRollupSummary__mdt rollup :
[select DeveloperName, Label from LookupRollupSummary__mdt order by Label])
options.add(new SelectOption(rollup.DeveloperName,rollup.Label));
return options;
}
public PageReference init() {
// URL parameter?
selectedLookup = ApexPages.currentPage().getParameters().get('developerName');
if(selectedLookup!=null)
{
LookupRollupSummary =
[select
Id,
Label,
Language,
MasterLabel,
NamespacePrefix,
DeveloperName,
QualifiedApiName,
ParentObject__c,
RelationshipField__c,
ChildObject__c,
RelationshipCriteria__c,
RelationshipCriteriaFields__c,
FieldToAggregate__c,
FieldToOrderBy__c,
Active__c,
CalculationMode__c,
AggregateOperation__c,
CalculationSharingMode__c,
AggregateResultField__c,
ConcatenateDelimiter__c,
CalculateJobId__c,
Description__c
from LookupRollupSummary__mdt
where DeveloperName = :selectedLookup];
}
return null;
}
public PageReference load() {
// Reload the page
PageReference newPage = Page.managelookuprollupsummaries;
newPage.setRedirect(true);
if(selectedLookup != '[new]')
newPage.getParameters().put('developerName', selectedLookup);
return newPage;
}
public PageReference save() {
try {
// Insert / Update the rollup custom metadata
if(LookupRollupSummary.Id==null)
CustomMetadataService.createMetadata(new List<SObject> { LookupRollupSummary });
else
CustomMetadataService.updateMetadata(new List<SObject> { LookupRollupSummary });
// Reload this page (and thus the rollup list in a new request, metadata changes are not visible until this request ends)
PageReference newPage = Page.managelookuprollupsummaries;
newPage.setRedirect(true);
newPage.getParameters().put('developerName', LookupRollupSummary.DeveloperName);
return newPage;
} catch (Exception e) {
ApexPages.addMessages(e);
}
return null;
}
public PageReference deleteX() {
try {
// Delete the rollup custom metadata
CustomMetadataService.deleteMetadata(
LookupRollupSummary.getSObjectType(), new List<String> { LookupRollupSummary.DeveloperName });
// Reload this page (and thus the rollup list in a new request, metadata changes are not visible until this request ends)
PageReference newPage = Page.managelookuprollupsummaries;
newPage.setRedirect(true);
return newPage;
} catch (Exception e) {
ApexPages.addMessages(e);
}
return null;
}
}
/**
* Copyright (c) 2013, Andrew Fawcett
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the Andrew Fawcett, nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**/
@IsTest
private class ManageLookupRollupSummariesTest {
/**
* This is purely for code coverage at this stage
**/
@IsTest
private static void testController() {
System.Test.setMock(WebServiceMock.class, new WebServiceMockImpl());
ManageLookupRollupSummariesController controller = new ManageLookupRollupSummariesController();
controller.init();
controller.getLookups();
controller.save();
controller.load();
controller.deleteX();
}
private class WebServiceMockImpl implements WebServiceMock
{
public void doInvoke(
Object stub, Object request, Map<String, Object> response,
String endpoint, String soapAction, String requestName,
String responseNS, String responseName, String responseType)
{
if(request instanceof MetadataService.updateMetadata_element)
response.put('response_x', new MetadataService.updateMetadataResponse_element());
else if(request instanceof MetadataService.deleteMetadata_element) {
MetadataService.deleteMetadataResponse_element deleteResponse = new MetadataService.deleteMetadataResponse_element();
deleteResponse.result = new List<MetadataService.DeleteResult>();
deleteResponse.result.add(new MetadataService.DeleteResult());
deleteResponse.result[0].success = false;
deleteResponse.result[0].errors = new List<MetadataService.Error>();
deleteResponse.result[0].errors.add(new MetadataService.Error());
deleteResponse.result[0].errors[0].message = 'Message';
deleteResponse.result[0].errors[0].statusCode = 'StatusCode';
response.put('response_x', deleteResponse);
}
else if(request instanceof MetadataService.createMetadata_element){
MetadataService.createMetadataResponse_element createResponse = new MetadataService.createMetadataResponse_element();
createResponse.result = new List<MetadataService.SaveResult>();
createResponse.result.add(new MetadataService.SaveResult());
createResponse.result[0].success = false;
createResponse.result[0].errors = new List<MetadataService.Error>();
createResponse.result[0].errors.add(new MetadataService.Error());
createResponse.result[0].errors[0].message = 'Message';
createResponse.result[0].errors[0].statusCode = 'StatusCode';
response.put('response_x', createResponse);
}
return;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment