├── Trigger Framework ├── TriggerFramework.png └── AbstractTriggerContext.cls ├── Export Meta data UI + Controller ├── ExportMetaDataDetails.page └── ExportMetaDataDetails.apxc ├── README.md └── ApexUtilities.cls /Trigger Framework/TriggerFramework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vimaltiwari2612/SalesforceApexUtilites/HEAD/Trigger Framework/TriggerFramework.png -------------------------------------------------------------------------------- /Export Meta data UI + Controller/ExportMetaDataDetails.page: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Export Meta data UI + Controller/ExportMetaDataDetails.apxc: -------------------------------------------------------------------------------- 1 | public class ExportMetaDataDetails { 2 | 3 | public List customFields {get;set;} 4 | public String objectName {get;set;} 5 | 6 | public void getAllCustomFields(){ 7 | customFields = new List(); 8 | System.debug('objectName '+objectName); 9 | if(objectName == null || objectName == '' || objectName.trim() == '') return; 10 | Map completeSchema = Schema.getGlobalDescribe(); 11 | Schema.SObjectType requiredObject = completeSchema.get(objectName); 12 | System.debug('requiredObject '+requiredObject); 13 | for(Schema.SObjectField f : requiredObject.getDescribe().fields.getMap().values()){ 14 | Schema.DescribeFieldResult fq = f.getDescribe(); 15 | String name = fq.getName(); 16 | String label = fq.getLabel(); 17 | String fType = fq.getType().name(); 18 | if(name.endsWithIgnoreCase('__c')){ 19 | System.debug(name+' '+label+' '+ftype); 20 | customFields.add(new field(label,name,fType)); 21 | } 22 | } 23 | } 24 | 25 | 26 | class Field{ 27 | public String label {get;set;} 28 | public String apiname {get;set;} 29 | public String fType {get;set;} 30 | 31 | public Field(String label,String apiname,String fType){ 32 | this.label=label; 33 | this.apiname=apiname; 34 | this.fType=fType; 35 | } 36 | 37 | } 38 | 39 | } 40 | 41 | 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SalesforceApexUtilites 2 | 3 | Many times, we came across such scenarios, where we need to use Apex standard APIs like Schema or Describe, and we end up searching the code on the Internet. 4 | 5 | Here is a Util class, that you can deploy in your org and use it whenever needed. Also, add new methods in it, if required. 6 | 7 | The class had All the bulkified versions of all methods too. 8 | 9 | https://medium.com/elevate-salesforce/salesforce-apex-utilities-fe11692a9288 10 | 11 | 12 | -------------------------------------------------- 13 | # Trigger Framework POC 14 | 15 | Complete Details : 16 | 17 | https://medium.com/elevate-salesforce/apex-trigger-framework-a-generic-way-to-join-tigger-contexts-46c4d9277db0 18 | 19 | ***This Framework is beneficial when we have more than one triggers on any object. This Framwwork will help us to keep only one trigger on an object irrrespective of different packages and business use cases*** 20 | 21 | Developers extend "AbstractTriggerContext" class, and override necessary methods which are needed. 22 | Then they need to register their classes in a Custom meta data with the context and operation type 23 | 24 | **Framework Overview** 25 | 26 | The idea is to join all the trigger contexts of similar types, together. For example, all BEFORE (INSERT/UPDATE/DELETE) will run together, doesn’t matter, it’s a package trigger or custom trigger logic added by customers. 27 | 28 | ![Trigger idea](https://user-images.githubusercontent.com/22127564/123540521-0aea7500-d75d-11eb-81f4-768b7dad307f.png) 29 | 30 | 31 | **Architecture Overview** 32 | 33 | ![Image](https://github.com/vimaltiwari2612/SalesforceApexUtilites/blob/master/Trigger%20Framework/TriggerFramework.png) 34 | 35 | 36 | 37 | 38 | 39 | 40 | Custom Meta Data details 41 | 1. Class Name : Name of class 42 | 2. Context : Before/After 43 | 3. operation : delete, undelete, insert, update 44 | 4. object name : for which object you want to run given class code 45 | 5. Is Active : to enable /disable trigge logics 46 | 47 | 48 | Usage : Sample Account Trigger 49 | trigger AccountTrigger on Account (before insert, after insert) { 50 | 51 | AbstractTriggerContext.run('Account',Trigger.operationType,Trigger.new, Trigger.old,Trigger.newMap,Trigger.oldMap); 52 | } 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /Trigger Framework/AbstractTriggerContext.cls: -------------------------------------------------------------------------------- 1 | /* 2 | This Framework is beneficial when we have more than one triggers on any object. 3 | Developers extend this class, and override necessary methods which are needed. 4 | Then they need to register their classes in a Custom meta data with the context and operation type 5 | 6 | 7 | Custom Meta Data details 8 | 1. Class Name : Name of class 9 | 2. Context : Before/After 10 | 3. operation : delete, undelete, insert, update 11 | 4. object name : for which object you want to run given class code 12 | 5. Is Active : to enable /disable trigge logics 13 | 14 | 15 | Usage : sample Account Trigger 16 | trigger AccountTrigger on Account (before insert, after insert) { 17 | 18 | AbstractTriggerContext.run('Account',Trigger.operationType,Trigger.new, Trigger.old,Trigger.newMap,Trigger.oldMap); 19 | } 20 | 21 | */ 22 | 23 | 24 | 25 | global abstract class AbstractTriggerContext { 26 | 27 | global static void run(String objectName, System.TriggerOperation operationType, List newList, List oldList, Map newMap, Map oldMap){ 28 | //query the meta data 29 | for(TriggerContext__mdt record : getMetaData(objectName,operationType)){ 30 | //get the instance 31 | AbstractTriggerContext instance = (AbstractTriggerContext)Type.forName(record.Class_Name__c).newInstance(); 32 | //call the method 33 | if(operationType == System.TriggerOperation.BEFORE_INSERT){ 34 | instance.beforeInsert(newList, oldList, newMap, oldMap); 35 | }else if(operationType == System.TriggerOperation.BEFORE_UPDATE){ 36 | instance.beforeUpdate(newList, oldList, newMap, oldMap); 37 | }else if(operationType == System.TriggerOperation.BEFORE_DELETE){ 38 | instance.beforeDelete(oldList, oldMap); 39 | }else if(operationType == System.TriggerOperation.AFTER_INSERT){ 40 | instance.afterInsert(newList, oldList, newMap, oldMap); 41 | }else if(operationType == System.TriggerOperation.AFTER_UPDATE){ 42 | instance.afterUpdate(newList, oldList, newMap, oldMap); 43 | }else if(operationType == System.TriggerOperation.AFTER_UNDELETE){ 44 | instance.afterUndelete(newList, newMap); 45 | } 46 | } 47 | } 48 | 49 | global virtual void beforeInsert(List newList, List oldList, Map newMap, Map oldMap){ 50 | //override for before insert logic 51 | } 52 | 53 | global virtual void beforeUpdate(List newList, List oldList, Map newMap, Map oldMap){ 54 | //override for before update logic 55 | } 56 | 57 | global virtual void afterInsert(List newList, List oldList, Map newMap, Map oldMap){ 58 | //override for after insert logic 59 | } 60 | 61 | global virtual void afterUpdate(List newList, List oldList, Map newMap, Map oldMap){ 62 | //override for after update logic 63 | } 64 | 65 | global virtual void beforeDelete(List oldList, Map oldMap){ 66 | //override for before delete logic 67 | } 68 | 69 | global virtual void afterUndelete(List newList, Map newMap){ 70 | //override for after undelete logic 71 | } 72 | 73 | 74 | global static List getMetaData(String objectName, System.TriggerOperation operationType){ 75 | 76 | String query = 'Select Class_Name__c from TriggerContext__mdt where Class_Name__c !=null And Is_Active__c = true AND Object_Name__C = :objectName'; 77 | String context = ''; 78 | if(operationType == System.TriggerOperation.BEFORE_INSERT){ 79 | context = ' AND Context__c = \'Before\' AND Operation__c = \'Insert\''; 80 | }else if(operationType == System.TriggerOperation.BEFORE_UPDATE){ 81 | context = ' AND Context__c = \'Before\' AND Operation__c = \'Update\''; 82 | }else if(operationType == System.TriggerOperation.BEFORE_DELETE){ 83 | context = ' AND Context__c = \'Before\' AND Operation__c = \'Delete\''; 84 | }else if(operationType == System.TriggerOperation.AFTER_INSERT){ 85 | context = ' AND Context__c = \'After\' AND Operation__c = \'Insert\''; 86 | }else if(operationType == System.TriggerOperation.AFTER_UPDATE){ 87 | context = ' AND Context__c = \'After\' AND Operation__c = \'Update\''; 88 | }else if(operationType == System.TriggerOperation.AFTER_UNDELETE){ 89 | context = ' AND Context__c = \'After\' AND Operation__c = \'UnDelete\''; 90 | } 91 | query +=context; 92 | return (List)Database.query(query); 93 | } 94 | } -------------------------------------------------------------------------------- /ApexUtilities.cls: -------------------------------------------------------------------------------- 1 | global class ApexUtilities{ 2 | 3 | /**************************************************** Object Fields Related Methods****************************************************/ 4 | 5 | /* 6 | * @methodName = getAllFields 7 | * @return = Set of field Names 8 | * @params = object name (both custom and standard) 9 | * @description = Method takes object name as an argument and return all (both custom and standard) fields 10 | */ 11 | global static Set getAllFields(String objectName){ 12 | Map completeSchema = Schema.getGlobalDescribe(); 13 | Schema.SObjectType requiredObject = completeSchema.get(objectName); 14 | return requiredObject.getDescribe().fields.getMap().keySet(); 15 | } 16 | 17 | /* 18 | * @methodName = getAllFields 19 | * @return = Map of object names and their field Names 20 | * @params = List of object name (both custom and standard) 21 | * @description = Method takes object name as an argument and return Map of object name and their (both custom and standard) fields 22 | */ 23 | global static Map> getAllFields(List objectNames){ 24 | Map completeSchema = Schema.getGlobalDescribe(); 25 | Map> objectVsFieldMap = new Map>(); 26 | for(String objectName : objectNames){ 27 | Schema.SObjectType requiredObject = completeSchema.get(objectName); 28 | objectVsFieldMap.put(objectName,requiredObject.getDescribe().fields.getMap().keySet()); 29 | } 30 | return objectVsFieldMap; 31 | } 32 | 33 | /* 34 | * @methodName = getAllCustomFields 35 | * @return = Set of field Names 36 | * @params = object name (both custom and standard) 37 | * @description = Method takes object name as an argument and return all Custom fields 38 | */ 39 | global static Set getAllCustomFields(String objectName){ 40 | Map completeSchema = Schema.getGlobalDescribe(); 41 | Schema.SObjectType requiredObject = completeSchema.get(objectName); 42 | Set customFields = new Set(); 43 | for(String field : requiredObject.getDescribe().fields.getMap().keySet()){ 44 | if(field.endsWithIgnoreCase('__c')){ 45 | customFields.add(field); 46 | } 47 | } 48 | return customFields; 49 | } 50 | 51 | /* 52 | * @methodName = getAllCustomFields 53 | * @return = Map of object names and their field Names 54 | * @params = List of object name (both custom and standard) 55 | * @description = Method takes object name as an argument and return Map of object name and their (custom) fields 56 | */ 57 | global static Map> getAllCustomFields(List objectNames){ 58 | Map completeSchema = Schema.getGlobalDescribe(); 59 | Map> objectVsFieldMap = new Map>(); 60 | for(String objectName : objectNames){ 61 | Schema.SObjectType requiredObject = completeSchema.get(objectName); 62 | Set customFields = new Set(); 63 | for(String field : requiredObject.getDescribe().fields.getMap().keySet()){ 64 | if(field.endsWithIgnoreCase('__c')){ 65 | customFields.add(field); 66 | } 67 | } 68 | objectVsFieldMap.put(objectName,customFields); 69 | } 70 | return objectVsFieldMap; 71 | } 72 | 73 | /* 74 | * @methodName = getAllStandardFields 75 | * @return = Set of field Names 76 | * @params = object name (both custom and standard) 77 | * @description = Method takes object name as an argument and return all Standard fields 78 | */ 79 | global static Set getAllStandardFields(String objectName){ 80 | Map completeSchema = Schema.getGlobalDescribe(); 81 | Schema.SObjectType requiredObject = completeSchema.get(objectName); 82 | Set standardFields = new Set(); 83 | for(String field : requiredObject.getDescribe().fields.getMap().keySet()){ 84 | if(!field.endsWithIgnoreCase('__c')){ 85 | standardFields.add(field); 86 | } 87 | } 88 | return standardFields; 89 | } 90 | 91 | /* 92 | * @methodName = getAllStandardFields 93 | * @return = Map of object names and their field Names 94 | * @params = List of object name (both custom and standard) 95 | * @description = Method takes object name as an argument and return Map of object name and their (standard) fields 96 | */ 97 | global static Map> getAllStandardFields(List objectNames){ 98 | Map completeSchema = Schema.getGlobalDescribe(); 99 | Map> objectVsFieldMap = new Map>(); 100 | for(String objectName : objectNames){ 101 | Schema.SObjectType requiredObject = completeSchema.get(objectName); 102 | Set sFields = new Set(); 103 | for(String field : requiredObject.getDescribe().fields.getMap().keySet()){ 104 | if(!field.endsWithIgnoreCase('__c')){ 105 | sFields.add(field); 106 | } 107 | } 108 | objectVsFieldMap.put(objectName,sFields); 109 | } 110 | return objectVsFieldMap; 111 | } 112 | 113 | /* 114 | * @methodName = getAllFieldsInNameSpace 115 | * @return = Set of field Names 116 | * @params = object name (both custom and standard), namespace 117 | * @description = Method takes object name as an argument and return all Standard fields 118 | */ 119 | global static Set getAllFieldsInNameSpace(String objectName,String namespace){ 120 | Map completeSchema = Schema.getGlobalDescribe(); 121 | Schema.SObjectType requiredObject = completeSchema.get(objectName); 122 | Set requiredFields = new Set(); 123 | for(String field : requiredObject.getDescribe().fields.getMap().keySet()){ 124 | if(field.startsWithIgnoreCase(namespace+'__')){ 125 | requiredFields.add(field); 126 | } 127 | } 128 | return requiredFields; 129 | } 130 | 131 | /* 132 | * @methodName = getAllFieldsInNameSpace 133 | * @return = Map of object names and their field Names 134 | * @params = list of object name (both custom and standard), namespace 135 | * @description = Method takes object name as an argument and return Map of object name and their fields 136 | */ 137 | global static Map> getAllFieldsInNameSpace(List objectNames,String namespace){ 138 | Map completeSchema = Schema.getGlobalDescribe(); 139 | Map> objectVsFieldMap = new Map>(); 140 | for(String objectName : objectNames){ 141 | Schema.SObjectType requiredObject = completeSchema.get(objectName); 142 | Set requiredFields = new Set(); 143 | for(String field : requiredObject.getDescribe().fields.getMap().keySet()){ 144 | if(field.startsWithIgnoreCase(namespace+'__')){ 145 | requiredFields.add(field); 146 | } 147 | } 148 | objectVsFieldMap.put(objectName,requiredFields); 149 | } 150 | return objectVsFieldMap; 151 | } 152 | 153 | /* 154 | * @methodName = getAllFieldsOfType 155 | * @return = Set of field Names 156 | * @params = object name (both custom and standard), and field type like String,Boolean etc 157 | * @description = Method takes object name as an argument and return all (both custom and standard) fields of specific type 158 | */ 159 | global static Set getAllFieldsOfType(String objectName,String fieldType){ 160 | Map completeSchema = Schema.getGlobalDescribe(); 161 | Schema.SObjectType requiredObject = completeSchema.get(objectName); 162 | Set requiredFields = new Set(); 163 | for(Schema.SObjectField field : requiredObject.getDescribe().fields.getMap().values()){ 164 | Schema.DescribeFieldResult fieldDetails = field.getDescribe(); 165 | Schema.DisplayType fType = fieldDetails.getType(); 166 | if(fType.name().toLowerCase() == fieldType.toLowerCase()){ 167 | requiredFields.add(fieldDetails.getName()); 168 | } 169 | } 170 | return requiredFields; 171 | } 172 | 173 | /* 174 | * @methodName = getAllFieldsOfType 175 | * @return = Map of object name vs Fields of required type 176 | * @params = list of object name (both custom and standard), and field type like String,Boolean etc 177 | * @description = Method takes object name as an argument and return all (both custom and standard) fields of specific type 178 | */ 179 | global static Map> getAllFieldsOfType(List objectNames, String fieldType){ 180 | Map completeSchema = Schema.getGlobalDescribe(); 181 | Map> objectVsFieldMap = new Map>(); 182 | for(String objectName : objectNames){ 183 | Schema.SObjectType requiredObject = completeSchema.get(objectName); 184 | Set requiredFields = new Set(); 185 | for(Schema.SObjectField field : requiredObject.getDescribe().fields.getMap().values()){ 186 | Schema.DescribeFieldResult fieldDetails = field.getDescribe(); 187 | Schema.DisplayType fType = fieldDetails.getType(); 188 | if(fType.name().toLowerCase() == fieldType.toLowerCase()){ 189 | requiredFields.add(fieldDetails.getName()); 190 | } 191 | objectVsFieldMap.put(objectName,requiredFields); 192 | } 193 | } 194 | return objectVsFieldMap; 195 | } 196 | 197 | /* 198 | * @methodName = getAllRequiredFields 199 | * @return = Set of required fields 200 | * @params = object name 201 | * @description = Method takes object name as an argument and return all (both custom and standard) fields which are required 202 | */ 203 | global static Set getAllRequiredFields(String objectName){ 204 | Map completeSchema = Schema.getGlobalDescribe(); 205 | Schema.SObjectType requiredObject = completeSchema.get(objectName); 206 | Set requiredFields = new Set(); 207 | for(Schema.SObjectField field : requiredObject.getDescribe().fields.getMap().values()){ 208 | Schema.DescribeFieldResult fieldDetails = field.getDescribe(); 209 | //IF FIELD IS CREATEABLE AND IS NOT NILLABLE AND IS NOT DEFAULTED ON CREATE THEN ITS REQUIRED 210 | if(fieldDetails.isCreateable() && !fieldDetails.isNillable() && !fieldDetails.isDefaultedOnCreate()){ 211 | requiredFields.add(fieldDetails.getName()); 212 | } 213 | } 214 | return requiredFields; 215 | } 216 | 217 | /* 218 | * @methodName = getAllRequiredFields 219 | * @return = Map of object name vs Fields which are required 220 | * @params = list of object name (both custom and standard), and field type like String,Boolean etc 221 | * @description = Method takes object name as an argument and return all (both custom and standard) fields which are required 222 | */ 223 | global static Map> getAllRequiredFields(List objectNames){ 224 | Map completeSchema = Schema.getGlobalDescribe(); 225 | Map> objectVsFieldMap = new Map>(); 226 | for(String objectName : objectNames){ 227 | Schema.SObjectType requiredObject = completeSchema.get(objectName); 228 | Set requiredFields = new Set(); 229 | for(Schema.SObjectField field : requiredObject.getDescribe().fields.getMap().values()){ 230 | Schema.DescribeFieldResult fieldDetails = field.getDescribe(); 231 | //IF FIELD IS CREATEABLE AND IS NOT NILLABLE AND IS NOT DEFAULTED ON CREATE THEN ITS REQUIRED 232 | if(fieldDetails.isCreateable() && !fieldDetails.isNillable() && !fieldDetails.isDefaultedOnCreate()){ 233 | requiredFields.add(fieldDetails.getName()); 234 | } 235 | objectVsFieldMap.put(objectName,requiredFields); 236 | } 237 | } 238 | return objectVsFieldMap; 239 | } 240 | 241 | /**************************************************** Object Related Methods****************************************************/ 242 | 243 | 244 | /* 245 | * @methodName = getObjectNameById 246 | * @return = Object Name 247 | * @params = Salesforce Id 248 | * @description = Method takes SF Id and returns Object name 249 | */ 250 | global static String getObjectNameById(Id objectId){ 251 | return objectId.getSObjectType().getDescribe().getName(); 252 | } 253 | 254 | /* 255 | * @methodName = getObjectNameById 256 | * @return = Map of Id vs Object Name 257 | * @params = Set of Salesforce Id 258 | * @description = Method takes SF Ids and returns Map of Id vs Object name 259 | */ 260 | global static Map getObjectNameById(Set objectIds){ 261 | Map objectIdVsName = new Map(); 262 | for(Id objectId : objectIds){ 263 | objectIdVsName.put(objectId,objectId.getSObjectType().getDescribe().getName()); 264 | } 265 | return objectIdVsName; 266 | } 267 | 268 | /* 269 | * @methodName = getAllParentOfAnObject 270 | * @return = List of parent objects 271 | * @params = object names 272 | * @description = Method takes object names and returns list of parent object names 273 | */ 274 | global static List getAllParentOfAnObject(String objectName){ 275 | Map completeSchema = Schema.getGlobalDescribe(); 276 | Schema.SObjectType requiredObject = completeSchema.get(objectName); 277 | List parentObjectName = new List(); 278 | for(Schema.SobjectField strFld: requiredObject.getDescribe().fields.getMap().Values()){ 279 | if(strFld.getDescribe().getType() == Schema.DisplayType.REFERENCE){ 280 | for(Schema.SObjectType obj : strFld.getDescribe().getReferenceTo()){ 281 | parentObjectName.add(obj.getDescribe().getName()); 282 | } 283 | } 284 | } 285 | return parentObjectName; 286 | } 287 | 288 | /* 289 | * @methodName = getAllParentOfAnObject 290 | * @return = List of parent objects 291 | * @params = object names 292 | * @description = Method takes object names and returns Map of object name vs parent object names 293 | */ 294 | global static Map> getAllParentOfAnObject(List objectNames){ 295 | Map completeSchema = Schema.getGlobalDescribe(); 296 | Map> objectNameVsParentObjectNames = new Map>(); 297 | for(String objectName : objectNames){ 298 | Schema.SObjectType requiredObject = completeSchema.get(objectName); 299 | List parentObjectNames = new List(); 300 | for(Schema.SobjectField strFld: requiredObject.getDescribe().fields.getMap().Values()){ 301 | if(strFld.getDescribe().getType() == Schema.DisplayType.REFERENCE){ 302 | for(Schema.SObjectType obj : strFld.getDescribe().getReferenceTo()){ 303 | parentObjectNames.add(obj.getDescribe().getName()); 304 | } 305 | } 306 | } 307 | objectNameVsParentObjectNames.put(objectName,parentObjectNames); 308 | } 309 | return objectNameVsParentObjectNames; 310 | } 311 | 312 | /* 313 | * @methodName = getAllChildrenOfAnObject 314 | * @return = List of parent objects 315 | * @params = object names 316 | * @description = Method takes object names and returns list of child object names 317 | */ 318 | global static List getAllChildrenOfAnObject(String objectName){ 319 | Map completeSchema = Schema.getGlobalDescribe(); 320 | Schema.SObjectType requiredObject = completeSchema.get(objectName); 321 | List childObjectName = new List(); 322 | Schema.DescribeSObjectResult result = requiredObject.getDescribe(); 323 | for (Schema.ChildRelationship cr: result.getChildRelationships()) { 324 | childObjectName.add(cr.getChildSObject().getDescribe().getName()); 325 | } 326 | return childObjectName; 327 | } 328 | 329 | /* 330 | * @methodName = getAllChildrenOfAnObject 331 | * @return = List of parent objects 332 | * @params = object names 333 | * @description = Method takes object names and returns Map of object name vs parent object names 334 | */ 335 | global static Map> getAllChildrenOfAnObject(List objectNames){ 336 | Map completeSchema = Schema.getGlobalDescribe(); 337 | Map> objectNameVsChildObjectNames = new Map>(); 338 | for(String objectName : objectNames){ 339 | Schema.SObjectType requiredObject = completeSchema.get(objectName); 340 | Schema.DescribeSObjectResult result = requiredObject.getDescribe(); 341 | List childObjectNames = new List(); 342 | for (Schema.ChildRelationship cr: result.getChildRelationships()) { 343 | childObjectNames.add(cr.getChildSObject().getDescribe().getName()); 344 | } 345 | objectNameVsChildObjectNames.put(objectName,childObjectNames); 346 | } 347 | return objectNameVsChildObjectNames; 348 | } 349 | 350 | 351 | } 352 | --------------------------------------------------------------------------------