Created
December 23, 2013 00:41
-
-
Save afawcett/8090245 to your computer and use it in GitHub Desktop.
Apex script for parsing and updating generated WSDL2Apex from Metadata API WSDL, uses Tooling REST API to dynamically create the modified code.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public with sharing class MetadataServicePatcher { | |
// Perhaps parse these from the WSDL in the future | |
private static final Map<String, String> METADATA_TYPES = | |
new Map<String, String> { | |
'CustomSite' => 'Metadata', | |
'InstalledPackage' => 'Metadata', | |
'CustomField' => 'Metadata', | |
'FieldSet' => 'Metadata', | |
'PicklistValue' => 'Metadata', | |
'RecordType' => 'Metadata', | |
'WebLink' => 'Metadata', | |
'AddressSettings' => 'Metadata', | |
'CaseSettings' => 'Metadata', | |
'CustomObject' => 'Metadata', | |
'Layout' => 'Metadata', | |
'ApexPage' => 'MetadataWithContent', | |
'ApexClass' => 'MetadataWithContent', | |
'StaticResource' => 'MetadataWithContent' | |
}; | |
public static void patch() | |
{ | |
// Query the current Apex Class | |
ApexClass apexClass = | |
[select Id, Body | |
from ApexClass | |
where Name = 'MetadataServiceImported']; | |
// Process and scan for lines to modify and/or insert | |
LineReader lineReader = new LineReader(apexClass.Body); | |
List<String> newLines = new List<String>(); | |
while(lineReader.hasNext()) | |
{ | |
// Read line | |
String line = lineReader.next(); | |
String trimmedLine = line.trim(); | |
// Adjust class name? | |
if(trimmedLine.contains('MetadataServiceImported')) | |
line = line.replace('MetadataServiceImported', 'MetadataService'); | |
// Adjust end point logic? | |
if(trimmedLine.startsWith('public String endpoint_x')) | |
line = ' public String endpoint_x = URL.getSalesforceBaseUrl().toExternalForm() + \'/services/Soap/m/30.0\';'; | |
// Adjust update_x method name? | |
else if(trimmedLine.contains('update_x(')) | |
line = line.replace('update_x', 'updateMetadata'); | |
// Adjust delete_x method name? | |
else if(trimmedLine.contains('delete_x(')) | |
line = line.replace('delete_x', 'deleteMetadata'); | |
// Adjust retrieve_x method name? | |
else if(trimmedLine.contains('retrieve_x(')) | |
line = line.replace('retrieve_x', 'retrieve'); | |
// Make Metadata virtual? | |
else if(trimmedLine.startsWith('public class Metadata ')) | |
line = line.replace('public class', 'public virtual class'); | |
// Make MetadataWithContent virtual? | |
else if(trimmedLine.startsWith('public class MetadataWithContent ')) | |
line = line.replace('public class MetadataWithContent', 'public virtual class MetadataWithContent extends Metadata'); | |
// Class definition? | |
else if(trimmedLine.startsWith('public class')) | |
{ | |
List<String> parts = trimmedLine.split(' '); | |
String className = parts[2]; | |
if(METADATA_TYPES.containsKey(className)) | |
{ | |
// Adjust class to extend base class and add base class members (XML serialiser does not support inheritance) | |
String extendsClassName = METADATA_TYPES.get(className); | |
line = line.replace('public class ' + className, 'public class ' + className + ' extends ' + extendsClassName); | |
newLines.add(line); | |
newLines.add(' public String type = \'' + className + '\';'); | |
newLines.add(' public String fullName;'); | |
if(extendsClassName.equals('MetadataWithContent')) | |
newLines.add(' public String content;'); | |
// Move forward until field_order_type_info member | |
while(lineReader.hasNext()) | |
{ | |
line = (String) lineReader.next(); | |
trimmedLine = line.trim(); | |
// Adjust class name? | |
if(trimmedLine.contains('MetadataServiceImported')) | |
line = line.replace('MetadataServiceImported', 'MetadataService'); | |
else if(trimmedLine.startsWith('private String[] field_order_type_info')) | |
{ | |
// Add type info descriptors and adjust field_order_type_info list | |
newLines.add(' private String[] type_att_info = new String[]{\'xsi:type\'};'); | |
newLines.add(' private String[] fullName_type_info = new String[]{\'fullName\',\'http://www.w3.org/2001/XMLSchema\',\'string\',\'0\',\'1\',\'false\'};'); | |
if(extendsClassName.equals('MetadataWithContent')) | |
{ | |
newLines.add(' private String[] content_type_info = new String[]{\'content\',\'http://www.w3.org/2001/XMLSchema\',\'base64Binary\',\'0\',\'1\',\'false\'};'); | |
line = line.replace('new String[]{', 'new String[]{\'fullName\', \'content\', '); | |
} | |
else | |
{ | |
line = line.replace('new String[]{', 'new String[]{\'fullName\', '); | |
} | |
newLines.add(line); | |
break; | |
} | |
newLines.add(line); | |
} | |
continue; | |
} | |
} | |
newLines.add(line); | |
} | |
// Query for the patched ApexClass (via Tooling REST API)? | |
Http h = new Http(); | |
HttpRequest queryReq = new HttpRequest(); | |
queryReq.setEndpoint(URL.getSalesforceBaseUrl().toExternalForm() + '/services/data/v29.0/tooling/query/?q=SELECT+Id+from+ApexClass+Where+Name=\'MetadataService\''); | |
queryReq.setHeader('Authorization', 'OAuth ' + UserInfo.getSessionId()); | |
queryReq.setHeader('Content-Type', 'application/json'); | |
queryReq.setMethod('GET'); | |
HttpResponse queryRes = h.send(queryReq); | |
if(queryRes.getStatusCode() != 200) | |
throw new PatchException(queryReq.getEndpoint() + ' ' + queryRes.getStatusCode() + ' ' + queryRes.getBody()); | |
Map<String, Object> queryData = (Map<String, Object>) JSON.deserializeUntyped(queryRes.getBody()); | |
Integer size = (Integer) queryData.get('size'); | |
if(size > 0) | |
{ | |
// Delete the patch ApexClass (via Tooling REST API) | |
List<Object> records = (List<Object>) queryData.get('records'); | |
Map<String, Object> recordList = (Map<String, Object>) records[0]; | |
String classId = (String) recordList.get('Id'); | |
HttpRequest deleteReq = new HttpRequest(); | |
deleteReq.setEndpoint(URL.getSalesforceBaseUrl().toExternalForm() + '/services/data/v29.0/tooling/sobjects/ApexClass/'+classId); | |
deleteReq.setHeader('Authorization', 'OAuth ' + UserInfo.getSessionId()); | |
deleteReq.setHeader('Content-Type', 'application/json'); | |
deleteReq.setMethod('DELETE'); | |
HttpResponse deleteRes = h.send(deleteReq); | |
if(deleteRes.getStatusCode() != 204) | |
throw new PatchException(deleteReq.getEndpoint() + ' ' + deleteRes.getStatusCode() + ' ' + deleteRes.getBody()); | |
} | |
// Create the patched ApexClass (via Tooling REST API) | |
String patchClass = String.join(newLines, '\n'); | |
JSONGenerator body = JSON.createGenerator(false); | |
body.writeStartObject(); | |
body.writeStringField('Name', 'MetadataService'); | |
body.writeStringField('Body', patchClass); | |
body.writeEndObject(); | |
HttpRequest createReq = new HttpRequest(); | |
createReq.setEndpoint(URL.getSalesforceBaseUrl().toExternalForm() + '/services/data/v29.0/tooling/sobjects/ApexClass'); | |
createReq.setBody( body.getAsString() ); | |
createReq.setHeader('Authorization', 'OAuth ' + UserInfo.getSessionId()); | |
createReq.setHeader('Content-Type', 'application/json'); | |
createReq.setMethod('POST'); | |
HttpResponse createRes = h.send(createReq); | |
if(createRes.getStatusCode() != 201) | |
throw new PatchException(createRes.getStatusCode() + ' ' + createRes.getBody()); | |
} | |
public class PatchException extends Exception { } | |
/** | |
* Utility class to iterate over lines in Apex source code | |
**/ | |
public class LineReader | |
implements Iterator<string>, Iterable<string> | |
{ | |
private String LF = '\n'; | |
private String textData; | |
public LineReader(String textData) | |
{ | |
this.textData = textData; | |
} | |
public Boolean hasNext() | |
{ | |
return textData.length() > 0 ? true : false; | |
} | |
public String next() | |
{ | |
String row = null; | |
Integer endPos = textData.indexOf(LF); | |
if(endPos == -1) | |
{ | |
row = textData; | |
textData = ''; | |
} | |
else | |
{ | |
row = textData.subString(0, endPos); | |
textData = textData.subString(endPos + LF.length(), textData.length()); | |
} | |
return row; | |
} | |
public Iterator<String> Iterator() | |
{ | |
return this; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment