diff --git a/rolluptool/src/classes/ApexClassesSelector.cls b/rolluptool/src/classes/ApexClassesSelector.cls index 9014b0b0..5f9bdbaf 100644 --- a/rolluptool/src/classes/ApexClassesSelector.cls +++ b/rolluptool/src/classes/ApexClassesSelector.cls @@ -27,7 +27,7 @@ /** * Performs various queries on the ApexClass object **/ -public with sharing class ApexClassesSelector extends SObjectSelector +public class ApexClassesSelector extends fflib_SObjectSelector { public List getSObjectFieldList() { @@ -59,12 +59,11 @@ public with sharing class ApexClassesSelector extends SObjectSelector **/ public Map selectByName(Set names) { - assertIsAccessible(); - List apexClasses = Database.query(String.format( - 'select {0} from {1} where Name in :names order by {2}', - new List{getFieldListString(), - getSObjectName(), - getOrderBy()})); + List apexClasses = + Database.query( + newQueryFactory(). + setCondition('Name in :names'). + toSOQL()); Map mapByName = new Map(); for(ApexClass apexClass : apexClasses) mapByName.put(apexClass.Name, apexClass); diff --git a/rolluptool/src/classes/ApexTriggersSelector.cls b/rolluptool/src/classes/ApexTriggersSelector.cls index 382b341b..46795738 100644 --- a/rolluptool/src/classes/ApexTriggersSelector.cls +++ b/rolluptool/src/classes/ApexTriggersSelector.cls @@ -27,7 +27,7 @@ /** * Performs various queries on the ApexTrigger object **/ -public with sharing class ApexTriggersSelector extends SObjectSelector +public class ApexTriggersSelector extends fflib_SObjectSelector { public List getSObjectFieldList() { @@ -68,12 +68,11 @@ public with sharing class ApexTriggersSelector extends SObjectSelector **/ public Map selectByName(Set names) { - assertIsAccessible(); - List apexTriggers = Database.query(String.format( - 'select {0} from {1} where Name in :names order by {2}', - new List{getFieldListString(), - getSObjectName(), - getOrderBy()})); + List apexTriggers = + Database.query( + newQueryFactory(). + setCondition('Name in :names'). + toSOQL()); Map mapByName = new Map(); for(ApexTrigger apexTrigger : apexTriggers) mapByName.put(apexTrigger.Name, apexTrigger); diff --git a/rolluptool/src/classes/AsyncApexJobsSelector.cls b/rolluptool/src/classes/AsyncApexJobsSelector.cls index 4c8ca771..d641e087 100644 --- a/rolluptool/src/classes/AsyncApexJobsSelector.cls +++ b/rolluptool/src/classes/AsyncApexJobsSelector.cls @@ -24,13 +24,8 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public with sharing class AsyncApexJobsSelector extends SObjectSelector -{ - public override String getOrderBy() - { - return 'CreatedDate'; - } - +public class AsyncApexJobsSelector extends fflib_SObjectSelector +{ List getSObjectFieldList() { return new List { @@ -49,6 +44,11 @@ public with sharing class AsyncApexJobsSelector extends SObjectSelector { return AsyncApexJob.sObjectType; } + + public override String getOrderBy() + { + return 'CreatedDate'; + } public List selectById(Set idSet) { @@ -63,12 +63,11 @@ public with sharing class AsyncApexJobsSelector extends SObjectSelector Set statuses = new Set { 'Queued', 'Processing', 'Preparing' }; String jobType = 'BatchApex'; String query = - String.format( - 'select {0} from {1} ' + - 'where JobType = :jobType And ' + + newQueryFactory(). + setCondition( + 'JobType = :jobType And ' + 'ApexClass.Name in :classNames And ' + - 'Status in :statuses', - new List{getFieldListString(),getSObjectName()}); + 'Status in :statuses').toSOQL(); List jobs = (List) Database.query(query); return jobs.size()>0; diff --git a/rolluptool/src/classes/RollupControllerTest.cls b/rolluptool/src/classes/RollupControllerTest.cls index 93469650..76c7698a 100644 --- a/rolluptool/src/classes/RollupControllerTest.cls +++ b/rolluptool/src/classes/RollupControllerTest.cls @@ -25,7 +25,7 @@ **/ @IsTest -private with sharing class RollupControllerTest +private class RollupControllerTest { private testmethod static void testDeployedStatus() { diff --git a/rolluptool/src/classes/RollupJobTest.cls b/rolluptool/src/classes/RollupJobTest.cls index 35488ed3..5ff5fc6b 100644 --- a/rolluptool/src/classes/RollupJobTest.cls +++ b/rolluptool/src/classes/RollupJobTest.cls @@ -25,7 +25,7 @@ **/ @IsTest -private with sharing class RollupJobTest +private class RollupJobTest { } \ No newline at end of file diff --git a/rolluptool/src/classes/RollupService.cls b/rolluptool/src/classes/RollupService.cls index 0ad9611a..5e5ae7e3 100644 --- a/rolluptool/src/classes/RollupService.cls +++ b/rolluptool/src/classes/RollupService.cls @@ -639,7 +639,7 @@ global with sharing class RollupService // Query applicable lookup definitions Schema.DescribeSObjectResult childRecordDescribe = childObjectType.getDescribe(); List lookups = - new RollupSummariesSelector().selectActiveByChildObject( + new RollupSummariesSelector(false).selectActiveByChildObject( new List { RollupSummaries.CalculationMode.Realtime, RollupSummaries.CalculationMode.Scheduled }, new Set { childRecordDescribe.getName() }); return lookups; diff --git a/rolluptool/src/classes/RollupSummaries.cls b/rolluptool/src/classes/RollupSummaries.cls index b15760c2..3f24de17 100644 --- a/rolluptool/src/classes/RollupSummaries.cls +++ b/rolluptool/src/classes/RollupSummaries.cls @@ -27,7 +27,7 @@ /** * Validation and other behaviour for the Lookup Rollup Summary custom object **/ -public with sharing class RollupSummaries extends SObjectDomain +public class RollupSummaries extends fflib_SObjectDomain { private static final Integer APEXTRIGGER_NAME_LENGTH = 40; // ApexTrigger.Name.getDescribe().getLength(); gives 255? @@ -221,9 +221,9 @@ public with sharing class RollupSummaries extends SObjectDomain private static final String MSG_INVALID_CRITERIA = 'Relationship Criteria \'\'{0}\'\' is not valid, see SOQL documentation http://www.salesforce.com/us/developer/docs/soql_sosl/Content/sforce_api_calls_soql_select_conditionexpression.htm, error is \'\'{1}\'\''; - public class Constructor implements SObjectDomain.IConstructable + public class Constructor implements fflib_SObjectDomain.IConstructable { - public SObjectDomain construct(List sObjectList) + public fflib_SObjectDomain construct(List sObjectList) { return new RollupSummaries(sObjectList); } diff --git a/rolluptool/src/classes/RollupSummariesSelector.cls b/rolluptool/src/classes/RollupSummariesSelector.cls index 583735aa..d667d115 100644 --- a/rolluptool/src/classes/RollupSummariesSelector.cls +++ b/rolluptool/src/classes/RollupSummariesSelector.cls @@ -27,8 +27,16 @@ /** * Various queries for the Lookup Rollup Summary custom object **/ -public with sharing class RollupSummariesSelector extends SObjectSelector +public class RollupSummariesSelector extends fflib_SObjectSelector { + public RollupSummariesSelector() { + super(); + } + + public RollupSummariesSelector(boolean enforceSecurity) { + super(false, enforceSecurity, enforceSecurity); + } + public List getSObjectFieldList() { return new List { @@ -73,15 +81,15 @@ public with sharing class RollupSummariesSelector extends SObjectSelector **/ public List selectActiveByChildObject(List calculationModes, Set childObjectNames) { - assertIsAccessible(); List calculationModeNames = new List(); for(RollupSummaries.CalculationMode calculationMode : calculationModes) calculationModeNames.add(calculationMode.name()); - return Database.query(String.format( - 'select {0} from {1} where CalculationMode__c in :calculationModeNames and ChildObject__c in :childObjectNames and Active__c = true order by ParentObject__c, RelationshipField__c', - new List{getFieldListString(), - getSObjectName(), - getOrderBy()})); + return Database.query( + newQueryFactory(). + setCondition('CalculationMode__c in :calculationModeNames and ChildObject__c in :childObjectNames and Active__c = true'). + addOrdering(LookupRollupSummary__c.ParentObject__c, fflib_QueryFactory.SortOrder.ASCENDING). + addOrdering(LookupRollupSummary__c.RelationshipField__c, fflib_QueryFactory.SortOrder.ASCENDING). + toSOQL()); } /** @@ -89,12 +97,11 @@ public with sharing class RollupSummariesSelector extends SObjectSelector **/ public List selectActiveByUniqueName(Set uniqueNames) { - assertIsAccessible(); - return Database.query(String.format( - 'select {0} from {1} where UniqueName__c in :uniqueNames and Active__c = true order by ParentObject__c, RelationshipField__c', - new List{getFieldListString(), - getSObjectName(), - getOrderBy()})); - + return Database.query( + newQueryFactory(). + setCondition('UniqueName__c in :uniqueNames and Active__c = true'). + addOrdering(LookupRollupSummary__c.ParentObject__c, fflib_QueryFactory.SortOrder.ASCENDING). + addOrdering(LookupRollupSummary__c.RelationshipField__c, fflib_QueryFactory.SortOrder.ASCENDING). + toSOQL()); } } \ No newline at end of file diff --git a/rolluptool/src/classes/RollupSummariesTest.cls b/rolluptool/src/classes/RollupSummariesTest.cls index 38443d95..3543130e 100644 --- a/rolluptool/src/classes/RollupSummariesTest.cls +++ b/rolluptool/src/classes/RollupSummariesTest.cls @@ -25,7 +25,7 @@ **/ @IsTest -private with sharing class RollupSummariesTest +private class RollupSummariesTest { // TODO: Write a test to validate the criteria fields validation // ... @@ -55,11 +55,11 @@ private with sharing class RollupSummariesTest rollupSummary.AggregateResultField__c = 'AnnualRevenue'; rollupSummary.Active__c = true; rollupSummary.CalculationMode__c = 'Realtime'; - SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); - SObjectDomain.triggerHandler(RollupSummaries.class); - System.assertEquals(1, SObjectDomain.Errors.getAll().size()); - System.assertEquals('Relationship Criteria \'StageName Equals Won\' is not valid, see SOQL documentation http://www.salesforce.com/us/developer/docs/soql_sosl/Content/sforce_api_calls_soql_select_conditionexpression.htm, error is \'unexpected token: \'Equals\'\'', SObjectDomain.Errors.getAll()[0].message); - System.assertEquals(LookupRollupSummary__c.RelationShipCriteria__c, ((SObjectDomain.FieldError)SObjectDomain.Errors.getAll()[0]).field); + fflib_SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); + fflib_SObjectDomain.triggerHandler(RollupSummaries.class); + System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size()); + System.assertEquals('Relationship Criteria \'StageName Equals Won\' is not valid, see SOQL documentation http://www.salesforce.com/us/developer/docs/soql_sosl/Content/sforce_api_calls_soql_select_conditionexpression.htm, error is \'unexpected token: \'Equals\'\'', fflib_SObjectDomain.Errors.getAll()[0].message); + System.assertEquals(LookupRollupSummary__c.RelationShipCriteria__c, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[0]).field); } /** @@ -83,11 +83,11 @@ private with sharing class RollupSummariesTest rollupSummary.RelationshipCriteriaFields__c = 'Bad'; rollupSummary.Active__c = false; rollupSummary.CalculationMode__c = 'Realtime'; - SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); - SObjectDomain.triggerHandler(RollupSummaries.class); - System.assertEquals(1, SObjectDomain.Errors.getAll().size()); - System.assertEquals('Object does not exist.', SObjectDomain.Errors.getAll()[0].message); - System.assertEquals(LookupRollupSummary__c.ChildObject__c, ((SObjectDomain.FieldError)SObjectDomain.Errors.getAll()[0]).field); + fflib_SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); + fflib_SObjectDomain.triggerHandler(RollupSummaries.class); + System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size()); + System.assertEquals('Object does not exist.', fflib_SObjectDomain.Errors.getAll()[0].message); + System.assertEquals(LookupRollupSummary__c.ChildObject__c, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[0]).field); } /** @@ -111,11 +111,11 @@ private with sharing class RollupSummariesTest rollupSummary.RelationshipCriteriaFields__c = 'Bad'; rollupSummary.Active__c = false; rollupSummary.CalculationMode__c = 'Realtime'; - SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); - SObjectDomain.triggerHandler(RollupSummaries.class); - System.assertEquals(1, SObjectDomain.Errors.getAll().size()); - System.assertEquals('Object does not exist.', SObjectDomain.Errors.getAll()[0].message); - System.assertEquals(LookupRollupSummary__c.ChildObject__c, ((SObjectDomain.FieldError)SObjectDomain.Errors.getAll()[0]).field); + fflib_SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); + fflib_SObjectDomain.triggerHandler(RollupSummaries.class); + System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size()); + System.assertEquals('Object does not exist.', fflib_SObjectDomain.Errors.getAll()[0].message); + System.assertEquals(LookupRollupSummary__c.ChildObject__c, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[0]).field); } private testmethod static void testInsertActiveValidation() @@ -135,11 +135,11 @@ private with sharing class RollupSummariesTest rollupSummary.AggregateResultField__c = 'AnnualRevenue'; rollupSummary.Active__c = true; rollupSummary.CalculationMode__c = 'Realtime'; - SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); - SObjectDomain.triggerHandler(RollupSummaries.class); - System.assertEquals(1, SObjectDomain.Errors.getAll().size()); - System.assertEquals('Apex Trigger ' + RollupSummaries.makeTriggerName(rollupSummary) + ' has not been deployed. Click Manage Child Trigger and try again.', SObjectDomain.Errors.getAll()[0].message); - System.assertEquals(LookupRollupSummary__c.Active__c, ((SObjectDomain.FieldError)SObjectDomain.Errors.getAll()[0]).field); + fflib_SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); + fflib_SObjectDomain.triggerHandler(RollupSummaries.class); + System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size()); + System.assertEquals('Apex Trigger ' + RollupSummaries.makeTriggerName(rollupSummary) + ' has not been deployed. Click Manage Child Trigger and try again.', fflib_SObjectDomain.Errors.getAll()[0].message); + System.assertEquals(LookupRollupSummary__c.Active__c, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[0]).field); } private testmethod static void testInsertParentObjectValidation() @@ -159,11 +159,11 @@ private with sharing class RollupSummariesTest rollupSummary.AggregateResultField__c = 'AnnualRevenue'; rollupSummary.Active__c = true; rollupSummary.CalculationMode__c = 'Realtime'; - SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); - SObjectDomain.triggerHandler(RollupSummaries.class); - System.assertEquals(1, SObjectDomain.Errors.getAll().size()); - System.assertEquals('Object does not exist.', SObjectDomain.Errors.getAll()[0].message); - System.assertEquals(LookupRollupSummary__c.ParentObject__c, ((SObjectDomain.FieldError)SObjectDomain.Errors.getAll()[0]).field); + fflib_SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); + fflib_SObjectDomain.triggerHandler(RollupSummaries.class); + System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size()); + System.assertEquals('Object does not exist.', fflib_SObjectDomain.Errors.getAll()[0].message); + System.assertEquals(LookupRollupSummary__c.ParentObject__c, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[0]).field); } private testmethod static void testInsertChildObjectValidation() @@ -183,11 +183,11 @@ private with sharing class RollupSummariesTest rollupSummary.AggregateResultField__c = 'AnnualRevenue'; rollupSummary.Active__c = true; rollupSummary.CalculationMode__c = 'Realtime'; - SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); - SObjectDomain.triggerHandler(RollupSummaries.class); - System.assertEquals(1, SObjectDomain.Errors.getAll().size()); - System.assertEquals('Object does not exist.', SObjectDomain.Errors.getAll()[0].message); - System.assertEquals(LookupRollupSummary__c.ChildObject__c, ((SObjectDomain.FieldError)SObjectDomain.Errors.getAll()[0]).field); + fflib_SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); + fflib_SObjectDomain.triggerHandler(RollupSummaries.class); + System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size()); + System.assertEquals('Object does not exist.', fflib_SObjectDomain.Errors.getAll()[0].message); + System.assertEquals(LookupRollupSummary__c.ChildObject__c, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[0]).field); } private testmethod static void testInsertRelationshipFieldValidation() @@ -207,11 +207,11 @@ private with sharing class RollupSummariesTest rollupSummary.AggregateResultField__c = 'AnnualRevenue'; rollupSummary.Active__c = true; rollupSummary.CalculationMode__c = 'Realtime'; - SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); - SObjectDomain.triggerHandler(RollupSummaries.class); - System.assertEquals(1, SObjectDomain.Errors.getAll().size()); - System.assertEquals('Field does not exist.', SObjectDomain.Errors.getAll()[0].message); - System.assertEquals(LookupRollupSummary__c.RelationShipField__c, ((SObjectDomain.FieldError)SObjectDomain.Errors.getAll()[0]).field); + fflib_SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); + fflib_SObjectDomain.triggerHandler(RollupSummaries.class); + System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size()); + System.assertEquals('Field does not exist.', fflib_SObjectDomain.Errors.getAll()[0].message); + System.assertEquals(LookupRollupSummary__c.RelationShipField__c, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[0]).field); } private testmethod static void testInsertFieldToAggregateValidation() @@ -231,11 +231,11 @@ private with sharing class RollupSummariesTest rollupSummary.AggregateResultField__c = 'AnnualRevenue'; rollupSummary.Active__c = true; rollupSummary.CalculationMode__c = 'Realtime'; - SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); - SObjectDomain.triggerHandler(RollupSummaries.class); - System.assertEquals(1, SObjectDomain.Errors.getAll().size()); - System.assertEquals('Field does not exist.', SObjectDomain.Errors.getAll()[0].message); - System.assertEquals(LookupRollupSummary__c.FieldToAggregate__c, ((SObjectDomain.FieldError)SObjectDomain.Errors.getAll()[0]).field); + fflib_SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); + fflib_SObjectDomain.triggerHandler(RollupSummaries.class); + System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size()); + System.assertEquals('Field does not exist.', fflib_SObjectDomain.Errors.getAll()[0].message); + System.assertEquals(LookupRollupSummary__c.FieldToAggregate__c, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[0]).field); } private testmethod static void testInsertFieldToOrderByValidation() @@ -256,11 +256,11 @@ private with sharing class RollupSummariesTest rollupSummary.AggregateResultField__c = 'AnnualRevenue'; rollupSummary.Active__c = true; rollupSummary.CalculationMode__c = 'Realtime'; - SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); - SObjectDomain.triggerHandler(RollupSummaries.class); - System.assertEquals(1, SObjectDomain.Errors.getAll().size()); - System.assertEquals('Field does not exist.', SObjectDomain.Errors.getAll()[0].message); - System.assertEquals(LookupRollupSummary__c.FieldToOrderBy__c, ((SObjectDomain.FieldError)SObjectDomain.Errors.getAll()[0]).field); + fflib_SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); + fflib_SObjectDomain.triggerHandler(RollupSummaries.class); + System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size()); + System.assertEquals('Field does not exist.', fflib_SObjectDomain.Errors.getAll()[0].message); + System.assertEquals(LookupRollupSummary__c.FieldToOrderBy__c, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[0]).field); } private testmethod static void testInsertAggregateResultFieldValidation() @@ -280,11 +280,11 @@ private with sharing class RollupSummariesTest rollupSummary.AggregateResultField__c = 'AnnualRevenueX'; rollupSummary.Active__c = true; rollupSummary.CalculationMode__c = 'Realtime'; - SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); - SObjectDomain.triggerHandler(RollupSummaries.class); - System.assertEquals(1, SObjectDomain.Errors.getAll().size()); - System.assertEquals('Field does not exist.', SObjectDomain.Errors.getAll()[0].message); - System.assertEquals(LookupRollupSummary__c.AggregateResultField__c, ((SObjectDomain.FieldError)SObjectDomain.Errors.getAll()[0]).field); + fflib_SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); + fflib_SObjectDomain.triggerHandler(RollupSummaries.class); + System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size()); + System.assertEquals('Field does not exist.', fflib_SObjectDomain.Errors.getAll()[0].message); + System.assertEquals(LookupRollupSummary__c.AggregateResultField__c, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[0]).field); } private testmethod static void testInsertBadFieldTypesStringNotValid() @@ -304,11 +304,11 @@ private with sharing class RollupSummariesTest rollupSummary.AggregateResultField__c = 'AnnualRevenue'; rollupSummary.Active__c = true; rollupSummary.CalculationMode__c = 'Realtime'; - SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); - SObjectDomain.triggerHandler(RollupSummaries.class); - System.assertEquals(1, SObjectDomain.Errors.getAll().size()); - System.assertEquals('Only Date/DateTime/Time/Numeric fields are allowed for Sum, Max, Min and Avg', SObjectDomain.Errors.getAll()[0].message); - System.assertEquals(rollupSummary, ((SObjectDomain.ObjectError)SObjectDomain.Errors.getAll()[0]).record); + fflib_SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); + fflib_SObjectDomain.triggerHandler(RollupSummaries.class); + System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size()); + System.assertEquals('Only Date/DateTime/Time/Numeric fields are allowed for Sum, Max, Min and Avg', fflib_SObjectDomain.Errors.getAll()[0].message); + System.assertEquals(rollupSummary, ((fflib_SObjectDomain.ObjectError)fflib_SObjectDomain.Errors.getAll()[0]).record); } private testmethod static void testInsertBadFieldTypeSumNotValid() @@ -328,11 +328,11 @@ private with sharing class RollupSummariesTest rollupSummary.AggregateResultField__c = 'SLAExpirationDate__c'; rollupSummary.Active__c = true; rollupSummary.CalculationMode__c = 'Realtime'; - SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); - SObjectDomain.triggerHandler(RollupSummaries.class); - System.assertEquals(1, SObjectDomain.Errors.getAll().size()); - System.assertEquals('Sum/Avg doesnt looks like valid for dates ! Still want, then implement the IRollerCoaster yourself and change this class as required.', SObjectDomain.Errors.getAll()[0].message); - System.assertEquals(rollupSummary, ((SObjectDomain.ObjectError)SObjectDomain.Errors.getAll()[0]).record); + fflib_SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); + fflib_SObjectDomain.triggerHandler(RollupSummaries.class); + System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size()); + System.assertEquals('Sum/Avg doesnt looks like valid for dates ! Still want, then implement the IRollerCoaster yourself and change this class as required.', fflib_SObjectDomain.Errors.getAll()[0].message); + System.assertEquals(rollupSummary, ((fflib_SObjectDomain.ObjectError)fflib_SObjectDomain.Errors.getAll()[0]).record); } private testmethod static void testTriggerAndTestClassNaming() @@ -374,13 +374,13 @@ private with sharing class RollupSummariesTest rollupSummary.AggregateResultField__c = 'AnnualRevenue'; rollupSummary.Active__c = true; rollupSummary.CalculationMode__c = 'Realtime'; - SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); - SObjectDomain.triggerHandler(RollupSummaries.class); - System.assertEquals(2, SObjectDomain.Errors.getAll().size()); - System.assertEquals('Field Stage does not exist on the child object.', SObjectDomain.Errors.getAll()[0].message); - System.assertEquals(LookupRollupSummary__c.RelationShipCriteriaFields__c, ((SObjectDomain.FieldError)SObjectDomain.Errors.getAll()[0]).field); - System.assertEquals('Relationship Criteria \'Stage = \'Won\'\' is not valid, see SOQL documentation http://www.salesforce.com/us/developer/docs/soql_sosl/Content/sforce_api_calls_soql_select_conditionexpression.htm, error is \'No such column \'Stage\' on entity \'Opportunity\'. If you are attempting to use a custom field, be sure to append the \'__c\' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\'', SObjectDomain.Errors.getAll()[1].message); - System.assertEquals(LookupRollupSummary__c.RelationShipCriteria__c, ((SObjectDomain.FieldError)SObjectDomain.Errors.getAll()[1]).field); + fflib_SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); + fflib_SObjectDomain.triggerHandler(RollupSummaries.class); + System.assertEquals(2, fflib_SObjectDomain.Errors.getAll().size()); + System.assertEquals('Field Stage does not exist on the child object.', fflib_SObjectDomain.Errors.getAll()[0].message); + System.assertEquals(LookupRollupSummary__c.RelationShipCriteriaFields__c, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[0]).field); + System.assertEquals('Relationship Criteria \'Stage = \'Won\'\' is not valid, see SOQL documentation http://www.salesforce.com/us/developer/docs/soql_sosl/Content/sforce_api_calls_soql_select_conditionexpression.htm, error is \'No such column \'Stage\' on entity \'Opportunity\'. If you are attempting to use a custom field, be sure to append the \'__c\' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\'', fflib_SObjectDomain.Errors.getAll()[1].message); + System.assertEquals(LookupRollupSummary__c.RelationShipCriteria__c, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[1]).field); } private testmethod static void testRelationshipCriteriaFieldsValidationMulti() @@ -401,13 +401,13 @@ private with sharing class RollupSummariesTest rollupSummary.AggregateResultField__c = 'AnnualRevenue'; rollupSummary.Active__c = true; rollupSummary.CalculationMode__c = 'Realtime'; - SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); - SObjectDomain.triggerHandler(RollupSummaries.class); - System.assertEquals(2, SObjectDomain.Errors.getAll().size()); - System.assertEquals('Fields Stage,Another do not exist on the child object.', SObjectDomain.Errors.getAll()[0].message); - System.assertEquals(LookupRollupSummary__c.RelationShipCriteriaFields__c, ((SObjectDomain.FieldError)SObjectDomain.Errors.getAll()[0]).field); - System.assertEquals('Relationship Criteria \'Stage = \'Won\'\' is not valid, see SOQL documentation http://www.salesforce.com/us/developer/docs/soql_sosl/Content/sforce_api_calls_soql_select_conditionexpression.htm, error is \'No such column \'Stage\' on entity \'Opportunity\'. If you are attempting to use a custom field, be sure to append the \'__c\' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\'', SObjectDomain.Errors.getAll()[1].message); - System.assertEquals(LookupRollupSummary__c.RelationShipCriteria__c, ((SObjectDomain.FieldError)SObjectDomain.Errors.getAll()[1]).field); + fflib_SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); + fflib_SObjectDomain.triggerHandler(RollupSummaries.class); + System.assertEquals(2, fflib_SObjectDomain.Errors.getAll().size()); + System.assertEquals('Fields Stage,Another do not exist on the child object.', fflib_SObjectDomain.Errors.getAll()[0].message); + System.assertEquals(LookupRollupSummary__c.RelationShipCriteriaFields__c, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[0]).field); + System.assertEquals('Relationship Criteria \'Stage = \'Won\'\' is not valid, see SOQL documentation http://www.salesforce.com/us/developer/docs/soql_sosl/Content/sforce_api_calls_soql_select_conditionexpression.htm, error is \'No such column \'Stage\' on entity \'Opportunity\'. If you are attempting to use a custom field, be sure to append the \'__c\' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\'', fflib_SObjectDomain.Errors.getAll()[1].message); + System.assertEquals(LookupRollupSummary__c.RelationShipCriteria__c, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[1]).field); } private testmethod static void testInsertCountByCloseDateNoErrors() @@ -427,9 +427,9 @@ private with sharing class RollupSummariesTest rollupSummary.AggregateResultField__c = 'SLAExpirationDate__c'; rollupSummary.Active__c = true; rollupSummary.CalculationMode__c = 'Realtime'; - SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); - SObjectDomain.triggerHandler(RollupSummaries.class); - System.assertEquals(0, SObjectDomain.Errors.getAll().size()); + fflib_SObjectDomain.Test.Database.onInsert(new LookupRollupSummary__c[] { rollupSummary } ); + fflib_SObjectDomain.triggerHandler(RollupSummaries.class); + System.assertEquals(0, fflib_SObjectDomain.Errors.getAll().size()); } } \ No newline at end of file diff --git a/rolluptool/src/classes/RollupSummaryScheduleItemsSelector.cls b/rolluptool/src/classes/RollupSummaryScheduleItemsSelector.cls index 16fa844b..09ae438d 100644 --- a/rolluptool/src/classes/RollupSummaryScheduleItemsSelector.cls +++ b/rolluptool/src/classes/RollupSummaryScheduleItemsSelector.cls @@ -24,7 +24,7 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public with sharing class RollupSummaryScheduleItemsSelector extends SObjectSelector +public class RollupSummaryScheduleItemsSelector extends fflib_SObjectSelector { public List getSObjectFieldList() { @@ -47,9 +47,7 @@ public with sharing class RollupSummaryScheduleItemsSelector extends SObjectSele public Database.Querylocator selectAllQueryLocator() { - assertIsAccessible(); - return Database.getQueryLocator(String.format('SELECT {0} FROM {1} ORDER BY {2}', - new List{getFieldListString(),getSObjectName(),getOrderBy()})); + return Database.getQueryLocator(newQueryFactory().toSOQL()); } public List selectById(Set idSet) diff --git a/rolluptool/src/classes/SObjectDomain.cls b/rolluptool/src/classes/SObjectDomain.cls deleted file mode 100644 index dcdfd811..00000000 --- a/rolluptool/src/classes/SObjectDomain.cls +++ /dev/null @@ -1,385 +0,0 @@ -/** - * Copyright (c), 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. -**/ - -/** - * Base class aiding in the implemetnation of a Domain Model around SObject collections - * - * Domain (software engineering). “a set of common requirements, terminology, and functionality - * for any software program constructed to solve a problem in that field”, - * http://en.wikipedia.org/wiki/Domain_(software_engineering) - * - * Domain Model, “An object model of the domain that incorporates both behavior and data.”, - * “At its worst business logic can be very complex. Rules and logic describe many different " - * "cases and slants of behavior, and it's this complexity that objects were designed to work with...” - * Martin Fowler, EAA Patterns - * http://martinfowler.com/eaaCatalog/domainModel.html - * - **/ -public virtual with sharing class SObjectDomain -{ - public List Records { get; private set;} - - public Schema.DescribeSObjectResult SObjectDescribe {get; private set;} - - public static ErrorFactory Errors {get; private set;} - - public static TestFactory Test {get; private set;} - - static - { - Errors = new ErrorFactory(); - - Test = new TestFactory(); - } - - public SObjectDomain(List sObjectList) - { - Records = sObjectList; - - SObjectDescribe = Records.getSObjectType().getDescribe(); - } - - /** - * Override this to apply defaults to the records, this is called by the handleBeforeInsert method - **/ - public virtual void onApplyDefaults() { } - - /** - * Override this to apply general validation to be performed during insert or update, called by the handleAfterInsert and handleAfterUpdate methods - **/ - public virtual void onValidate() { } - - /** - * Override this to apply validation to be performed during insert, called by the handleAfterUpdate method - **/ - public virtual void onValidate(Map existingRecords) { } - - /** - * Override this to perform processing during the before insert phase, this is called by the handleBeforeInsert method - **/ - public virtual void onBeforeInsert() { } - - /** - * Override this to perform processing during the before update phase, this is called by the handleBeforeUpdate method - **/ - public virtual void onBeforeUpdate(Map existingRecords) { } - - /** - * Override this to perform processing during the before delete phase, this is called by the handleBeforeDelete method - **/ - public virtual void onBeforeDelete() { } - - /** - * Override this to perform processing during the after insert phase, this is called by the handleAfterInsert method - **/ - public virtual void onAfterInsert() { } - - /** - * Override this to perform processing during the after update phase, this is called by the handleAfterUpdate method - **/ - public virtual void onAfterUpdate(Map existingRecords) { } - - /** - * Override this to perform processing during the after delete phase, this is called by the handleAfterDelete method - **/ - public virtual void onAfterDelete() { } - - /** - * Base handler for the Apex Trigger event Before Insert, calls the onApplyDefaults method, followed by onBeforeInsert - **/ - public virtual void handleBeforeInsert() - { - onApplyDefaults(); - onBeforeInsert(); - } - - /** - * Base handler for the Apex Trigger event Before Update, calls the onBeforeUpdate method - **/ - public void handleBeforeUpdate(Map existingRecords) - { - onBeforeUpdate(existingRecords); - } - - /** - * Base handler for the Apex Trigger event Before Delete, calls the onBeforeDelete method - **/ - public void handleBeforeDelete() - { - onBeforeDelete(); - } - - /** - * Base handler for the Apex Trigger event After Insert, checks object security and calls the onValidate and onAfterInsert methods - * - * @throws DomainException if the current user context is not able to create records - **/ - public void handleAfterInsert() - { - if(!SObjectDescribe.isCreateable()) - throw new DomainException('Permission to create an ' + SObjectDescribe.getName() + ' denied.'); - - onValidate(); - onAfterInsert(); - } - - /** - * Base handler for the Apex Trigger event After Update, checks object security and calls the onValidate, onValidate(Map) and onAfterUpdate methods - * - * @throws DomainException if the current user context is not able to update records - **/ - public void handleAfterUpdate(Map existingRecords) - { - if(!SObjectDescribe.isUpdateable()) - throw new DomainException('Permission to udpate an ' + SObjectDescribe.getName() + ' denied.'); - - onValidate(); - onValidate(existingRecords); - onAfterUpdate(existingRecords); - } - - /** - * Base handler for the Apex Trigger event After Delete, checks object security and calls the onAfterDelete method - * - * @throws DomainException if the current user context is not able to delete records - **/ - public void handleAfterDelete() - { - if(!SObjectDescribe.isDeletable()) - throw new DomainException('Permission to delete an ' + SObjectDescribe.getName() + ' denied.'); - - onAfterDelete(); - } - - public interface IConstructable - { - SObjectDomain construct(List sObjectList); - } - - /** - * Method constructs the given Domain class with the current Trigger context - * before calling the applicable override methods such as beforeInsert, beforeUpdate etc. - **/ - public static void triggerHandler(Type domainClass) - { - // Construct the domain class constructor class - String domainClassName = domainClass.getName(); - Type constructableClass = domainClassName.endsWith('Constructor') ? Type.forName(domainClassName) : Type.forName(domainClassName+'.Constructor'); - IConstructable constructor = (IConstructable) constructableClass.newInstance(); - - // Process the trigger context - if(System.Test.isRunningTest() & Test.Database.hasRecords()) - { - // If in test context and records in the mock database delegate initially to the mock database trigger handler - Test.Database.testTriggerHandler(constructor); - } - else - { - // Process the runtime Apex Trigger context - triggerHandler(constructor, - Trigger.isBefore, - Trigger.isAfter, - Trigger.isInsert, - Trigger.isUpdate, - Trigger.isDelete, - Trigger.new, - Trigger.oldMap); - } - } - - /** - * Calls the applicable override methods such as beforeInsert, beforeUpdate etc. based on a Trigger context - **/ - private static void triggerHandler(IConstructable domainConstructor, Boolean isBefore, Boolean isAfter, Boolean isInsert, Boolean isUpdate, Boolean isDelete, List newRecords, Map oldRecordsMap) - { - if(isBefore) - { - if(isInsert) domainConstructor.construct(newRecords).handleBeforeInsert(); - else if(isUpdate) domainConstructor.construct(newRecords).handleBeforeUpdate(oldRecordsMap); - else if(isDelete) domainConstructor.construct(oldRecordsMap.values()).handleBeforeDelete(); - } - else - { - if(isInsert) domainConstructor.construct(newRecords).handleAfterInsert(); - else if(isUpdate) domainConstructor.construct(newRecords).handleAfterUpdate(oldRecordsMap); - else if(isDelete) domainConstructor.construct(oldRecordsMap.values()).handleAfterDelete(); - } - } - - public class DomainException extends Exception - { - } - - public String error(String message, SObject record) - { - return Errors.error(this, message, record); - } - - public String error(String message, SObject record, SObjectField field) - { - return Errors.error(this, message, record, field); - } - - public class ErrorFactory - { - private List errorList = new List(); - - private ErrorFactory() - { - - } - - public String error(String message, SObject record) - { - return error(null, message, record); - } - - private String error(SObjectDomain domain, String message, SObject record) - { - ObjectError objectError = new ObjectError(); - objectError.domain = domain; - objectError.message = message; - objectError.record = record; - errorList.add(objectError); - return message; - } - - public String error(String message, SObject record, SObjectField field) - { - return error(null, message, record, field); - } - - private String error(SObjectDomain domain, String message, SObject record, SObjectField field) - { - FieldError fieldError = new FieldError(); - fieldError.domain = domain; - fieldError.message = message; - fieldError.record = record; - fieldError.field = field; - errorList.add(fieldError); - return message; - } - - public List getAll() - { - return errorList.clone(); - } - - public void clearAll() - { - errorList.clear(); - } - } - - public class FieldError extends ObjectError - { - public SObjectField field; - - private FieldError() - { - - } - } - - public virtual class ObjectError extends Error - { - public SObject record; - - private ObjectError() - { - - } - } - - public abstract class Error - { - public String message; - public SObjectDomain domain; - } - - public class TestFactory - { - public MockDatabase Database = new MockDatabase(); - - private TestFactory() - { - - } - } - - public class MockDatabase - { - private Boolean isInsert = false; - private Boolean isUpdate = false; - private Boolean isDelete = false; - private List records = new List(); - private Map oldRecords = new Map(); - - private MockDatabase() - { - - } - - private void testTriggerHandler(IConstructable domainConstructor) - { - // Mock Before - triggerHandler(domainConstructor, true, false, isInsert, isUpdate, isDelete, records, oldRecords); - - // Mock After - triggerHandler(domainConstructor, false, true, isInsert, isUpdate, isDelete, records, oldRecords); - } - - public void onInsert(List records) - { - this.isInsert = true; - this.isUpdate = false; - this.isDelete = false; - this.records = records; - } - - public void onUpdate(List records, Map oldRecords) - { - this.isInsert = false; - this.isUpdate = true; - this.isDelete = false; - this.records = records; - this.oldRecords = oldRecords; - } - - public void onDelete(Map records) - { - this.isInsert = false; - this.isUpdate = false; - this.isDelete = true; - this.oldRecords = records; - } - - public Boolean hasRecords() - { - return records!=null && records.size()>0 || oldRecords!=null && oldRecords.size()>0; - } - } -} \ No newline at end of file diff --git a/rolluptool/src/classes/SObjectDomainTest.cls b/rolluptool/src/classes/SObjectDomainTest.cls deleted file mode 100644 index 14ad5428..00000000 --- a/rolluptool/src/classes/SObjectDomainTest.cls +++ /dev/null @@ -1,238 +0,0 @@ -/** - * Copyright (c), 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 with sharing class SObjectDomainTest -{ - - @IsTest - private static void testValidationWithoutDML() - { - TestSObjectDomain opps = new TestSObjectDomain(new Opportunity[] { new Opportunity ( Name = 'Test', Type = 'Existing Account' ) } ); - opps.onValidate(); - System.assertEquals(1, SObjectDomain.Errors.getAll().size()); - System.assertEquals('You must provide an Account for Opportunities for existing Customers.', SObjectDomain.Errors.getAll()[0].message); - System.assertEquals(Opportunity.AccountId, ((SObjectDomain.FieldError)SObjectDomain.Errors.getAll()[0]).field); - } - - @IsTest - private static void testInsertValidationFailedWithoutDML() - { - Opportunity opp = new Opportunity ( Name = 'Test', Type = 'Existing Account' ); - System.assertEquals(false, SObjectDomain.Test.Database.hasRecords()); - SObjectDomain.Test.Database.onInsert(new Opportunity[] { opp } ); - System.assertEquals(true, SObjectDomain.Test.Database.hasRecords()); - SObjectDomain.triggerHandler(TestSObjectDomainConstructor.class); - System.assertEquals(1, SObjectDomain.Errors.getAll().size()); - System.assertEquals('You must provide an Account for Opportunities for existing Customers.', SObjectDomain.Errors.getAll()[0].message); - System.assertEquals(Opportunity.AccountId, ((SObjectDomain.FieldError)SObjectDomain.Errors.getAll()[0]).field); - } - - @IsTest - private static void testUpdateValidationFailedWithoutDML() - { - Opportunity oldOpp = (Opportunity) Opportunity.sObjectType.newSObject('006E0000006mkRQ'); - oldOpp.Name = 'Test'; - oldOpp.Type = 'Existing Account'; - Opportunity newOpp = (Opportunity) Opportunity.sObjectType.newSObject('006E0000006mkRQ'); - newOpp.Name = 'Test'; - newOpp.Type = 'New Account'; - System.assertEquals(false, SObjectDomain.Test.Database.hasRecords()); - SObjectDomain.Test.Database.onUpdate(new Opportunity[] { newOpp }, new Map { newOpp.Id => oldOpp } ); - System.assertEquals(true, SObjectDomain.Test.Database.hasRecords()); - SObjectDomain.triggerHandler(TestSObjectDomainConstructor.class); - System.assertEquals(1, SObjectDomain.Errors.getAll().size()); - System.assertEquals('You cannot change the Opportunity type once it has been created.', SObjectDomain.Errors.getAll()[0].message); - System.assertEquals(Opportunity.Type, ((SObjectDomain.FieldError)SObjectDomain.Errors.getAll()[0]).field); - } - - @IsTest - private static void testOnBeforeDeleteWithoutDML() - { - Opportunity opp = (Opportunity) Opportunity.sObjectType.newSObject('006E0000006mkRQ'); - opp.Name = 'Test'; - opp.Type = 'Existing Account'; - System.assertEquals(false, SObjectDomain.Test.Database.hasRecords()); - SObjectDomain.Test.Database.onDelete(new Map { opp.Id => opp } ); - System.assertEquals(true, SObjectDomain.Test.Database.hasRecords()); - SObjectDomain.triggerHandler(TestSObjectDomainConstructor.class); - System.assertEquals(1, SObjectDomain.Errors.getAll().size()); - System.assertEquals('You cannot delete this Opportunity.', SObjectDomain.Errors.getAll()[0].message); - } - - @IsTest - private static void testObjectSecurity() - { - // Create a user which will not have access to the test object type - User testUser = createChatterExternalUser(); - if(testUser==null) - return; // Abort the test if unable to create a user with low enough acess - System.runAs(testUser) - { - // Test Create object security - Opportunity opp = new Opportunity ( Name = 'Test', Type = 'Existing Account' ); - SObjectDomain.Test.Database.onInsert(new Opportunity[] { opp } ); - try { - SObjectDomain.triggerHandler(TestSObjectDomainConstructor.class); - System.assert(false, 'Expected access denied exception'); - } catch (Exception e) { - System.assertEquals('Permission to create an Opportunity denied.', e.getMessage()); - } - - // Test Update object security - Opportunity existingOpp = (Opportunity) Opportunity.sObjectType.newSObject('006E0000006mkRQ'); - existingOpp.Name = 'Test'; - existingOpp.Type = 'Existing Account'; - SObjectDomain.Test.Database.onUpdate(new List { opp }, new Map { opp.Id => opp } ); - try { - SObjectDomain.triggerHandler(TestSObjectDomainConstructor.class); - System.assert(false, 'Expected access denied exception'); - } catch (Exception e) { - System.assertEquals('Permission to udpate an Opportunity denied.', e.getMessage()); - } - - // Test Delete object security - SObjectDomain.Test.Database.onDelete(new Map { opp.Id => opp }); - try { - SObjectDomain.triggerHandler(TestSObjectDomainConstructor.class); - System.assert(false, 'Expected access denied exception'); - } catch (Exception e) { - System.assertEquals('Permission to delete an Opportunity denied.', e.getMessage()); - } - } - } - - @IsTest - public static void testErrorLogging() - { - // Test static helpers for raise none domain object instance errors - Opportunity opp = new Opportunity ( Name = 'Test', Type = 'Existing Account' ); - SObjectDomain.Errors.error('Error', opp); - SObjectDomain.Errors.error('Error', opp, Opportunity.Type); - System.assertEquals(2, SObjectDomain.Errors.getAll().size()); - System.assertEquals('Error', SObjectDomain.Errors.getAll()[0].message); - System.assertEquals('Error', SObjectDomain.Errors.getAll()[1].message); - System.assertEquals(Opportunity.Type, ((SObjectDomain.FieldError)SObjectDomain.Errors.getAll()[1]).field); - SObjectDomain.Errors.clearAll(); - System.assertEquals(0, SObjectDomain.Errors.getAll().size()); - } - - private static User createChatterExternalUser() - { - // Can only proceed with test if we have a suitable profile - Chatter External license has no access to Opportunity - List testProfiles = [Select Id From Profile where UserLicense.Name='Chatter External' limit 1]; - if(testProfiles.size()!=1) - return null; - - // Can only proceed with test if we can successfully insert a test user - String testUsername = System.now().format('yyyyMMddhhmmss') + '@testorg.com'; - User testUser = new User(Alias = 'test1', Email='testuser1@testorg.com', EmailEncodingKey='UTF-8', LastName='Testing', LanguageLocaleKey='en_US', LocaleSidKey='en_US', ProfileId = testProfiles[0].Id, TimeZoneSidKey='America/Los_Angeles', UserName=testUsername); - try { - insert testUser; - } catch (Exception e) { - return null; - } - return testUser; - } - - /** - * Test domain class - **/ - public with sharing class TestSObjectDomain extends SObjectDomain - { - public TestSObjectDomain(List sObjectList) - { - // Domain classes are initialised with lists to enforce bulkification throughout - super(sObjectList); - } - - public override void onApplyDefaults() - { - // Not required in production code - super.onApplyDefaults(); - - // Apply defaults to TestSObjectDomain - for(Opportunity opportunity : (List) Records) - { - opportunity.CloseDate = System.today().addDays(30); - } - } - - public override void onValidate() - { - // Not required in production code - super.onValidate(); - - // Validate TestSObjectDomain - for(Opportunity opp : (List) Records) - { - if(opp.Type!=null && opp.Type.startsWith('Existing') && opp.AccountId == null) - { - opp.AccountId.addError( error('You must provide an Account for Opportunities for existing Customers.', opp, Opportunity.AccountId) ); - } - } - } - - public override void onValidate(Map existingRecords) - { - // Not required in production code - super.onValidate(existingRecords); - - // Validate changes to TestSObjectDomain - for(Opportunity opp : (List) Records) - { - Opportunity existingOpp = (Opportunity) existingRecords.get(opp.Id); - if(opp.Type != existingOpp.Type) - { - opp.Type.addError( error('You cannot change the Opportunity type once it has been created.', opp, Opportunity.Type) ); - } - } - } - - public override void onBeforeDelete() - { - // Not required in production code - super.onBeforeDelete(); - - // Validate changes to TestSObjectDomain - for(Opportunity opp : (List) Records) - { - opp.addError( error('You cannot delete this Opportunity.', opp) ); - } - } - } - - /** - * Typically an inner class to the domain class, supported here for test purposes - **/ - public class TestSObjectDomainConstructor implements SObjectDomain.IConstructable - { - public SObjectDomain construct(List sObjectList) - { - return new TestSObjectDomain(sObjectList); - } - } -} \ No newline at end of file diff --git a/rolluptool/src/classes/SObjectSelector.cls b/rolluptool/src/classes/SObjectSelector.cls deleted file mode 100644 index 137b3b1c..00000000 --- a/rolluptool/src/classes/SObjectSelector.cls +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Copyright (c), 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 abstract with sharing class SObjectSelector -{ - private StringBuilder.FieldListBuilder m_fieldListBuilder; - - private static Set whiteListStandardWithoutCurrencyIso = - new Set { 'AsyncApexJob', 'ApexClass', 'ApexTrigger' }; - - public SObjectSelector() - { - } - - abstract List getSObjectFieldList(); - - abstract Schema.SObjectType getSObjectType(); - - public StringBuilder.FieldListBuilder getFieldListBuilder() - { - if(m_fieldListBuilder == null) - m_fieldListBuilder = whiteListStandardWithoutCurrencyIso.contains(getSObjectType().getDescribe().getName()) ? - new StringBuilder.FieldListBuilder( getSObjectFieldList() ) : - new StringBuilder.MultiCurrencyFieldListBuilder( getSObjectFieldList()); - return m_fieldListBuilder; - } - - public void setFieldListBuilder(StringBuilder.FieldListBuilder fieldListBuilder) - { - m_fieldListBuilder = fieldListBuilder; - } - - public String getFieldListString() - { - return getFieldListBuilder().getStringValue(); - } - - public String getRelatedFieldListString(String relation) - { - return getFieldListBuilder().getStringValue(relation + '.'); - } - - public String getSObjectName() - { - return getSObjectType().getDescribe().getName(); - } - - public virtual String getOrderBy() - { - return 'Name'; - } - - public List selectSObjectsById(Set idSet) - { - assertIsAccessible(); - return Database.query(String.format('SELECT {0} FROM {1} WHERE id in :idSet ORDER BY {2}', new List{getFieldListString(),getSObjectName(),getOrderBy()})); - } - - public void assertIsAccessible() - { - if(!getSObjectType().getDescribe().isAccessible()) - throw new SObjectDomain.DomainException('Permission to access an ' + getSObjectType().getDescribe().getName() + ' denied.'); - } -} \ No newline at end of file diff --git a/rolluptool/src/classes/SObjectSelectorTest.cls b/rolluptool/src/classes/SObjectSelectorTest.cls deleted file mode 100644 index 1a1de596..00000000 --- a/rolluptool/src/classes/SObjectSelectorTest.cls +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Copyright (c), 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 with sharing class SObjectSelectorTest -{ - static testMethod void testGetFieldListString() - { - TestSObjectSelector selector = new TestSObjectSelector(); - if(UserInfo.isMultiCurrencyOrganization()) - system.assertEquals('Name,Id,AccountNumber,AnnualRevenue,CurrencyIsoCode',selector.getFieldListString()); - else - system.assertEquals('Name,Id,AccountNumber,AnnualRevenue',selector.getFieldListString()); - } - - static testMethod void testGetSObjectName() - { - TestSObjectSelector selector = new TestSObjectSelector(); - system.assertEquals('Account',selector.getSObjectName()); - } - - static testMethod void testSelectSObjectsById() - { - // Inserting in reverse order so that we can test the order by of select - List accountList = new List { - new Account(Name='TestAccount2',AccountNumber='A2',AnnualRevenue=12345.67), - new Account(Name='TestAccount1',AccountNumber='A1',AnnualRevenue=76543.21) }; - insert accountList; - Set idSet = new Set(); - for(Account item : accountList) - idSet.add(item.Id); - - Test.startTest(); - TestSObjectSelector selector = new TestSObjectSelector(); - List result = (List) selector.selectSObjectsById(idSet); - Test.stopTest(); - - system.assertEquals(2,result.size()); - system.assertEquals('TestAccount1',result[0].Name); - system.assertEquals('A1',result[0].AccountNumber); - system.assertEquals(76543.21,result[0].AnnualRevenue); - system.assertEquals('TestAccount2',result[1].Name); - system.assertEquals('A2',result[1].AccountNumber); - system.assertEquals(12345.67,result[1].AnnualRevenue); - } - - static testMethod void testAssertIsAccessible() - { - List accountList = new List { - new Account(Name='TestAccount2',AccountNumber='A2',AnnualRevenue=12345.67), - new Account(Name='TestAccount1',AccountNumber='A1',AnnualRevenue=76543.21) }; - insert accountList; - Set idSet = new Set(); - for(Account item : accountList) - idSet.add(item.Id); - - // Create a user which will not have access to the test object type - User testUser = createChatterExternalUser(); - if(testUser==null) - return; // Abort the test if unable to create a user with low enough acess - System.runAs(testUser) - { - TestSObjectSelector selector = new TestSObjectSelector(); - try - { - List result = (List) selector.selectSObjectsById(idSet); - System.assert(false,'Expected exception was not thrown'); - } - catch(SObjectDomain.DomainException e) - { - System.assertEquals('Permission to access an Account denied.',e.getMessage()); - } - } - } - - private class TestSObjectSelector extends SObjectSelector - { - public List getSObjectFieldList() - { - return new List { - Account.Name, - Account.Id, - Account.AccountNumber, - Account.AnnualRevenue - }; - } - - public Schema.SObjectType getSObjectType() - { - return Account.sObjectType; - } - } - - private static User createChatterExternalUser() - { - // Can only proceed with test if we have a suitable profile - Chatter External license has no access to Opportunity - List testProfiles = [Select Id From Profile where UserLicense.Name='Chatter External' limit 1]; - if(testProfiles.size()!=1) - return null; - - // Can only proceed with test if we can successfully insert a test user - String testUsername = System.now().format('yyyyMMddhhmmss') + '@testorg.com'; - User testUser = new User(Alias = 'test1', Email='testuser1@testorg.com', EmailEncodingKey='UTF-8', LastName='Testing', LanguageLocaleKey='en_US', LocaleSidKey='en_US', ProfileId = testProfiles[0].Id, TimeZoneSidKey='America/Los_Angeles', UserName=testUsername); - try { - insert testUser; - } catch (Exception e) { - return null; - } - return testUser; - } -} \ No newline at end of file diff --git a/rolluptool/src/classes/StringBuilder.cls b/rolluptool/src/classes/StringBuilder.cls deleted file mode 100644 index a7c13746..00000000 --- a/rolluptool/src/classes/StringBuilder.cls +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Copyright (c), 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 virtual with sharing class StringBuilder -{ - protected String m_stringValue; - - public StringBuilder() {} - - public StringBuilder(List values) - { - add(values); - } - - public virtual void add(List values) - { - for(String value : values) - add(value); - } - - public virtual void add(String value) - { - m_stringValue = ( m_stringValue==null ? value : m_stringValue + value ); - } - - public virtual String getStringValue() - { - return m_stringValue; - } - - - /** Subclasses **/ - - public virtual with sharing class CSVBuilder extends StringBuilder - { - public CSVBuilder() {} - - public CSVBuilder(List values) - { - super(values); - } - - public virtual override void add(String value) - { - m_stringValue = ( m_stringValue==null ? '{0}' + value : m_stringValue + ',{0}' + value ); - } - - public override String getStringValue() - { - return getStringValue(''); - } - - public String getStringValue(String itemPrefix) - { - return m_stringValue==null ? null : String.format(m_stringValue,new List{itemPrefix}); - } - } - - public virtual with sharing class FieldListBuilder extends CSVBuilder - { - public FieldListBuilder(List values) - { - for(Schema.SObjectField value : values) - add(value.getDescribe().getName()); - } - } - - public with sharing class MultiCurrencyFieldListBuilder extends FieldListBuilder - { - public MultiCurrencyFieldListBuilder(List values) - { - super(values); - - if(Userinfo.isMultiCurrencyOrganization()) - add('CurrencyIsoCode'); - } - } -} \ No newline at end of file diff --git a/rolluptool/src/classes/StringBuilder.cls-meta.xml b/rolluptool/src/classes/StringBuilder.cls-meta.xml deleted file mode 100644 index 08d159fb..00000000 --- a/rolluptool/src/classes/StringBuilder.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 32.0 - Active - diff --git a/rolluptool/src/classes/TestContext.cls b/rolluptool/src/classes/TestContext.cls index b181c47f..a98c3252 100644 --- a/rolluptool/src/classes/TestContext.cls +++ b/rolluptool/src/classes/TestContext.cls @@ -24,7 +24,7 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public with sharing class TestContext +public class TestContext { private TestContext() { } diff --git a/rolluptool/src/classes/Utilities.cls b/rolluptool/src/classes/Utilities.cls index 60be0d94..cf74e4b3 100644 --- a/rolluptool/src/classes/Utilities.cls +++ b/rolluptool/src/classes/Utilities.cls @@ -1,4 +1,30 @@ -public with sharing class Utilities { +/** + * 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 class Utilities { /** * Get the namespace of this package **/ diff --git a/rolluptool/src/classes/fflib_ApexMocks.cls b/rolluptool/src/classes/fflib_ApexMocks.cls new file mode 100644 index 00000000..ee38f1f8 --- /dev/null +++ b/rolluptool/src/classes/fflib_ApexMocks.cls @@ -0,0 +1,255 @@ +/** + * Copyright (c) 2014, FinancialForce.com, inc + * 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 FinancialForce.com, inc 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 fflib_ApexMocks +{ + public static final Integer NEVER = 0; + + private final fflib_MethodCountRecorder methodCountRecorder; + private final fflib_MethodReturnValueRecorder methodReturnValueRecorder; + + public Boolean Verifying + { + get + { + return methodCountRecorder.Verifying; + } + + private set; + } + + public Boolean Stubbing + { + get + { + return methodReturnValueRecorder.Stubbing; + } + + private set; + } + + public Exception DoThrowWhenException + { + get + { + return methodReturnValueRecorder.DoThrowWhenException; + } + + set + { + methodReturnValueRecorder.DoThrowWhenException = value; + } + } + + /** + * Construct an ApexMocks instance. + */ + public fflib_ApexMocks() + { + methodCountRecorder = new fflib_MethodCountRecorder(); + methodReturnValueRecorder = new fflib_MethodReturnValueRecorder(); + + methodCountRecorder.Verifying = false; + methodReturnValueRecorder.Stubbing = false; + } + + public static String extractTypeName(Object mockInstance) + { + return String.valueOf(mockInstance).split(':').get(0); + } + + /** + * Verify a method was called on a mock object. + * @param mockInstance The mock object instance. + * @return The mock object instance. + */ + public Object verify(Object mockInstance) + { + return verify(mockInstance, 1); + } + + /** + * Verify a method was called on a mock object. + * @param mockInstance The mock object instance. + * @param times The number of times you expect the method to have been called. + * @return The mock object instance. + */ + public Object verify(Object mockInstance, Integer times) + { + methodCountRecorder.Verifying = true; + methodCountRecorder.VerifyCount = times; + return mockInstance; + } + + /** + * Verfiy a method was called on a mock object. + * @param mockInstance The mock object instance. + * @param methodName The method you expect to have been called. + * @param methodArg The argument you expect to have been passed to the method being verified. + */ + public void verifyMethodCall(Object mockInstance, String methodName, Object methodArg) + { + methodCountRecorder.verifyMethodCall(mockInstance, methodName, methodArg); + } + + /** + * Tell ApexMocks framework you are about to start stubbing using when() calls. + */ + public void startStubbing() + { + methodReturnValueRecorder.Stubbing = true; + } + + /** + * Tell ApexMocks framework you are about to stop stubbing using when() calls. + */ + public void stopStubbing() + { + methodReturnValueRecorder.Stubbing = false; + } + + /** + * Setup when stubbing for a mock object instance. + * @param This is the return value from the method called on the mockInstance, and is ignored here since we are about to setup + * the stubbed return value using thenReturn() (see MethodReturnValue class below). + */ + public fflib_MethodReturnValue when(Object ignoredRetVal) + { + return methodReturnValueRecorder.MethodReturnValue; + } + + /** + * Record a method was called on a mock object. + * @param mockInstance The mock object instance. + * @param methodName The method to be recorded. + * @param methodArg The method argument to be recorded. + */ + public void recordMethod(Object mockInstance, String methodName, Object methodArg) + { + methodCountRecorder.recordMethod(mockInstance, methodName, methodArg); + } + + /** + * Prepare a stubbed method return value. + * @param mockInstance The mock object instance. + * @param methodName The method for which to prepare a return value. + * @param methodArg The method argument for which to prepare a return value. + */ + public fflib_MethodReturnValue prepareMethodReturnValue(Object mockInstance, String methodName, Object methodArg) + { + return methodReturnValueRecorder.prepareMethodReturnValue(mockInstance, methodName, methodArg); + } + + /** + * Get the method return value for the given method call. + * @param mockInstance The mock object instance. + * @param methodName The method for which to prepare a return value. + * @param methodArg The method argument for which to prepare a return value. + * @return The MethodReturnValue instance. + */ + public fflib_MethodReturnValue getMethodReturnValue(Object mockInstance, String methodName, Object methodArg) + { + return methodReturnValueRecorder.getMethodReturnValue(mockInstance, methodName, methodArg); + } + + /** + * Setup exception stubbing for a void method. + * @param e The exception to throw. + * @param mockInstance The mock object instance. + */ + public Object doThrowWhen(Exception e, Object mockInstance) + { + methodReturnValueRecorder.prepareDoThrowWhenException(e); + return mockInstance; + } + + /** + * Mock a void method. Called by generated mock instance classes, not directly by a developers + * code. + * @param mockInstance The mock object instance. + * @param methodName The method for which to prepare a return value. + * @param methodArg The method argument for which to prepare a return value. + */ + public void mockVoidMethod(Object mockInstance, String methodName, Object methodArg) + { + if (Verifying) + { + verifyMethodCall(mockInstance, methodName, methodArg); + } + else if (Stubbing) + { + prepareMethodReturnValue(mockInstance, methodName, methodArg).thenThrow(DoThrowWhenException); + } + else + { + fflib_MethodReturnValue methodReturnValue = getMethodReturnValue(mockInstance, methodName, methodArg); + + if (methodReturnValue != null && methodReturnValue.ReturnValue instanceof Exception) + { + throw ((Exception) methodReturnValue.ReturnValue); + } + + recordMethod(mockInstance, methodName, methodArg); + } + } + + /** + * Mock a non-void method. Called by generated mock instance classes, not directly by a developers + * code. + * @param mockInstance The mock object instance. + * @param methodName The method for which to prepare a return value. + * @param methodArg The method argument for which to prepare a return value. + */ + public Object mockNonVoidMethod(Object mockInstance, String methodName, Object methodArg) + { + if (Verifying) + { + verifyMethodCall(mockInstance, methodName, methodArg); + } + else if (Stubbing) + { + prepareMethodReturnValue(mockInstance, methodName, methodArg); + return null; + } + else + { + recordMethod(mockInstance, methodName, methodArg); + + fflib_MethodReturnValue methodReturnValue = getMethodReturnValue(mockInstance, methodName, methodArg); + + if (methodReturnValue != null) + { + if (methodReturnValue.ReturnValue instanceof Exception) + { + throw ((Exception) methodReturnValue.ReturnValue); + } + + return methodReturnValue.ReturnValue; + } + } + + return null; + } +} \ No newline at end of file diff --git a/rolluptool/src/classes/SObjectSelector.cls-meta.xml b/rolluptool/src/classes/fflib_ApexMocks.cls-meta.xml similarity index 80% rename from rolluptool/src/classes/SObjectSelector.cls-meta.xml rename to rolluptool/src/classes/fflib_ApexMocks.cls-meta.xml index 08d159fb..04433dae 100644 --- a/rolluptool/src/classes/SObjectSelector.cls-meta.xml +++ b/rolluptool/src/classes/fflib_ApexMocks.cls-meta.xml @@ -1,5 +1,5 @@ - 32.0 + 30.0 Active diff --git a/rolluptool/src/classes/fflib_ApexMocksTest.cls b/rolluptool/src/classes/fflib_ApexMocksTest.cls new file mode 100644 index 00000000..58f33efe --- /dev/null +++ b/rolluptool/src/classes/fflib_ApexMocksTest.cls @@ -0,0 +1,387 @@ +/** + * Copyright (c) 2014, FinancialForce.com, inc + * 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 FinancialForce.com, inc 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 fflib_ApexMocksTest +{ + @isTest + static void whenStubSingleCallWithSingleArgumentShouldReturnStubbedValue() + { + // Given + fflib_ApexMocks mocks = new fflib_ApexMocks(); + fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks); + + mocks.startStubbing(); + mocks.when(mockList.get(0)).thenReturn('bob'); + mocks.stopStubbing(); + + // When + String actualValue = mockList.get(0); + + // Then + System.assertEquals('bob', actualValue); + } + + @isTest + static void whenStubMultipleCallsWithSingleArgumentShouldReturnStubbedValues() + { + // Given + fflib_ApexMocks mocks = new fflib_ApexMocks(); + fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks); + + mocks.startStubbing(); + mocks.when(mockList.get(0)).thenReturn('bob'); + mocks.when(mockList.get(1)).thenReturn('fred'); + mocks.stopStubbing(); + + // When + String actualValueArg0 = mockList.get(0); + String actualValueArg1 = mockList.get(1); + String actualValueArg2 = mockList.get(2); + + // Then + System.assertEquals('bob', actualValueArg0); + System.assertEquals('fred', actualValueArg1); + System.assertEquals(null, actualValueArg2); + } + + @isTest + static void whenStubSameCallWithDifferentArgumentValueShouldReturnLastStubbedValue() + { + // Given + fflib_ApexMocks mocks = new fflib_ApexMocks(); + fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks); + + mocks.startStubbing(); + mocks.when(mockList.get(0)).thenReturn('bob1'); + mocks.when(mockList.get(0)).thenReturn('bob2'); + mocks.stopStubbing(); + + // When + String actualValue = mockList.get(0); + + // Then + System.assertEquals('bob2', actualValue); + } + + @isTest + static void whenStubCallWithNoArgumentsShouldReturnStubbedValue() + { + // Given + fflib_ApexMocks mocks = new fflib_ApexMocks(); + fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks); + + mocks.startStubbing(); + mocks.when(mockList.isEmpty()).thenReturn(false); + mocks.stopStubbing(); + + // When + Boolean actualValue = mockList.isEmpty(); + + // Then + System.assertEquals(false, actualValue); + } + + @isTest + static void verifySingleMethodCallWithNoArguments() + { + // Given + fflib_ApexMocks mocks = new fflib_ApexMocks(); + fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks); + + // When + mockList.isEmpty(); + + // Then + ((fflib_MyList.IList) mocks.verify(mockList)).isEmpty(); + } + + @isTest + static void verifySingleMethodCallWithSingleArgument() + { + // Given + fflib_ApexMocks mocks = new fflib_ApexMocks(); + fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks); + + // When + mockList.add('bob'); + + // Then + ((fflib_MyList.IList) mocks.verify(mockList)).add('bob'); + } + + @isTest + static void verifyMultipleMethodCallsWithSameSingleArgument() + { + // Given + fflib_ApexMocks mocks = new fflib_ApexMocks(); + fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks); + + // When + mockList.add('bob'); + mockList.add('bob'); + + // Then + ((fflib_MyList.IList) mocks.verify(mockList, 2)).add('bob'); + } + + @isTest + static void verifyMultipleMethodCallsWithDifferentSingleArgument() + { + // Given + fflib_ApexMocks mocks = new fflib_ApexMocks(); + fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks); + + // When + mockList.add('bob'); + mockList.add('fred'); + + // Then + ((fflib_MyList.IList) mocks.verify(mockList)).add('bob'); + ((fflib_MyList.IList) mocks.verify(mockList)).add('fred'); + } + + @isTest + static void verifyMethodNotCalled() + { + // Given + fflib_ApexMocks mocks = new fflib_ApexMocks(); + fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks); + + // When + mockList.get(0); + + // Then + ((fflib_MyList.IList) mocks.verify(mockList, fflib_ApexMocks.NEVER)).add('bob'); + ((fflib_MyList.IList) mocks.verify(mockList)).get(0); + } + + @isTest + static void stubAndVerifyMethodCallsWithNoArguments() + { + // Given + fflib_ApexMocks mocks = new fflib_ApexMocks(); + fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks); + + mocks.startStubbing(); + mocks.when(mockList.isEmpty()).thenReturn(false); + mocks.stopStubbing(); + + mockList.clear(); + + // When + Boolean actualValue = mockList.isEmpty(); + + // Then + System.assertEquals(false, actualValue); + ((fflib_MyList.IList) mocks.verify(mockList)).clear(); + } + + @isTest + static void whenStubExceptionTheExceptionShouldBeThrown() + { + // Given + fflib_ApexMocks mocks = new fflib_ApexMocks(); + fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks); + + mocks.startStubbing(); + mocks.when(mockList.get(0)).thenThrow(new MyException('Stubbed exception.')); + mocks.stopStubbing(); + + // When + try + { + mockList.get(0); + System.assert(false, 'Stubbed exception should have been thrown.'); + } + catch(Exception e) + { + // Then + System.assert(e instanceof MyException); + System.assertEquals('Stubbed exception.', e.getMessage()); + } + } + + @isTest + static void whenStubVoidMethodWithExceptionThenExceptionShouldBeThrown() + { + // Given + fflib_ApexMocks mocks = new fflib_ApexMocks(); + fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks); + + mocks.startStubbing(); + ((fflib_MyList.IList) mocks.doThrowWhen(new MyException('Stubbed exception.'), mockList)).clear(); + mocks.stopStubbing(); + + // When + try + { + mockList.clear(); + System.assert(false, 'Stubbed exception should have been thrown.'); + } + catch(Exception e) + { + // Then + System.assert(e instanceof MyException); + System.assertEquals('Stubbed exception.', e.getMessage()); + } + } + + @isTest + static void whenStubMultipleVoidMethodsWithExceptionsThenExceptionsShouldBeThrown() + { + // Given + fflib_ApexMocks mocks = new fflib_ApexMocks(); + fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks); + + mocks.startStubbing(); + ((fflib_MyList.IList) mocks.doThrowWhen(new MyException('clear stubbed exception.'), mockList)).clear(); + ((fflib_MyList.IList) mocks.doThrowWhen(new MyException('add stubbed exception.'), mockList)).add('bob'); + mocks.stopStubbing(); + + // When + try + { + mockList.clear(); + System.assert(false, 'Stubbed exception should have been thrown.'); + } + catch(Exception e) + { + // Then + System.assert(e instanceof MyException); + System.assertEquals('clear stubbed exception.', e.getMessage()); + } + + // When + try + { + mockList.add('bob'); + System.assert(false, 'Stubbed exception should have been thrown.'); + } + catch(Exception e) + { + // Then + System.assert(e instanceof MyException); + System.assertEquals('add stubbed exception.', e.getMessage()); + } + } + + @isTest + static void whenStubVoidMethodWithExceptionAndCallMethodTwiceThenExceptionShouldBeThrownTwice() + { + // Given + fflib_ApexMocks mocks = new fflib_ApexMocks(); + fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks); + + mocks.startStubbing(); + ((fflib_MyList.IList) mocks.doThrowWhen(new MyException('clear stubbed exception.'), mockList)).clear(); + mocks.stopStubbing(); + + // When + try + { + mockList.clear(); + System.assert(false, 'Stubbed exception should have been thrown.'); + } + catch(Exception e) + { + // Then + System.assert(e instanceof MyException); + System.assertEquals('clear stubbed exception.', e.getMessage()); + } + + // When + try + { + mockList.clear(); + System.assert(false, 'Stubbed exception should have been thrown.'); + } + catch(Exception e) + { + // Then + System.assert(e instanceof MyException); + System.assertEquals('clear stubbed exception.', e.getMessage()); + } + } + + @isTest + static void verifyMethodCallWhenNoCallsBeenMadeForType() + { + // Given + fflib_ApexMocks mocks = new fflib_ApexMocks(); + fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks); + + // Then + ((fflib_MyList.IList) mocks.verify(mockList, fflib_ApexMocks.NEVER)).add('bob'); + } + + @isTest + static void verifySingleMethodCallWithMultipleArguments() + { + // Given + fflib_ApexMocks mocks = new fflib_ApexMocks(); + fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks); + + // When + mockList.set(0, 'bob'); + + // Then + ((fflib_MyList.IList) mocks.verify(mockList)).set(0, 'bob'); + ((fflib_MyList.IList) mocks.verify(mockList, fflib_ApexMocks.NEVER)).set(0, 'fred'); + } + + @isTest + static void whenStubMultipleCallsWithMultipleArgumentShouldReturnStubbedValues() + { + // Given + fflib_ApexMocks mocks = new fflib_ApexMocks(); + fflib_MyList.IList mockList = new fflib_Mocks.Mockfflib_MyList(mocks); + + mocks.startStubbing(); + mocks.when(mockList.get2(0, 'zero')).thenReturn('bob'); + mocks.when(mockList.get2(1, 'one')).thenReturn('fred'); + mocks.when(mockList.get2(0, 'two')).thenReturn('bob'); + mocks.when(mockList.get2(1, 'three')).thenReturn('bub'); + mocks.stopStubbing(); + + // When + String actualValueArg0 = mockList.get2(0, 'zero'); + String actualValueArg1 = mockList.get2(1, 'one'); + String actualValueArg2 = mockList.get2(0, 'two'); + String actualValueArg3 = mockList.get2(1, 'three'); + String actualValueArg4 = mockList.get2(0, 'three'); + + // Then + System.assertEquals('bob', actualValueArg0); + System.assertEquals('fred', actualValueArg1); + System.assertEquals('bob', actualValueArg2); + System.assertEquals('bub', actualValueArg3); + System.assertEquals(null, actualValueArg4); + } + + private class MyException extends Exception + { + } +} \ No newline at end of file diff --git a/rolluptool/src/classes/SObjectDomain.cls-meta.xml b/rolluptool/src/classes/fflib_ApexMocksTest.cls-meta.xml similarity index 80% rename from rolluptool/src/classes/SObjectDomain.cls-meta.xml rename to rolluptool/src/classes/fflib_ApexMocksTest.cls-meta.xml index 08d159fb..04433dae 100644 --- a/rolluptool/src/classes/SObjectDomain.cls-meta.xml +++ b/rolluptool/src/classes/fflib_ApexMocksTest.cls-meta.xml @@ -1,5 +1,5 @@ - 32.0 + 30.0 Active diff --git a/rolluptool/src/classes/fflib_Application.cls b/rolluptool/src/classes/fflib_Application.cls new file mode 100644 index 00000000..701e25a3 --- /dev/null +++ b/rolluptool/src/classes/fflib_Application.cls @@ -0,0 +1,304 @@ +/** + * Copyright (c) 2014, FinancialForce.com, inc + * 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 FinancialForce.com, inc 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. +**/ + +/** + * Class provides inner classes implementing factories for the main components + * of the Apex Enterprise Patterns, Service, Unit Of Work, Selector and Domain. + * See the sample applications Application.cls file for an example + **/ +public class fflib_Application +{ + /** + * Class implements a Unit of Work factory + **/ + public class UnitOfWorkFactory + { + private List m_objectTypes; + private fflib_ISObjectUnitOfWork m_mockUow; + + /** + * Constructs a Unit Of Work factory + * + * @param objectTypes List of SObjectTypes in dependency order + **/ + public UnitOfWorkFactory(List objectTypes) + { + m_objectTypes = objectTypes.clone(); + } + + /** + * Returns a new fflib_SObjectUnitOfWork configured with the + * SObjectType list provided in the constructor, returns a Mock implementation + * if set via the setMock method + **/ + public fflib_ISObjectUnitOfWork newInstance() + { + // Mock? + if(m_mockUow!=null) + return m_mockUow; + return new fflib_SObjectUnitOfWork(m_objectTypes); + } + + @TestVisible + private void setMock(fflib_ISObjectUnitOfWork mockUow) + { + m_mockUow = mockUow; + } + } + + /** + * Simple Service Factory implementaiton + **/ + public class ServiceFactory + { + private Map m_serviceInterfaceTypeByServiceImplType; + + private Map m_serviceInterfaceTypeByMockService; + + /** + * Constructs a simple Service Factory, + * using a Map of Apex Interfaces to Apex Classes implementing the interface + * Note that this will not check the Apex Classes given actually implement the interfaces + * as this information is not presently available via the Apex runtime + * + * @param serviceInterfaceTypeByServiceImplType Map ofi interfaces to classes + **/ + public ServiceFactory(Map serviceInterfaceTypeByServiceImplType) + { + m_serviceInterfaceTypeByServiceImplType = serviceInterfaceTypeByServiceImplType; + m_serviceInterfaceTypeByMockService = new Map(); + } + + /** + * Returns a new instance of the Apex class associated with the given Apex interface + * Will return any mock implementation of the interface provided via setMock + * Note that this method will not check the configured Apex class actually implements the interface + * + * @param serviceInterfaceType Apex interface type + * @exception Is thrown if there is no registered Apex class for the interface type + **/ + public Object newInstance(Type serviceInterfaceType) + { + // Mock implementation? + if(m_serviceInterfaceTypeByMockService.containsKey(serviceInterfaceType)) + return m_serviceInterfaceTypeByMockService.get(serviceInterfaceType); + + // Create an instance of the type impleneting the given interface + Type serviceImpl = m_serviceInterfaceTypeByServiceImplType.get(serviceInterfaceType); + if(serviceImpl==null) + throw new DeveloperException('No implementation registered for service interface ' + serviceInterfaceType.getName()); + return serviceImpl.newInstance(); + } + + @TestVisible + private void setMock(Type serviceInterfaceType, Object serviceImpl) + { + m_serviceInterfaceTypeByMockService.put(serviceInterfaceType, serviceImpl); + } + } + + /** + * Class implements a Selector class factory + **/ + public class SelectorFactory + { + private Map m_sObjectBySelectorType; + private Map m_sObjectByMockSelector; + + /** + * Consturcts a Selector Factory linking SObjectType's with Apex Classes implement the fflib_ISObjectSelector interface + * Note that the factory does not chekc the given Apex Classes implement the interface + * currently this is not possible in Apex. + * + * @Param sObjectBySelectorType Map of SObjectType's to Selector Apex Classes + **/ + public SelectorFactory(Map sObjectBySelectorType) + { + m_sObjectBySelectorType = sObjectBySelectorType; + m_sObjectByMockSelector = new Map(); + } + + /** + * Creates a new instance of the associated Apex Class implementing fflib_ISObjectSelector + * for the given SObjectType, or if provided via setMock returns the Mock implementaton + * + * @param sObjectType An SObjectType token, e.g. Account.SObjectType + **/ + public fflib_ISObjectSelector newInstance(SObjectType sObjectType) + { + // Mock implementation? + if(m_sObjectByMockSelector.containsKey(sObjectType)) + return m_sObjectByMockSelector.get(sObjectType); + + // Determine Apex class for Selector class + Type selectorClass = m_sObjectBySelectorType.get(sObjectType); + if(selectorClass==null) + throw new DeveloperException('Selector class not found for SObjectType ' + sObjectType); + + // Construct Selector class and query by Id for the records + return (fflib_ISObjectSelector) selectorClass.newInstance(); + } + + /** + * Helper method to query the given SObject records + * Internally creates an instance of the registered Selector and calls its + * selectSObjectById method + * + * @param recordIds The SObject record Ids, must be all the same SObjectType + * @exception Is thrown if the record Ids are not all the same or the SObjectType is not registered + **/ + public List selectById(Set recordIds) + { + // No point creating an empty Domain class, nor can we determine the SObjectType anyway + if(recordIds==null || recordIds.size()==0) + throw new DeveloperException('Invalid record Id\'s set'); + + // Determine SObjectType + SObjectType domainSObjectType = new List(recordIds)[0].getSObjectType(); + for(Id recordId : recordIds) + if(recordId.getSobjectType()!=domainSObjectType) + throw new DeveloperException('Unable to determine SObjectType, Set contains Id\'s from different SObject types'); + + // Construct Selector class and query by Id for the records + return newInstance(domainSObjectType).selectSObjectsById(recordIds); + } + + /** + * Helper method to query related records to those provided, for example + * if passed a list of Opportunity records and the Account Id field will + * construct internally a list of Account Ids and call the registered + * Account selector to query the related Account records, e.g. + * + * List accounts = + * (List) Applicaiton.Selector.selectByRelationship(myOpps, Opportunity.AccountId); + * + * @param relatedRecords used to extract the related record Ids, e.g. Opportunty records + * @param relationshipField field in the passed records that contains the relationship records to query, e.g. Opportunity.AccountId + **/ + public List selectByRelationship(List relatedRecords, SObjectField relationshipField) + { + Set relatedIds = new Set(); + for(SObject relatedRecord : relatedRecords) + { + Id relatedId = (Id) relatedRecord.get(relationshipField); + if(relatedId!=null) + relatedIds.add(relatedId); + } + return selectById(relatedIds); + } + + @TestVisible + private void setMock(fflib_ISObjectSelector selectorInstance) + { + m_sObjectByMockSelector.put(selectorInstance.sObjectType(), selectorInstance); + } + } + + /** + * Class implements a Domain class factory + **/ + public class DomainFactory + { + private fflib_Application.SelectorFactory m_selectorFactory; + + private Map m_sObjectByDomainConstructorType; + + private Map m_sObjectByMockDomain; + + /** + * Consturcts a Domain factory, using an instance of the Selector Factory + * and a map of Apex classes implementing fflib_ISObjectDomain by SObjectType + * Note this will not check the Apex classes provided actually implement the interfaces + * since this is not possible in the Apex runtime at present + * + * @param selectorFactory, e.g. Application.Selector + * @param sObjectByDomainConstructorType Map of Apex classes by SObjectType + **/ + public DomainFactory(fflib_Application.SelectorFactory selectorFactory, + Map sObjectByDomainConstructorType) + { + m_selectorFactory = selectorFactory; + m_sObjectByDomainConstructorType = sObjectByDomainConstructorType; + m_sObjectByMockDomain = new Map(); + } + + /** + * Dynamically constructs an instance of a Domain class for the given record Ids + * Internally uses the Selector Factory to query the records before passing to a + * dynamically constructed instance of the application Apex Domain class + * + * @param recordIds A list of Id's of the same type + * @exception Throws an exception via the Selector Factory if the Ids are not all of the same SObjectType + **/ + public fflib_ISObjectDomain newInstance(Set recordIds) + { + return newInstance(m_selectorFactory.selectById(recordIds)); + + } + + /** + * Dynamically constructs an instace of the Domain class for the given records + * Will return a Mock implementation if one has been provided via setMock + * + * @param records A concreate list (e.g. List vs List) of records + * @exception Throws an exception if the SObjectType cannot be determined from the list + * or the constructor for Domain class was not registered for the SOBjectType + **/ + public fflib_ISObjectDomain newInstance(List records) + { + SObjectType domainSObjectType = records.getSObjectType(); + if(domainSObjectType==null) + throw new DeveloperException('Unable to determine SObjectType'); + + // Mock implementation? + if(m_sObjectByMockDomain.containsKey(domainSObjectType)) + return m_sObjectByMockDomain.get(domainSObjectType); + + // Determine SObjectType and Apex classes for Domain class + Type domainConstructorClass = m_sObjectByDomainConstructorType.get(domainSObjectType); + if(domainConstructorClass==null) + throw new DeveloperException('Domain constructor class not found for SObjectType ' + domainSObjectType); + + // Construct Domain class passing in the queried records + fflib_SObjectDomain.IConstructable domainConstructor = + (fflib_SObjectDomain.IConstructable) domainConstructorClass.newInstance(); + return (fflib_ISObjectDomain) domainConstructor.construct(records); + } + + @TestVisible + private void setMock(fflib_ISObjectDomain mockDomain) + { + m_sObjectByMockDomain.put(mockDomain.sObjectType(), mockDomain); + } + } + + public class ApplicationException extends Exception { } + + /** + * Exception representing a developer coding error, not intended for end user eyes + **/ + public class DeveloperException extends Exception { } +} \ No newline at end of file diff --git a/rolluptool/src/classes/SObjectSelectorTest.cls-meta.xml b/rolluptool/src/classes/fflib_Application.cls-meta.xml similarity index 80% rename from rolluptool/src/classes/SObjectSelectorTest.cls-meta.xml rename to rolluptool/src/classes/fflib_Application.cls-meta.xml index 08d159fb..b12420ea 100644 --- a/rolluptool/src/classes/SObjectSelectorTest.cls-meta.xml +++ b/rolluptool/src/classes/fflib_Application.cls-meta.xml @@ -1,5 +1,5 @@ - 32.0 + 31.0 Active diff --git a/rolluptool/src/classes/fflib_IDGenerator.cls b/rolluptool/src/classes/fflib_IDGenerator.cls new file mode 100644 index 00000000..48f99f75 --- /dev/null +++ b/rolluptool/src/classes/fflib_IDGenerator.cls @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2014, FinancialForce.com, inc + * 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 FinancialForce.com, inc 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 fflib_IDGenerator +{ + private static Integer fakeIdCount = 0; + private static final String ID_PATTERN = '000000000000'; + + /** + * Generate a fake Salesforce Id for the given SObjectType + */ + public static Id generate(Schema.SObjectType sobjectType) + { + String keyPrefix = sobjectType.getDescribe().getKeyPrefix(); + fakeIdCount++; + + String fakeIdPrefix = ID_PATTERN.substring(0, 12 - fakeIdCount.format().length()); + + return Id.valueOf(keyPrefix + fakeIdPrefix + fakeIdCount); + } +} \ No newline at end of file diff --git a/rolluptool/src/classes/SObjectDomainTest.cls-meta.xml b/rolluptool/src/classes/fflib_IDGenerator.cls-meta.xml similarity index 80% rename from rolluptool/src/classes/SObjectDomainTest.cls-meta.xml rename to rolluptool/src/classes/fflib_IDGenerator.cls-meta.xml index 08d159fb..04433dae 100644 --- a/rolluptool/src/classes/SObjectDomainTest.cls-meta.xml +++ b/rolluptool/src/classes/fflib_IDGenerator.cls-meta.xml @@ -1,5 +1,5 @@ - 32.0 + 30.0 Active diff --git a/rolluptool/src/classes/fflib_IDGeneratorTest.cls b/rolluptool/src/classes/fflib_IDGeneratorTest.cls new file mode 100644 index 00000000..e9c893ef --- /dev/null +++ b/rolluptool/src/classes/fflib_IDGeneratorTest.cls @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2014, FinancialForce.com, inc + * 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 FinancialForce.com, inc 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 fflib_IDGeneratorTest +{ + @isTest + static void itShouldGenerateValidIDs() + { + String id1 = fflib_IDGenerator.generate(Account.SObjectType); + String id2 = fflib_IDGenerator.generate(Account.SObjectType); + String id3 = fflib_IDGenerator.generate(Account.SObjectType); + String id4 = fflib_IDGenerator.generate(Account.SObjectType); + String id5 = fflib_IDGenerator.generate(Account.SObjectType); + String id6 = fflib_IDGenerator.generate(Account.SObjectType); + String id7 = fflib_IDGenerator.generate(Account.SObjectType); + String id8 = fflib_IDGenerator.generate(Account.SObjectType); + String id9 = fflib_IDGenerator.generate(Account.SObjectType); + String id10 = fflib_IDGenerator.generate(Account.SObjectType); + String id11 = fflib_IDGenerator.generate(Account.SObjectType); + + System.assertEquals('001000000000001AAA', id1); + System.assertEquals('001000000000002AAA', id2); + System.assertEquals('001000000000003AAA', id3); + System.assertEquals('001000000000004AAA', id4); + System.assertEquals('001000000000005AAA', id5); + System.assertEquals('001000000000006AAA', id6); + System.assertEquals('001000000000007AAA', id7); + System.assertEquals('001000000000008AAA', id8); + System.assertEquals('001000000000009AAA', id9); + System.assertEquals('001000000000010AAA', id10); + System.assertEquals('001000000000011AAA', id11); + } +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_IDGeneratorTest.cls-meta.xml b/rolluptool/src/classes/fflib_IDGeneratorTest.cls-meta.xml new file mode 100644 index 00000000..04433dae --- /dev/null +++ b/rolluptool/src/classes/fflib_IDGeneratorTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 30.0 + Active + diff --git a/rolluptool/src/classes/fflib_ISObjectDomain.cls b/rolluptool/src/classes/fflib_ISObjectDomain.cls new file mode 100644 index 00000000..71c0f1fe --- /dev/null +++ b/rolluptool/src/classes/fflib_ISObjectDomain.cls @@ -0,0 +1,23 @@ +/* +* FinancialForce.com, inc. claims copyright in this software, its screen +* display designs and supporting documentation. FinancialForce and +* FinancialForce.com are trademarks of FinancialForce.com, inc. Any +* unauthorized use, copying or sale of the above may constitute an +* infringement of copyright and may result in criminal or other legal +* proceedings. +* +* Copyright (c) 2013 FinancialForce.com, inc. All rights reserved. +*/ + +public interface fflib_ISObjectDomain +{ + /** + * Returns the SObjectType this Domain class represents + **/ + Schema.SObjectType sObjectType(); + + /** + * Alternative to the Records property, provided to support mocking of Domain classes + **/ + List getRecords(); +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_ISObjectDomain.cls-meta.xml b/rolluptool/src/classes/fflib_ISObjectDomain.cls-meta.xml new file mode 100644 index 00000000..b12420ea --- /dev/null +++ b/rolluptool/src/classes/fflib_ISObjectDomain.cls-meta.xml @@ -0,0 +1,5 @@ + + + 31.0 + Active + diff --git a/rolluptool/src/classes/fflib_ISObjectSelector.cls b/rolluptool/src/classes/fflib_ISObjectSelector.cls new file mode 100644 index 00000000..d4fee01a --- /dev/null +++ b/rolluptool/src/classes/fflib_ISObjectSelector.cls @@ -0,0 +1,23 @@ +/* +* FinancialForce.com, inc. claims copyright in this software, its screen +* display designs and supporting documentation. FinancialForce and +* FinancialForce.com are trademarks of FinancialForce.com, inc. Any +* unauthorized use, copying or sale of the above may constitute an +* infringement of copyright and may result in criminal or other legal +* proceedings. +* +* Copyright (c) 2013 FinancialForce.com, inc. All rights reserved. +*/ + +public interface fflib_ISObjectSelector +{ + /** + * Provides the SObjectType for the object the given Selector is providing query logic for + **/ + Schema.SObjectType sObjectType(); + + /** + * Selects by Id records using the fields defined by the Selector configuration + **/ + List selectSObjectsById(Set idSet); +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_ISObjectSelector.cls-meta.xml b/rolluptool/src/classes/fflib_ISObjectSelector.cls-meta.xml new file mode 100644 index 00000000..b12420ea --- /dev/null +++ b/rolluptool/src/classes/fflib_ISObjectSelector.cls-meta.xml @@ -0,0 +1,5 @@ + + + 31.0 + Active + diff --git a/rolluptool/src/classes/fflib_ISObjectUnitOfWork.cls b/rolluptool/src/classes/fflib_ISObjectUnitOfWork.cls new file mode 100644 index 00000000..484915a5 --- /dev/null +++ b/rolluptool/src/classes/fflib_ISObjectUnitOfWork.cls @@ -0,0 +1,75 @@ +/* +* FinancialForce.com, inc. claims copyright in this software, its screen +* display designs and supporting documentation. FinancialForce and +* FinancialForce.com are trademarks of FinancialForce.com, inc. Any +* unauthorized use, copying or sale of the above may constitute an +* infringement of copyright and may result in criminal or other legal +* proceedings. +* +* Copyright (c) 2013 FinancialForce.com, inc. All rights reserved. +*/ + +/** + * @see fflib_SObjectUnitOfWork + **/ +public interface fflib_ISObjectUnitOfWork +{ + /** + * Register a newly created SObject instance to be inserted when commitWork is called + * + * @param record A newly created SObject instance to be inserted during commitWork + **/ + void registerNew(SObject record); + /** + * Register a list of newly created SObject instances to be inserted when commitWork is called + * + * @param records A list of newly created SObject instances to be inserted during commitWork + **/ + void registerNew(List records); + /** + * Register a newly created SObject instance to be inserted when commitWork is called, + * you may also provide a reference to the parent record instance (should also be registered as new separatly) + * + * @param record A newly created SObject instance to be inserted during commitWork + * @param relatedToParentField A SObjectField reference to the child field that associates the child record with its parent + * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separatly) + **/ + void registerNew(SObject record, Schema.sObjectField relatedToParentField, SObject relatedToParentRecord); + /** + * Register a relationship between two records that have yet to be inserted to the database. This information will be + * used during the commitWork phase to make the references only when related records have been inserted to the database. + * + * @param record An existing or newly created record + * @param relatedToField A SObjectField referene to the lookup field that relates the two records together + * @param relatedTo A SOBject instance (yet to be commited to the database) + */ + void registerRelationship(SObject record, Schema.sObjectField relatedToField, SObject relatedTo); + /** + * Register an existing record to be updated during the commitWork method + * + * @param record An existing record + **/ + void registerDirty(SObject record); + /** + * Register a list of existing records to be updated during the commitWork method + * + * @param records A list of existing records + **/ + void registerDirty(List records); + /** + * Register an existing record to be deleted during the commitWork method + * + * @param record An existing record + **/ + void registerDeleted(SObject record); + /** + * Register a list of existing records to be deleted during the commitWork method + * + * @param records A list of existing records + **/ + void registerDeleted(List records); + /** + * Takes all the work that has been registered with the UnitOfWork and commits it to the database + **/ + void commitWork(); +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_ISObjectUnitOfWork.cls-meta.xml b/rolluptool/src/classes/fflib_ISObjectUnitOfWork.cls-meta.xml new file mode 100644 index 00000000..b12420ea --- /dev/null +++ b/rolluptool/src/classes/fflib_ISObjectUnitOfWork.cls-meta.xml @@ -0,0 +1,5 @@ + + + 31.0 + Active + diff --git a/rolluptool/src/classes/fflib_MethodCountRecorder.cls b/rolluptool/src/classes/fflib_MethodCountRecorder.cls new file mode 100644 index 00000000..1d3f6343 --- /dev/null +++ b/rolluptool/src/classes/fflib_MethodCountRecorder.cls @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2014, FinancialForce.com, inc + * 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 FinancialForce.com, inc 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 fflib_MethodCountRecorder +{ + public Boolean Verifying { get; set; } + public Integer VerifyCount { get; set; } + + /** + * Map of method counts by type name. + * + * Key: typeName + * Object: map of method calls by methodName. + * + * Key: methodName + * Object: map of count by method call argument. + */ + private Map>> methodCountsByTypeName; + + public fflib_MethodCountRecorder() + { + methodCountsByTypeName = new Map>>(); + } + + /** + * Verfiy a method was called on a mock object. + * @param mockInstance The mock object instance. + * @param methodName The method you expect to have been called. + * @param methodArg The argument you expect to have been passed to the method being verified. + */ + public void verifyMethodCall(Object mockInstance, String methodName, Object methodArg) + { + String typeName = fflib_ApexMocks.extractTypeName(mockInstance); + System.assertEquals(VerifyCount, getMethodCount(mockInstance, methodName, methodArg), 'Wanted but not invoked: ' + typeName + '.' + methodName + '.'); + Verifying = false; + } + + /** + * Record a method was called on a mock object. + * @param mockInstance The mock object instance. + * @param methodName The method to be recorded. + * @param methodArg The method argument to be recorded. + */ + public void recordMethod(Object mockInstance, String methodName, Object methodArg) + { + String typeName = fflib_ApexMocks.extractTypeName(mockInstance); + Map> methodCountsForType = methodCountsByTypeName.get(typeName); + + if (methodCountsForType == null) + { + recordInitialMethodCall(typeName, methodName, methodArg); + } + else + { + Map methodCountsForArg = methodCountsForType.get(methodName); + Integer count; + + if (methodCountsForArg == null) + { + count = null; + } + else + { + count = methodCountsForArg.get(methodArg); + } + + if (count == null) + { + if (methodCountsForType.get(methodName) == null) + { + methodCountsForType.put(methodName, new Map()); + } + + methodCountsByTypeName.get(typeName).get(methodName).put(methodArg, 1); + } + else + { + methodCountsForType.get(methodName).put(methodArg, count + 1); + } + } + } + + private void recordInitialMethodCall(String typeName, String methodName, Object methodArg) + { + methodCountsByTypeName.put(typeName, new Map>()); + methodCountsByTypeName.get(typeName).put(methodName, new Map()); + methodCountsByTypeName.get(typeName).get(methodName).put(methodArg, 1); + } + + private Integer getMethodCount(Object mockInstance, String methodName, Object methodArg) + { + String typeName = fflib_ApexMocks.extractTypeName(mockInstance); + Map> methodCountsForType = methodCountsByTypeName.get(typeName); + + if (methodCountsForType == null) + { + return 0; + } + else + { + Map methodCountsForArg = methodCountsForType.get(methodName); + + if (methodCountsForArg == null) + { + return 0; + } + + Integer methodCounts = methodCountsForArg.get(methodArg); + return methodCounts == null ? 0 : methodCounts; + } + } +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_MethodCountRecorder.cls-meta.xml b/rolluptool/src/classes/fflib_MethodCountRecorder.cls-meta.xml new file mode 100644 index 00000000..04433dae --- /dev/null +++ b/rolluptool/src/classes/fflib_MethodCountRecorder.cls-meta.xml @@ -0,0 +1,5 @@ + + + 30.0 + Active + diff --git a/rolluptool/src/classes/fflib_MethodReturnValue.cls b/rolluptool/src/classes/fflib_MethodReturnValue.cls new file mode 100644 index 00000000..e6350b71 --- /dev/null +++ b/rolluptool/src/classes/fflib_MethodReturnValue.cls @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2014, FinancialForce.com, inc + * 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 FinancialForce.com, inc 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 +public with sharing class fflib_MethodReturnValue +{ + /** + * Class defining a method return value. + */ + public Object ReturnValue { get; private set; } + + /** + * Setup a stubbed return value. + * @param value The value to return from the stubbed method call. + */ + public void thenReturn(Object value) + { + returnValue = value; + } + + /** + * Setup a stubbed exception. + * @param e The exception to throw from the stubbed method call. + */ + public void thenThrow(Exception e) + { + returnValue = e; + } +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_MethodReturnValue.cls-meta.xml b/rolluptool/src/classes/fflib_MethodReturnValue.cls-meta.xml new file mode 100644 index 00000000..04433dae --- /dev/null +++ b/rolluptool/src/classes/fflib_MethodReturnValue.cls-meta.xml @@ -0,0 +1,5 @@ + + + 30.0 + Active + diff --git a/rolluptool/src/classes/fflib_MethodReturnValueRecorder.cls b/rolluptool/src/classes/fflib_MethodReturnValueRecorder.cls new file mode 100644 index 00000000..5e54a774 --- /dev/null +++ b/rolluptool/src/classes/fflib_MethodReturnValueRecorder.cls @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2014, FinancialForce.com, inc + * 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 FinancialForce.com, inc 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 fflib_MethodReturnValueRecorder +{ + public Boolean Stubbing { get; set; } + + public Exception DoThrowWhenException { get; set; } + + /** + * Map of method return values by type name. + * + * Key: typeName + * Object: map of method return values by methodName. + * + * Key: methodName + * Object: map of MethodReturnVakue by method call argument. + */ + private Map>> methodReturnValuesByTypeName; + + public fflib_MethodReturnValue MethodReturnValue { get; private set; } + + public fflib_MethodReturnValueRecorder() + { + methodReturnValuesByTypeName = new Map>>(); + MethodReturnValue = null; + } + + /** + * Prepare a stubbed method return value. + * @param mockInstance The mock object instance. + * @param methodName The method for which to prepare a return value. + * @param methodArg The method argument for which to prepare a return value. + */ + public fflib_MethodReturnValue prepareMethodReturnValue(Object mockInstance, String methodName, Object methodArg) + { + String typeName = fflib_ApexMocks.extractTypeName(mockInstance); + + if (methodReturnValuesByTypeName.get(typeName) == null) + { + methodReturnValuesByTypeName.put(typeName, new Map>()); + } + + Map> methodReturnValues = methodReturnValuesByTypeName.get(typeName); + + MethodReturnValue = new fflib_MethodReturnValue(); + + if (methodReturnValues.get(methodName) == null) + { + methodReturnValues.put(methodName, new Map()); + } + + methodReturnValues.get(methodName).put(methodArg, MethodReturnValue); + + return MethodReturnValue; + } + + /** + * Get the method return value for the given method call. + * @param mockInstance The mock object instance. + * @param methodName The method for which to prepare a return value. + * @param methodArg The method argument for which to prepare a return value. + * @return The MethodReturnValue instance. + */ + public fflib_MethodReturnValue getMethodReturnValue(Object mockInstance, String methodName, Object methodArg) + { + String typeName = fflib_ApexMocks.extractTypeName(mockInstance); + + if (methodReturnValuesByTypeName.get(typeName) != null && methodReturnValuesByTypeName.get(typeName).get(methodName) != null) + { + return methodReturnValuesByTypeName.get(typeName).get(methodName).get(methodArg); + } + + return null; + } + + /** + * Prepare a stubbed exception for a void method. + * @param e The exception to throw. + */ + public void prepareDoThrowWhenException(Exception e) + { + DoThrowWhenException = e; + } +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_MethodReturnValueRecorder.cls-meta.xml b/rolluptool/src/classes/fflib_MethodReturnValueRecorder.cls-meta.xml new file mode 100644 index 00000000..04433dae --- /dev/null +++ b/rolluptool/src/classes/fflib_MethodReturnValueRecorder.cls-meta.xml @@ -0,0 +1,5 @@ + + + 30.0 + Active + diff --git a/rolluptool/src/classes/fflib_Mocks.cls b/rolluptool/src/classes/fflib_Mocks.cls new file mode 100644 index 00000000..a580bf7f --- /dev/null +++ b/rolluptool/src/classes/fflib_Mocks.cls @@ -0,0 +1,44 @@ +@isTest +public class fflib_Mocks +{ + + public class Mockfflib_MyList implements fflib_MyList.IList + { + private fflib_ApexMocks mocks; + + public Mockfflib_MyList(fflib_ApexMocks mocks) + { + this.mocks = mocks; + } + + public void add(String value) + { + mocks.mockVoidMethod(this, 'add', new List {value}); + } + + public String get(Integer index) + { + return (String) mocks.mockNonVoidMethod(this, 'get', new List {index}); + } + + public String get2(Integer index, String value) + { + return (String) mocks.mockNonVoidMethod(this, 'get2', new List {index, value}); + } + + public void clear() + { + mocks.mockVoidMethod(this, 'clear', new List {}); + } + + public Boolean isEmpty() + { + return (Boolean) mocks.mockNonVoidMethod(this, 'isEmpty', new List {}); + } + + public void set(Integer index, Object value) + { + mocks.mockVoidMethod(this, 'set', new List {index, value}); + } + } +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_Mocks.cls-meta.xml b/rolluptool/src/classes/fflib_Mocks.cls-meta.xml new file mode 100644 index 00000000..04433dae --- /dev/null +++ b/rolluptool/src/classes/fflib_Mocks.cls-meta.xml @@ -0,0 +1,5 @@ + + + 30.0 + Active + diff --git a/rolluptool/src/classes/fflib_MyList.cls b/rolluptool/src/classes/fflib_MyList.cls new file mode 100644 index 00000000..2b2f949d --- /dev/null +++ b/rolluptool/src/classes/fflib_MyList.cls @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2014, FinancialForce.com, inc + * 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 FinancialForce.com, inc 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 fflib_MyList implements IList +{ + public interface IList + { + void add(String value); + String get(Integer index); + String get2(Integer index, String value); // This is just a method signature to allow me to test stubbing a method with multiple arguments + void clear(); + Boolean isEmpty(); + void set(Integer index, Object value); + } + + public void add(String value) + { + } + + public String get(Integer index) + { + return 'fred'; + } + + public void clear() + { + } + + public Boolean isEmpty() + { + return true; + } + + public void set(Integer index, Object value) + { + } + + public String get2(Integer index, String value) + { + return 'mary'; + } +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_MyList.cls-meta.xml b/rolluptool/src/classes/fflib_MyList.cls-meta.xml new file mode 100644 index 00000000..04433dae --- /dev/null +++ b/rolluptool/src/classes/fflib_MyList.cls-meta.xml @@ -0,0 +1,5 @@ + + + 30.0 + Active + diff --git a/rolluptool/src/classes/fflib_QueryFactory.cls b/rolluptool/src/classes/fflib_QueryFactory.cls new file mode 100644 index 00000000..06381d4f --- /dev/null +++ b/rolluptool/src/classes/fflib_QueryFactory.cls @@ -0,0 +1,657 @@ +/** + * Copyright (c) 2014, FinancialForce.com, inc + * 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 FinancialForce.com, inc 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. +**/ + +/** + * QueryFactor provides an object-oriented way of building SOQL queries without resorting to string manipulation. + * This class is not meant to be used as a replacement for all SOQL queries, and due to the relativley high overhead in both CPU and describe calls + * should be used in places where highly dynamic queries, such as those that include field sets or are mutated heavilly + * in multiple locations are a good fit for use with fflib_QueryFactory. + * + * To use call construct a new instance for each query you intend to make. + * To add additional fields to the query make use of the selectField(s) methods. + * + * Currently the WHERE clause of the query is manipulated as a single string, and is decidedly less OO-styled than other methods. + * This is expected to be expanded upon in the future. + * + * To include one or more sort expression(s), use one of the addOrdering methods. If not specified, the "NULLS FIRST" keywords + * will be included by default. + * + * Subselect Queries are supported with the subselectQuery method. + * More than one sub-query can be added to a single query, but sub-queries can only be 1 level deep. + * An exception will thrown from the subselectQuery method when there is an attempt to add a subquery to a sub-query + * or to add a subquery to a query with an invalid relationship. + * + * Current limitations: + * - Aggregate functions are not currently supported. + * - Cross-object references currently require using String argument(s) to selectField(s). + * - The behavior of serializing and deserializing an fflib_QueryFactory instance is currently untested and undefined. + * + * There is a google doc providing additional guideance on the use of this class with field sets at + * https://docs.google.com/a/financialforce.com/document/d/1I4cxN4xHT4UJj_3Oi0YBL_MJ5chm-KG8kMN1D1un8-g/edit?usp=sharing +**/ +public class fflib_QueryFactory { //No explicit sharing declaration - inherit from caller + public enum SortOrder {ASCENDING, DESCENDING} + + /** + * This property is read-only and may not be set after instantiation. + * The {@link Schema.SObjectType} token of the SObject that will be used in the FROM clause of the resultant query. + **/ + public Schema.SObjectType table {get; private set;} + @testVisible + private Set fields; + private String conditionExpression; + private Integer limitCount; + private Integer offset; + private List order; + /** + * each item in sortExpressions contains the field and the direction (ascending or descending) + * use the addOrdering method to add fields to sort by. the sort fields + * appear in the SOQL query in the order they are added to the query. + **/ + /** + /* Integrate checking for READ Field Level Security within the selectField(s) methods + /* This can optionally be enforced (or not) by calling the setEnforceFLS method prior to calling + /* one of the selectField or selectFieldset methods. + **/ + private Boolean enforceFLS; + + /** + * The relationship and subselectQueryMap variables are used to support subselect queries. Subselects can be added to + * a query, as long as it isn't a subselect query itself. You may have many subselects inside + * a query, but they may only be 1 level deep (no subselect inside a subselect) + * to add a subselect, call the subselectQuery method, passing in the ChildRelationship. + **/ + private Schema.ChildRelationship relationship; + private Map subselectQueryMap; + + private QueryField getFieldToken(String fieldName){ + QueryField result; + if(!fieldName.contains('.')){ //single field + Schema.SObjectField token = fflib_SObjectDescribe.getDescribe(table).getField(fieldName.toLowerCase()); + if(token == null) + throw new InvalidFieldException(fieldName,this.table); + if (enforceFLS) + fflib_SecurityUtils.checkFieldIsReadable(this.table, token); + result = new QueryField(token); + }else{ //traversing FK relationship(s) + List fieldPath = new List(); + Schema.sObjectType lastSObjectType = table; + Iterator i = fieldName.split('\\.').iterator(); + while(i.hasNext()){ + String field = i.next(); + Schema.SObjectField token = fflib_SObjectDescribe.getDescribe(lastSObjectType).getField(field.toLowerCase()); + if (token != null && enforceFLS) + fflib_SecurityUtils.checkFieldIsReadable(lastSObjectType, token); + if(token != null && i.hasNext() && token.getDescribe().getSOAPType() == Schema.SOAPType.ID){ + lastSObjectType = token.getDescribe().getReferenceTo()[0]; //if it's polymorphic doesn't matter which one we get + fieldPath.add(token); + }else if(token != null && !i.hasNext()){ + fieldPath.add(token); + }else{ + if(token == null) + throw new InvalidFieldException(field,lastSObjectType); + else + throw new NonReferenceFieldException(lastSObjectType+'.'+field+' is not a lookup or master-detail field but is used in a cross-object query field.'); + } + } + result = new QueryField(fieldPath); + } + return result; + } + + /** + * fflib_QueryFactory instances will be considered equal if they produce the same SOQL query. + * A faster comparison will first be attempted to check if they apply to the same table, and contain the same number of fields selected. + * This method will never return true if the provided object is not an instance of fflib_QueryFactory. + * @param obj the object to check equality of. + **/ + public boolean equals(Object obj){ + if( !(obj instanceof fflib_QueryFactory) || ((fflib_QueryFactory)obj).table != this.table || ((fflib_QueryFactory)obj).fields.size() != this.fields.size() ) + return false; + return ((fflib_QueryFactory)obj).toSOQL() == this.toSOQL(); + } + + /** + * Construct a new fflib_QueryFactory instance with no options other than the FROM caluse. + * You *must* call selectField(s) before {@link #toSOQL} will return a valid, runnable query. + * @param table the SObject to be used in the FROM clause of the resultant query. This sets the value of {@link #table}. + **/ + public fflib_QueryFactory(Schema.SObjectType table){ + this.table = table; + fields = new Set(); + order = new List(); + enforceFLS = false; + } + + /** + * Construct a new fflib_QueryFactory instance with no options other than the FROM clause and the relationship. + * This should be used when constructing a subquery query for addition to a parent query. + * Objects created with this constructor cannot be added to another object using the subselectQuery method. + * You *must* call selectField(s) before {@link #toSOQL} will return a valid, runnable query. + * @param relationship the ChildRelationship to be used in the FROM Clause of the resultant Query (when set overrides value of table). This sets the value of {@link #relationship} and {@link #table}. + **/ + private fflib_QueryFactory(Schema.ChildRelationship relationship){ + this(relationship.getChildSObject()); + this.relationship = relationship; + } + + /** + * This method checks to see if the User has Read Access on {@link #table}. + * Asserts true if User has access. + **/ + public fflib_QueryFactory assertIsAccessible(){ + fflib_SecurityUtils.checkObjectIsReadable(table); + return this; + } + + /** + * This method sets a flag to indicate that this query should have FLS Read + * permission enforced. If this method is not called, the default behavior + * is that FLS read permission will not be checked. + * @param enforce whether to enforce field level security (read) + **/ + public fflib_QueryFactory setEnforceFLS(Boolean enforce){ + this.enforceFLS = enforce; + return this; + } + + /** + * Selects a single field from the SObject specified in {@link #table}. + * Selecting fields is idempotent, if this field is already selected calling this method will have no additional impact. + * @param fieldName the API name of the field to add to the query's SELECT clause. + **/ + public fflib_QueryFactory selectField(String fieldName){ + fields.add( getFieldToken(fieldName) ); + return this; + } + /** + * Selects a field, avoiding the possible ambiguitiy of String API names. + * @see #selectField(String) + * @param field the {@link Schema.SObjectField} to select with this query. + * @exception InvalidFieldException If the field is null {@code field}. + **/ + public fflib_QueryFactory selectField(Schema.SObjectField field){ + if(field == null) + throw new InvalidFieldException(null,this.table); + if (enforceFLS) + fflib_SecurityUtils.checkFieldIsReadable(table, field); + fields.add( new QueryField(field) ); + return this; + } + /** + * Selects multiple fields. This acts the same as calling {@link #selectField(String)} multiple times. + * @param fieldNames the Set of field API names to select. + **/ + public fflib_QueryFactory selectFields(Set fieldNames){ + List fieldList = new List(); + Set toAdd = new Set(); + for(String fieldName:fieldNames){ + toAdd.add( getFieldToken(fieldName) ); + } + fields.addAll(toAdd); + return this; + } + /** + * Selects multiple fields. This acts the same as calling {@link #selectField(String)} multiple times. + * @param fieldNames the List of field API names to select. + **/ + public fflib_QueryFactory selectFields(List fieldNames){ + Set toAdd = new Set(); + for(String fieldName:fieldNames) + toAdd.add( getFieldToken(fieldName) ); + fields.addAll(toAdd); + return this; + } + /** + * Selects multiple fields. This acts the same as calling {@link #selectField(Schema.SObjectField)} multiple times. + * @param fieldNames the set of {@link Schema.SObjectField}s to select. + * @exception InvalidFieldException if the fields are null {@code fields}. + **/ + public fflib_QueryFactory selectFields(Set fields){ + for(Schema.SObjectField token:fields){ + if(token == null) + throw new InvalidFieldException(); + if (enforceFLS) + fflib_SecurityUtils.checkFieldIsReadable(table, token); + this.fields.add( new QueryField(token) ); + } + return this; + } + /** + * Selects multiple fields. This acts the same as calling {@link #selectField(Schema.SObjectField)} multiple times. + * @param fieldNames the set of {@link Schema.SObjectField}s to select. + * @exception InvalidFieldException if the fields are null {@code fields}. + **/ + public fflib_QueryFactory selectFields(List fields){ + for(Schema.SObjectField token:fields){ + if(token == null) + throw new InvalidFieldException(); + if (enforceFLS) + fflib_SecurityUtils.checkFieldIsReadable(table, token); + this.fields.add( new QueryField(token) ); + } + return this; + } + /** + * @see #selectFieldSet(Schema.FieldSet,Boolean) + **/ + public fflib_QueryFactory selectFieldSet(Schema.FieldSet fieldSet){ + return selectFieldSet(fieldSet,true); + } + /** + * This is equivielent to iterating the fields in the field set and calling {@link #selectField(String)} on each. + * @param fieldSet Select all fields included in the field set. + * @param allowCrossObject if false this method will throw an exception if any fields in the field set reference fields on a related record. + * @exception InvalidFieldSetException if the fieldset is invalid for table {@code fields}. + **/ + public fflib_QueryFactory selectFieldSet(Schema.FieldSet fieldSet, Boolean allowCrossObject){ + if(fieldSet.getSObjectType() != table) + throw new InvalidFieldSetException('Field set "'+fieldSet.getName()+'" is not for SObject type "'+table+'"'); + for(Schema.FieldSetMember field: fieldSet.getFields()){ + if(!allowCrossObject && field.getFieldPath().contains('.')) + throw new InvalidFieldSetException('Cross-object fields not allowed and field "'+field.getFieldPath()+'"" is a cross-object field.'); + fields.add( getFieldToken(field.getFieldPath()) ); + } + return this; + } + /** + * @param conditionExpression Sets the WHERE clause to the string provided. Do not include the "WHERE". + **/ + public fflib_QueryFactory setCondition(String conditionExpression){ + this.conditionExpression = conditionExpression; + return this; + } + /** + * @returns the current value of the WHERE clause, if any, as set by {@link #setCondition} + **/ + public String getCondition(){ + return this.conditionExpression; + } + /** + * @param limitCount if not null causes a LIMIT caluse to be added to the resulting query. + **/ + public fflib_QueryFactory setLimit(Integer limitCount){ + this.limitCount = limitCount; + return this; + } + /** + * @returns the current value of the LIMIT clause, if any. + **/ + public Integer getLimit(){ + return this.limitCount; + } + /** + * @param o an instance of {@link fflib_QueryFactory.Ordering} to be added to the query's ORDER BY clause. + **/ + public fflib_QueryFactory addOrdering(Ordering o){ + this.order.add(o); + return this; + } + /** + * @returns the list of orderings that will be used as the query's ORDER BY clause. You may remove elements from the returned list, or otherwise mutate it, to remove previously added orderings. + **/ + public List getOrderings(){ + return this.order; + } + + /** + * @returns the selected fields + **/ + public Set getSelectedFields() { + return this.fields; + } + + /** + * Add a subquery query to this query. If a subquery for this relationship already exists, it will be returned. + * If not, a new one will be created and returned. + * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship + * @param related The related object type + **/ + public fflib_QueryFactory subselectQuery(SObjectType related){ + return setSubselectQuery(getChildRelationship(related), false); + } + + /** + * Add a subquery query to this query. If a subquery for this relationship already exists, it will be returned. + * If not, a new one will be created and returned. + * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship + * @param related The related object type + * @param assertIsAccessible indicates whether to check if the user has access to the subquery object + **/ + public fflib_QueryFactory subselectQuery(SObjectType related, Boolean assertIsAccessible){ + return setSubselectQuery(getChildRelationship(related), assertIsAccessible); + } + + /** + * Add a subquery query to this query. If a subquery for this relationship already exists, it will be returned. + * If not, a new one will be created and returned. + * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship + * @param relationship The ChildRelationship to be added as a subquery + **/ + private fflib_QueryFactory setSubselectQuery(ChildRelationship relationship, Boolean assertIsAccessible){ + if (this.relationship != null){ + throw new InvalidSubqueryRelationshipException('Invalid call to subselectQuery. You may not add a subselect query to a subselect query.'); + } + if (this.subselectQueryMap == null){ + this.subselectQueryMap = new Map(); + } + if (this.subselectQueryMap.containsKey(relationship)){ + return subselectQueryMap.get(relationship); + } + + fflib_QueryFactory subselectQuery = new fflib_QueryFactory(relationship); + subSelectQuery.assertIsAccessible(); + subselectQueryMap.put(relationship, subSelectQuery); + return subSelectQuery; + } + + /** + * @returns the list of subquery instances of fflib_QueryFactory which will be added to the SOQL as relationship/child/sub-queries. + **/ + public List getSubselectQueries(){ + if (subselectQueryMap != null) { + return subselectQueryMap.values(); + } + return null; + } + + /** + * Get the ChildRelationship from the Table for the object type passed in. + * @param objType The object type of the child relationship to get + **/ + private Schema.ChildRelationship getChildRelationship(sObjectType objType){ + for (Schema.ChildRelationship childRow : table.getDescribe().getChildRelationships()){ + //occasionally on some standard objects (Like Contact child of Contact) do not have a relationship name. + //if there is no relationship name, we cannot query on it, so throw an exception. + if (childRow.getChildSObject() == objType && childRow.getRelationshipName() != null){ + return childRow; + } + } + throw new InvalidSubqueryRelationshipException('Invalid call to subselectQuery. Invalid relationship for table '+table + ' and objtype='+objType); + } + + /** + * Add a field to be sorted on. This may be a direct field or a field + * related through an object lookup or master-detail relationship. + * Use the set to store unique field names, since we only want to sort + * by the same field one time. The sort expressions are stored in a list + * so that they are applied to the SOQL in the same order that they + * were added in. + * @param fieldName The string value of the field to be sorted on + * @param SortOrder the direction to be sorted on (ASCENDING or DESCENDING) + * @param nullsLast whether to sort null values last (NULLS LAST keyword included). + **/ + public fflib_QueryFactory addOrdering(String fieldName, SortOrder direction, Boolean nullsLast){ + order.add( + new Ordering(getFieldToken(fieldName), direction, nullsLast) + ); + return this; + } + + /** + * Add a field to be sorted on. This may be a direct field or a field + * related through an object lookup or master-detail relationship. + * Use the set to store unique field names, since we only want to sort + * by the same field one time. The sort expressions are stored in a list + * so that they are applied to the SOQL in the same order that they + * were added in. + * @param field The SObjectfield to sort. This can only be a direct reference. + * @param SortOrder the direction to be sorted on (ASCENDING or DESCENDING) + * @param nullsLast whether to sort null values last (NULLS LAST keyword included). + **/ + public fflib_QueryFactory addOrdering(SObjectField field, SortOrder direction, Boolean nullsLast){ + order.add( + new Ordering(new QueryField(field), direction, nullsLast) + ); + return this; + } + + /** + * Add a field to be sorted on. This may be a direct field or a field + * related through an object lookup or master-detail relationship. + * Use the set to store unique field names, since we only want to sort + * by the same field one time. The sort expressions are stored in a list + * so that they are applied to the SOQL in the same order that they + * were added in. + * The "NULLS FIRST" keywords will be included by default. If "NULLS LAST" + * is required, use one of the overloaded addOrdering methods which include this parameter. + * @param fieldName The string value of the field to be sorted on + * @param SortOrder the direction to be sorted on (ASCENDING or DESCENDING) + **/ + public fflib_QueryFactory addOrdering(String fieldName, SortOrder direction){ + order.add( + new Ordering(getFieldToken(fieldName), direction) + ); + return this; + } + + /** + * Add a field to be sorted on. This may be a direct field or a field + * related through an object lookup or master-detail relationship. + * Use the set to store unique field names, since we only want to sort + * by the same field one time. The sort expressions are stored in a list + * so that they are applied to the SOQL in the same order that they + * were added in. + * The "NULLS FIRST" keywords will be included by default. If "NULLS LAST" + * is required, use one of the overloaded addOrdering methods which include this parameter. + * @param field The SObjectfield to sort. This can only be a direct reference. + * @param SortOrder the direction to be sorted on (ASCENDING or DESCENDING) + **/ + public fflib_QueryFactory addOrdering(SObjectField field, SortOrder direction){ + order.add( + new Ordering(new QueryField(field), direction) + ); + return this; + } + + /** + * Convert the values provided to this instance into a full SOQL string for use with Database.query + * Check to see if subqueries queries need to be added after the field list. + **/ + public String toSOQL(){ + String result = 'SELECT '; + //if no fields have been added, just add the Id field so that the query or subquery will not just fail + if (fields.size() == 0){ + if (enforceFLS) fflib_SecurityUtils.checkFieldIsReadable(table, 'Id'); + result += 'Id '; + }else{ + List fieldsToQuery = new List(fields); + fieldsToQuery.sort(); //delegates to QueryFilter's comparable implementation + for(QueryField field:fieldsToQuery){ + result += field + ', '; + } + } + if(subselectQueryMap != null && !subselectQueryMap.isEmpty()){ + for (fflib_QueryFactory childRow : subselectQueryMap.values()){ + result += ' (' + childRow.toSOQL() + '), '; + } + } + result = result.substring(0,result.length()-2) + ' FROM ' + (relationship != null ? relationship.getRelationshipName() : table.getDescribe().getName()); + if(conditionExpression != null) + result += ' WHERE '+conditionExpression; + + if(order.size() > 0){ + result += ' ORDER BY '; + for(Ordering o:order) + result += o.toSOQL() +', '; + result = result.substring(0,result.length()-2); + } + + if(limitCount != null) + result += ' LIMIT '+limitCount; + return result; + } + + public class Ordering{ + private SortOrder direction; + private boolean nullsLast; + private QueryField field; + + public Ordering(String sobjType, String fieldName, SortOrder direction){ + this( + fflib_SObjectDescribe.getDescribe(sobjType).getField(fieldName), + direction + ); + } + /** + * Construct a new ordering instance for use with {@link fflib_QueryFactory#addOrdering} + * Once constructed it's properties may not be modified. + **/ + public Ordering(Schema.SObjectField field, SortOrder direction){ + this(field, direction, false); //SOQL docs state NULLS FIRST is default behavior + } + public Ordering(Schema.SObjectField field, SortOrder direction, Boolean nullsLast){ + this(new QueryField(field), direction, nullsLast); + } + @testVisible + private Ordering(QueryField field, SortOrder direction){ + this(field, direction, false); + } + @testVisible + private Ordering(QueryField field, SortOrder direction, Boolean nullsLast){ + this.direction = direction; + this.field = field; + this.nullsLast = nullsLast; + } + /** + * @deprecated + * Use of this method is discouraged. Only the first field of any cross-object fields is returned. + * Use getFields() instead. + **/ + public Schema.SObjectField getField(){ + System.debug(LoggingLevel.WARN, 'fflib_QueryFactory.Ordering.getField is deprecated and should not be used.'); + return field.getBaseField(); + } + public List getFields(){ + return this.field.getFieldPath(); + } + public SortOrder getDirection(){ + return direction; + } + public String toSOQL(){ + return field + ' ' + (direction == SortOrder.ASCENDING ? 'ASC' : 'DESC') + (nullsLast ? ' NULLS LAST ' : ' NULLS FIRST '); + } + } + + + public class QueryField implements Comparable{ + List fields; + + /** + * The first field in the path to to field being queried + **/ + public SObjectField getBaseField(){ + return fields[0]; + } + + /** + * The full list of fields representing the path to the field being queried + **/ + public List getFieldPath(){ + return fields.clone(); + } + + @testVisible + private QueryField(List fields){ + if(fields == null || fields.size() == 0) + throw new InvalidFieldException('Invalid field: null'); + this.fields = fields.clone(); //don't let clients mutate after setting! + } + @testVisible + private QueryField(Schema.SObjectField field){ + if(field == null) + throw new InvalidFieldException('Invalid field: null'); + fields = new List{ field }; + } + public override String toString(){ + String result = ''; + Iterator i = fields.iterator(); + while(i.hasNext()){ + String fieldName = i.next().getDescribe().getName(); + if(fieldName.endsWithIgnoreCase('Id') && i.hasNext()) + fieldName = fieldName.removeEndIgnoreCase('Id'); + if(fieldName.endsWithIgnoreCase('__c') && i.hasNext()) + fieldName = fieldName.removeEndIgnoreCase('__c')+'__r'; + result += fieldName + (i.hasNext() ? '.' :''); + } + return result; + } + public integer hashCode(){ + return String.valueOf(this.fields).hashCode(); + } + public boolean equals(Object obj){ + if(!(obj instanceof QueryField)) + return false; + if( String.valueOf(((QueryField) obj).fields) != String.valueOf(this.fields)) + return false; + Set objFields = new Set(); + objFields.addAll( ((QueryField)obj).fields ); + objFields.retainAll(this.fields); + objFields.removeAll(this.fields); + return objFields.size() == 0; + } + /** + * Allows sorting QueryField instances, which means we'll get deterministic field ordering by just sorting the parent + * QueryFactory's array when toSOQL'ing. + * + * Returns: + * - Objects that are not QueryField instances as -2, which functions as -1 but with more flair + * - QueryField instances with less joins in their path as -1 + * - QueryField instances with an equal number of joins and alphabetically first as an undefined negative integer + * - equals as 0 + * - anything else an undefined positive integer (usually, but not always 1) + **/ + public Integer compareTo(Object o){ + if(!(o instanceof QueryField)) + return -2; //We can't possibly do a sane comparison against an unknwon type, go athead and let it "win" + QueryField that = (QueryField) o; + if(this.fields.size() < that.fields.size()){ + return -1; + }else if( this.fields.size() == that.fields.size() ){ + if(this.equals(that)){ + return 0; + }else{ + return this.toString().compareTo(that.toString()); + } + }else{ + return 1; + } + } + } + + public class InvalidFieldException extends Exception{ + private String fieldName; + private Schema.SObjectType objectType; + public InvalidFieldException(String fieldname, Schema.SObjectType objectType){ + this.objectType = objectType; + this.fieldName = fieldName; + this.setMessage( 'Invalid field \''+fieldName+'\' for object \''+objectType+'\'' ); + } + } + public class InvalidFieldSetException extends Exception{} + public class NonReferenceFieldException extends Exception{} + public class InvalidSubqueryRelationshipException extends Exception{} +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_QueryFactory.cls-meta.xml b/rolluptool/src/classes/fflib_QueryFactory.cls-meta.xml new file mode 100644 index 00000000..b12420ea --- /dev/null +++ b/rolluptool/src/classes/fflib_QueryFactory.cls-meta.xml @@ -0,0 +1,5 @@ + + + 31.0 + Active + diff --git a/rolluptool/src/classes/fflib_QueryFactoryTest.cls b/rolluptool/src/classes/fflib_QueryFactoryTest.cls new file mode 100644 index 00000000..545eeba5 --- /dev/null +++ b/rolluptool/src/classes/fflib_QueryFactoryTest.cls @@ -0,0 +1,545 @@ +/** + * Copyright (c) 2014, FinancialForce.com, inc + * 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 FinancialForce.com, inc 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 fflib_QueryFactoryTest { + @isTest + static void simpleFieldSelection() { + fflib_QueryFactory qf = new fflib_QueryFactory(Contact.SObjectType); + qf.selectField('NAMe').selectFields( new Set{'naMe', 'email'}); + String query = qf.toSOQL(); + System.assert( Pattern.matches('SELECT.*Name.*FROM.*',query), 'Expected Name field in query, got '+query); + System.assert( Pattern.matches('SELECT.*Email.*FROM.*',query), 'Expected Name field in query, got '+query); + qf.setLimit(100); + System.assertEquals(100,qf.getLimit()); + System.assert( qf.toSOQL().endsWithIgnoreCase('LIMIT '+qf.getLimit()), 'Failed to respect limit clause:'+qf.toSOQL() ); + } + + @isTest + static void fieldSelections(){ + fflib_QueryFactory qf = new fflib_QueryFactory(Contact.SObjectType); + qf.selectField('firstName'); + qf.selectField(Schema.Contact.SObjectType.fields.lastName); + qf.selectFields( new Set{'acCounTId', 'account.name'} ); + qf.selectFields( new List{'homePhonE','fAX'} ); + qf.selectFields( new List{ Contact.Email, Contact.Title } ); + } + + @isTest + static void simpleFieldCondition(){ + String whereClause = 'name = \'test\''; + fflib_QueryFactory qf = new fflib_QueryFactory(Contact.SObjectType); + qf.selectField('name'); + qf.selectField('email'); + qf.setCondition( whereClause ); + System.assertEquals(whereClause,qf.getCondition()); + String query = qf.toSOQL(); + System.assert(query.endsWith('WHERE name = \'test\''),'Query should have ended with a filter on name, got: '+query); + } + + @isTest + static void duplicateFieldSelection() { + fflib_QueryFactory qf = new fflib_QueryFactory(Contact.SObjectType); + qf.selectField('NAMe').selectFields( new Set{'naMe', 'email'}); + String query = qf.toSOQL(); + System.assertEquals(1, query.countMatches('Name'), 'Expected one name field in query: '+query ); + } + + @isTest + static void equalityCheck(){ + fflib_QueryFactory qf1 = new fflib_QueryFactory(Contact.SObjectType); + fflib_QueryFactory qf2 = new fflib_QueryFactory(Contact.SObjectType); + System.assertEquals(qf1,qf2); + qf1.selectField('name'); + System.assertNotEquals(qf1,qf2); + qf2.selectField('NAmE'); + System.assertEquals(qf1,qf2); + qf1.selectField('name').selectFields( new Set{ 'NAME', 'name' }).selectFields( new Set{ Contact.Name, Contact.Name} ); + System.assertEquals(qf1,qf2); + } + + @isTest + static void nonReferenceField(){ + fflib_QueryFactory qf = new fflib_QueryFactory(Contact.SObjectType); + fflib_QueryFactory.NonReferenceFieldException e; + try{ + qf.selectField('name.title'); + }catch(fflib_QueryFactory.NonReferenceFieldException ex){ + e = ex; + } + System.assertNotEquals(null,e,'Cross-object notation on a non-reference field should throw NonReferenceFieldException.'); + } + + @isTest + static void invalidCrossObjectField(){ + fflib_QueryFactory qf = new fflib_QueryFactory(Contact.SObjectType); + fflib_QueryFactory.InvalidFieldException e; + try{ + qf.selectField('account.NOT_A_REAL_FIELD'); + }catch(fflib_QueryFactory.InvalidFieldException ex){ + e = ex; + } + System.assertNotEquals(null,e,'Cross-object notation on a non-reference field should throw NonReferenceFieldException.'); + } + + @isTest + static void invalidFieldTests(){ + List exceptions = new List(); + fflib_QueryFactory qf = new fflib_QueryFactory(Contact.SObjectType); + try{ + qf.selectField('Not_a_field'); + }catch(fflib_QueryFactory.InvalidFieldException e){ + exceptions.add(e); + } + try{ + qf.selectFields( new Set{ 'Not_a_field','alsoNotreal'}); + }catch(fflib_QueryFactory.InvalidFieldException e){ + exceptions.add(e); + } + try{ + qf.selectFields( new Set{ null }); + }catch(fflib_QueryFactory.InvalidFieldException e){ + exceptions.add(e); + } + try{ + qf.selectFields( new List{ null, Contact.title }); + }catch(fflib_QueryFactory.InvalidFieldException e){ + exceptions.add(e); + } + System.assertEquals(4,exceptions.size()); + } + + @isTest + static void ordering(){ + fflib_QueryFactory qf = new fflib_QueryFactory(Contact.SObjectType); + qf.selectField('name'); + qf.selectField('email'); + qf.setCondition( 'name = \'test\'' ); + qf.addOrdering( new fflib_QueryFactory.Ordering('Contact','name',fflib_QueryFactory.SortOrder.ASCENDING) ).addOrdering( new fflib_QueryFactory.Ordering('Contact','CreatedDATE',fflib_QueryFactory.SortOrder.DESCENDING) ); + String query = qf.toSOQL(); + + System.assertEquals(2,qf.getOrderings().size()); + System.assertEquals(Contact.name,qf.getOrderings()[0].getField() ); + System.assertEquals(fflib_QueryFactory.SortOrder.DESCENDING,qf.getOrderings()[1].getDirection() ); + + + System.assert( Pattern.matches('SELECT.*Name.*FROM.*',query), 'Expected Name field in query, got '+query); + System.assert( Pattern.matches('SELECT.*Email.*FROM.*',query), 'Expected Name field in query, got '+query); + } + + @isTest + static void invalidField_string(){ + fflib_QueryFactory qf = new fflib_QueryFactory(Contact.SObjectType); + qf.selectField('name'); + Exception e; + try{ + qf.selectField('not_a__field'); + }catch(fflib_QueryFactory.InvalidFieldException ex){ + e = ex; + } + System.assertNotEquals(null,e); + } + + @isTest + static void invalidFields_string(){ + fflib_QueryFactory qf = new fflib_QueryFactory(Contact.SObjectType); + qf.selectField('name'); + Exception e; + try{ + qf.selectFields( new List{'not_a__field'} ); + }catch(fflib_QueryFactory.InvalidFieldException ex){ + e = ex; + } + System.assertNotEquals(null,e); + } + + @isTest + static void invalidField_nullToken(){ + fflib_QueryFactory qf = new fflib_QueryFactory(Contact.SObjectType); + qf.selectField('name'); + Exception e; + Schema.SObjectField token = null; + try{ + qf.selectField( token ); + }catch(fflib_QueryFactory.InvalidFieldException ex){ + e = ex; + } + System.assertNotEquals(null,e); + } + + @isTest + static void invalidFields_nullToken(){ + fflib_QueryFactory qf = new fflib_QueryFactory(Contact.SObjectType); + qf.selectField('name'); + Exception e; + List token = new List{ + null + }; + try{ + qf.selectFields( token ); + }catch(fflib_QueryFactory.InvalidFieldException ex){ + e = ex; + } + System.assertNotEquals(null,e); + } + + @isTest + static void invalidFields_noQueryFields(){ + Exception e; + List sObjectFields = new List(); + try { + fflib_QueryFactory.QueryField qfld = new fflib_QueryFactory.QueryField(sObjectFields); + } catch (Exception ex) { + e = ex; + } + System.assertNotEquals(null,e); + } + + @isTest + static void invalidFields_noQueryField(){ + Exception e; + Schema.SObjectField sObjectField; + try { + fflib_QueryFactory.QueryField qfld = new fflib_QueryFactory.QueryField(sObjectField); + } catch (Exception ex) { + e = ex; + } + System.assertNotEquals(null,e); + } + + @isTest + static void invalidFields_queryFieldsNotEquals(){ + Exception e; + Schema.SObjectField sObjectField; + fflib_QueryFactory.QueryField qfld = new fflib_QueryFactory.QueryField(Contact.Name); + fflib_QueryFactory.QueryField qfld2 = new fflib_QueryFactory.QueryField(Contact.LastName); + System.assert(!qfld.equals(qfld2)); + } + + @isTest + static void queryIdFieldNotEquals(){ + //this is the equivalent of calling setField('account.name'), where table = Contact + fflib_QueryFactory.QueryField qfld = new fflib_QueryFactory.QueryField(new List{ + Schema.Contact.SObjectType.fields.AccountId, + Schema.Account.SObjectType.fields.name + }); + String fldString = qfld.toString(); + } + + @isTest + static void queryIdFieldNotEqualsWrongObjType(){ + fflib_QueryFactory.QueryField qfld = new fflib_QueryFactory.QueryField(new List{ + Schema.Contact.SObjectType.fields.AccountId}); + System.assert(!qfld.equals(new Contact())); + } + + @isTest + static void addChildQueries_success(){ + Account acct = new Account(); + acct.Name = 'testchildqueriesacct'; + insert acct; + Contact cont = new Contact(); + cont.FirstName = 'test'; + cont.LastName = 'test'; + cont.AccountId = acct.Id; + insert cont; + Task tsk = new Task(); + tsk.WhoId = cont.Id; + tsk.Subject = 'test'; + tsk.ActivityDate = System.today(); + insert tsk; + + fflib_QueryFactory qf = new fflib_QueryFactory(Contact.SObjectType); + qf.selectField('name').selectField('Id').setCondition( 'name like \'%test%\'' ).addOrdering('CreatedDate',fflib_QueryFactory.SortOrder.DESCENDING, true); + Schema.DescribeSObjectResult descResult = Contact.SObjectType.getDescribe(); + //explicitly assert object accessibility when creating the subselect + qf.subselectQuery(Task.SObjectType, true).selectField('Id').selectField('Subject').setCondition(' IsDeleted = false '); + List queries = qf.getSubselectQueries(); + System.assert(queries != null); + List contacts = Database.query(qf.toSOQL()); + System.assert(contacts != null && contacts.size() == 1); + System.assert(contacts[0].Tasks.size() == 1); + System.assert(contacts[0].Tasks[0].Subject == 'test'); + } + + @isTest + static void addChildQuerySameRelationshipAgain_success(){ + Account acct = new Account(); + acct.Name = 'testchildqueriesacct'; + insert acct; + Contact cont = new Contact(); + cont.FirstName = 'test'; + cont.LastName = 'test'; + cont.AccountId = acct.Id; + insert cont; + Task tsk = new Task(); + tsk.WhoId = cont.Id; + tsk.Subject = 'test'; + tsk.ActivityDate = System.today(); + insert tsk; + fflib_QueryFactory qf = new fflib_QueryFactory(Contact.SObjectType); + qf.selectField('name'); + qf.selectField('Id'); + qf.setCondition( 'name like \'%test%\'' ); + qf.addOrdering( new fflib_QueryFactory.Ordering('Contact','name',fflib_QueryFactory.SortOrder.ASCENDING) ).addOrdering('CreatedBy.Name',fflib_QueryFactory.SortOrder.DESCENDING); + Schema.DescribeSObjectResult descResult = Contact.SObjectType.getDescribe(); + ChildRelationship relationship; + for (Schema.ChildRelationship childRow : descResult.getChildRelationships()) { + if (childRow.getRelationshipName() == 'Tasks') { + relationship = childRow; + } + } + System.assert(qf.getSubselectQueries() == null); + fflib_QueryFactory childQf = qf.subselectQuery(Task.SObjectType); + childQf.assertIsAccessible(); + childQf.setEnforceFLS(true); + childQf.selectField('Id'); + fflib_QueryFactory childQf2 = qf.subselectQuery(Task.SObjectType); + List queries = qf.getSubselectQueries(); + System.assert(queries != null); + System.assert(queries.size() == 1); + } + + @isTest + static void addChildQueries_invalidChildRelationship(){ + Account acct = new Account(); + acct.Name = 'testchildqueriesacct'; + insert acct; + Contact cont = new Contact(); + cont.FirstName = 'test'; + cont.LastName = 'test'; + cont.AccountId = acct.Id; + insert cont; + fflib_QueryFactory qf = new fflib_QueryFactory(Contact.SObjectType); + qf.selectField('name'); + qf.selectField('email'); + qf.setCondition( 'name like \'%test%\'' ); + qf.addOrdering( new fflib_QueryFactory.Ordering('Contact','name',fflib_QueryFactory.SortOrder.ASCENDING) ).addOrdering( 'CreatedDATE',fflib_QueryFactory.SortOrder.DESCENDING); + Schema.DescribeSObjectResult descResult = Account.SObjectType.getDescribe(); + Exception e; + try { + fflib_QueryFactory childQf = qf.subselectQuery(Contact.SObjectType); + childQf.selectField('Id'); + } catch (fflib_QueryFactory.InvalidSubqueryRelationshipException ex) { + e = ex; + } + System.assertNotEquals(e, null); + } + + @isTest + static void addChildQueries_invalidChildRelationshipTooDeep(){ + Account acct = new Account(); + acct.Name = 'testchildqueriesacct'; + insert acct; + Contact cont = new Contact(); + cont.FirstName = 'test'; + cont.LastName = 'test'; + cont.AccountId = acct.Id; + insert cont; + fflib_QueryFactory qf = new fflib_QueryFactory(Contact.SObjectType); + qf.selectField('name'); + qf.selectField('email'); + qf.setCondition( 'name like \'%test%\'' ); + qf.addOrdering( new fflib_QueryFactory.Ordering('Contact','name',fflib_QueryFactory.SortOrder.ASCENDING) ).addOrdering('CreatedDATE',fflib_QueryFactory.SortOrder.DESCENDING); + Schema.DescribeSObjectResult descResult = Contact.SObjectType.getDescribe(); + + fflib_QueryFactory childQf = qf.subselectQuery(Task.SObjectType); + childQf.selectField('Id'); + childQf.selectField('Subject'); + Exception e; + try { + fflib_QueryFactory subChildQf = childQf.subselectQuery(Task.SObjectType); + } catch (fflib_QueryFactory.InvalidSubqueryRelationshipException ex) { + e = ex; + } + System.assertNotEquals(e, null); + } + + @isTest + static void checkFieldObjectReadSort_success(){ + fflib_QueryFactory qf = new fflib_QueryFactory(Contact.SObjectType); + qf.assertIsAccessible() + .setEnforceFLS(true) + .selectField('createdby.name') + .selectField(Contact.LastModifiedById) + .selectFields(new List{Contact.LastModifiedDate}) + .setEnforceFLS(false) + .selectField(Contact.LastName) + .selectFields(new List{Contact.Id}) + .setCondition( 'name like \'%test%\'' ) + .setEnforceFLS(true) + .selectFields(new Set{Contact.FirstName}) + .addOrdering(new fflib_QueryFactory.Ordering('Contact','name',fflib_QueryFactory.SortOrder.ASCENDING) ) + .addOrdering(Contact.LastModifiedDate,fflib_QueryFactory.SortOrder.DESCENDING) + .addOrdering(Contact.CreatedDate,fflib_QueryFactory.SortOrder.DESCENDING, true); + Set fields = qf.getSelectedFields(); + fflib_QueryFactory.Ordering ordering = new fflib_QueryFactory.Ordering('Contact','name',fflib_QueryFactory.SortOrder.ASCENDING); + ordering.getFields(); + for (fflib_QueryFactory.QueryField qfRow : fields) { + SObjectField fld = qfRow.getBaseField(); + List flds = qfRow.getFieldPath(); + break; + } + System.assert(qf.toSOQL().containsIgnoreCase('NULLS LAST')); + } + + @isTest + static void checkObjectRead_fail(){ + User usr = createTestUser_noAccess(); + if (usr != null){ + System.runAs(usr){ + //create a query factory object for Account. + fflib_QueryFactory qf = new fflib_QueryFactory(Account.SObjectType); + Boolean excThrown = false; + try { + //check to see if this record is accessible, it isn't. + qf.assertIsAccessible(); + } catch (fflib_SecurityUtils.CrudException e) { + excThrown = true; + } + System.assert(excThrown); + } + } + } + + @isTest + static void checkFieldRead_fail(){ + User usr = createTestUser_noAccess(); + if (usr != null){ + System.runAs(usr){ + //create a query factory object for Account. + fflib_QueryFactory qf = new fflib_QueryFactory(Account.SObjectType); + Boolean excThrown = false; + try { + //set field to enforce FLS, then try to add a field. + qf.setEnforceFLS(true); + qf.selectField('Name'); + } catch (fflib_SecurityUtils.FlsException e) { + excThrown = true; + } + System.assert(excThrown); + } + } + } + + @isTest + static void queryWith_noFields(){ + fflib_QueryFactory qf = new fflib_QueryFactory(Contact.SObjectType); + qf.assertIsAccessible().setEnforceFLS(true).setCondition( 'name like \'%test%\'' ).addOrdering('CreatedDate',fflib_QueryFactory.SortOrder.DESCENDING); + String query = qf.toSOQL(); + System.assert(query.containsIgnoreCase('Id FROM')); + } + + @isTest + static void queryField_compareTo(){ + String otherType = 'bob'; + fflib_QueryFactory.QueryField qf = new fflib_QueryFactory.QueryField(Contact.SObjectType.fields.Name); + fflib_QueryFactory.QueryField joinQf = new fflib_QueryFactory.QueryField(new List{ + Contact.SObjectType.fields.LastModifiedById, + Account.SObjectType.fields.OwnerId, + User.SObjectType.fields.Name + }); + fflib_QueryFactory.QueryField otherJoinQf = new fflib_QueryFactory.QueryField(new List{ + Contact.SObjectType.fields.AccountId, + Account.SObjectType.fields.CreatedById, + User.SObjectType.fields.Name + }); + System.assertEquals(-2, qf.compareTo(otherType)); + System.assertEquals(0, qf.compareTo(qf)); + System.assertEquals( + 0, + qf.compareTo(new fflib_QueryFactory.QueryField(Contact.SObjectType.fields.Name)), + 'An equal but non-identical instance should return 0' + ); + System.assertEquals(-1 , qf.compareTo(joinQf)); + System.assertEquals(1, joinQf.compareTo(qf)); + System.assert(joinQf.compareTo(otherJoinQf) > 0); + System.assert(otherJoinQf.compareTo(joinQf) < 0); + } + + @isTest + static void deterministic_toSOQL(){ + fflib_QueryFactory qf1 = new fflib_QueryFactory(User.SObjectType); + fflib_QueryFactory qf2 = new fflib_QueryFactory(User.SObjectType); + for(fflib_QueryFactory qf:new Set{qf1,qf2}){ + qf.selectFields(new List{ + 'Id', + 'FirstName', + 'LastName', + 'CreatedBy.Name', + 'CreatedBy.Manager', + 'LastModifiedBy.Email' + }); + } + String expectedQuery = + 'SELECT ' + +'FirstName, Id, LastName, ' //less joins come first, alphabetically + +'CreatedBy.ManagerId, CreatedBy.Name, LastModifiedBy.Email ' //alphabetical on the same number of joinrs' + +'FROM User'; + System.assertEquals(qf1.toSOQL(), qf2.toSOQL()); + System.assertEquals(expectedQuery, qf1.toSOQL()); + System.assertEquals(expectedQuery, qf2.toSOQL()); + } + + public static User createTestUser_noAccess(){ + User usr; + try { + //look for a profile that does not have access to the Account object + PermissionSet ps = + [SELECT Profile.Id, profile.name + FROM PermissionSet + WHERE IsOwnedByProfile = true + AND Profile.UserType = 'Standard' + AND Id NOT IN (SELECT ParentId + FROM ObjectPermissions + WHERE SObjectType = 'Account' + AND PermissionsRead = true) + LIMIT 1]; + + if (ps != null){ + //create a user with the profile found that doesn't have access to the Account object + usr = new User( + firstName = 'testUsrF', + LastName = 'testUsrL', + Alias = 'tstUsr', + Email = 'testy.test@test.com', + UserName='test'+ Math.random().format()+'user99@test.com', + EmailEncodingKey = 'ISO-8859-1', + LanguageLocaleKey = 'en_US', + TimeZoneSidKey = 'America/Los_Angeles', + LocaleSidKey = 'en_US', + ProfileId = ps.Profile.Id, + IsActive=true + ); + insert usr; + } + } catch (Exception e) { + //do nothing, just return null User because this test case won't work in this org. + return null; + } + return usr; + } +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_QueryFactoryTest.cls-meta.xml b/rolluptool/src/classes/fflib_QueryFactoryTest.cls-meta.xml new file mode 100644 index 00000000..b12420ea --- /dev/null +++ b/rolluptool/src/classes/fflib_QueryFactoryTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 31.0 + Active + diff --git a/rolluptool/src/classes/fflib_SObjectDescribe.cls b/rolluptool/src/classes/fflib_SObjectDescribe.cls new file mode 100644 index 00000000..8d4774d3 --- /dev/null +++ b/rolluptool/src/classes/fflib_SObjectDescribe.cls @@ -0,0 +1,330 @@ +/** + * Copyright (c) 2014, FinancialForce.com, inc + * 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 FinancialForce.com, inc 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. +**/ + +/** + * fflib_SObjectDescribe is a semi-intelligent wrapper for standard apex Schema methods. + * It provides an internal caching layer, to avoid hitting describe limits from repeated use, + * as well as wrapper classes and methods to make common tasks like working with relationship field name oddities + * as well namespace handling. + * + * Of particular note for use in contexts that may be released as managed packages are the #getFields and get #getGlobalDescribe methods + * These return special immutable wrapper objects that automatically imply the current namespace (detected as the one this class is contained in) + * and allow an older API style of omitting the namespace when working with fields or global describe maps. + * This allows both upgrading old code to APIv29 by making use of these as a nearly drop in replacement, as well as keeping + * namespace detection logic encapsulated. +**/ +public class fflib_SObjectDescribe { + //internal implementation details + private Schema.SObjectType token; + private Schema.SObjectField nameField; + private Schema.DescribeSObjectResult describe { //lazy load - keep this leightweight until we need more data + get{ + if(describe == null) + describe = token.getDescribe(); + return describe; + } + set; + } + private Map fields { + get{ + if(fields == null) + fields = describe.fields.getMap(); + return fields; + } + set; + } + private Map fieldSets { + get{ + if(fieldSets == null) + fieldSets = describe.fieldSets.getMap(); + return fieldSets; + } + set; + } + private FieldsMap wrappedFields { + get{ + if(wrappedFields == null){ + wrappedFields = new FieldsMap(this.fields); + } + return wrappedFields; + } + set; + } + + private fflib_SObjectDescribe(Schema.SObjectType token){ + if(token == null) + throw new InvalidDescribeException('Invalid SObject type: null'); + if(instanceCache.containsKey( String.valueOf(token) )) + throw new DuplicateDescribeException(token + ' is already in the describe cache'); + this.token = token; + instanceCache.put( String.valueOf(token).toLowerCase() , this); + } + + //public instace methods + /** + * Returns the Schema.SObjectType this fflib_SObjectDescribe instance is based on. + **/ + public Schema.SObjectType getSObjectType(){ + return token; + } + /** + * This method is a convenient shorthand for calling getField(name, true) + **/ + public Schema.SObjectField getField(String name){ + return this.getField(name, true); + } + /** + * This method provides a simplified shorthand for calling #getFields and getting the provided field. + * Additionally it handles finding the correct SObjectField for relationship notation, + * e.g. getting the Account field on Contact would fail without being referenced as AccountId - both work here. + **/ + public Schema.SObjectField getField(String fieldName, boolean implyNamespace){ + Schema.SObjectField result = wrappedFields.get( + (fieldName.endsWithIgnoreCase('__r') ? //resolve custom field cross-object (__r) syntax + (fieldName.removeEndIgnoreCase('__r')+'__c') : + fieldName), + implyNamespace + ); + if(result == null){ + result = wrappedFields.get(fieldName+'Id', implyNamespace); //in case it's a standard lookup in cross-object format + } + return result; + } + + /** + * Returns the field where isNameField() is true (if any); otherwise returns null + **/ + public Schema.SObjectField getNameField() + { + if(nameField == null) { + for(Schema.SObjectField field : wrappedFields.values()) { + if(field.getDescribe().isNameField()) { + nameField = field; + break; + } + } + } + return nameField; + } + + /** + * Returns the raw Schema.DescribeSObjectResult an fflib_SObjectDescribe instance wraps. + **/ + public Schema.DescribeSObjectResult getDescribe(){ + return describe; + } + /** + * This method returns the raw data and provides no namespace handling. + * Due to this, __use of this method is discouraged__ in favor of getFields(). + **/ + public Map getFieldsMap(){ + return fields; + } + public FieldsMap getFields(){ + return wrappedFields; + } + public Map getFieldSetsMap(){ + return fieldSets; + } + + + + private static Map rawGlobalDescribe { + get{ + if(rawGlobalDescribe == null) + rawGlobalDescribe = Schema.getGlobalDescribe(); + return rawGlobalDescribe; + } + set; + } + private static GlobalDescribeMap wrappedGlobalDescribe{ + get{ + if(wrappedGlobalDescribe == null){ + wrappedGlobalDescribe = new GlobalDescribeMap(rawGlobalDescribe); + } + return wrappedGlobalDescribe; + } + set; + } + /** + * This is used to cache fflib_SObjectDescribe instances as they're consutrcted + * to prevent repeatedly re-constructing the same type. + * These instances are not guaranteed to be, but typically will be, unique per sObject type due to the presence of flushCache. + **/ + private static Map instanceCache {get{ + if(instanceCache == null) + instanceCache = new Map(); + return instanceCache; + } + set; + } + public static fflib_SObjectDescribe getDescribe(String sObjectName){ + fflib_SObjectDescribe result = instanceCache.get(sObjectName.toLowerCase()); + if(result == null){ + Schema.SObjectType token = wrappedGlobalDescribe.get(sObjectName.toLowerCase()); + if(token == null) + result = null; + else + result = new fflib_SObjectDescribe(token); + } + return result; + } + public static fflib_SObjectDescribe getDescribe(Schema.SObjectType token){ + fflib_SObjectDescribe result = instanceCache.get(String.valueOf(token).toLowerCase()); + if(result == null) + result = new fflib_SObjectDescribe(token); + return result; + } + public static fflib_SObjectDescribe getDescribe(Schema.DescribeSObjectResult nativeDescribe){ + fflib_SObjectDescribe result = instanceCache.get(nativeDescribe.getName().toLowerCase()); + if(result == null) + result = new fflib_SObjectDescribe(nativeDescribe.getSobjectType()); + return result; + } + public static fflib_SObjectDescribe getDescribe(SObject instance){ + return getDescribe(instance.getSobjectType()); + } + + //returns the same results as the native method, just with caching built in to avoid limits + public static Map getRawGlobalDescribe(){ + return rawGlobalDescribe; + } + public static GlobalDescribeMap getGlobalDescribe(){ + return wrappedGlobalDescribe; + } + //Useful when working in heap space constrained environments. + //Existing references to SObjectDescribe instances will continue to work. + public static void flushCache(){ + rawGlobalDescribe = null; + instanceCache = null; + } + + + /** + * This class handles emulating a Map's non-mutating instance methods and helps navigate the complex topic of + * handling implicit namespace behavior like pre-APIv29 did, while also allowing fully qualified references. + * Note that this requires the API version of fflib_SObjectDescribe to be 29 or higher to function properly. + * + * Due to the lack of language support for covariant return types sublasses are responsible for implementing the get methods. + * A minimal implementation of these would be a cast and returning getObject's result. + **/ + private abstract class NamespacedAttributeMap{ + @testVisible + protected String currentNamespace; + protected Map values; + + protected NamespacedAttributeMap(Map values){ + //namespace detection courtesey http://salesforce.stackexchange.com/a/28977/60 + currentNamespace = fflib_SObjectDescribe.class.getName().substringBefore('fflib_SObjectDescribe').removeEnd('.').toLowerCase(); + this.values = values; + } + //A no-args constructor to allow subclasses with different contructor signatures + protected NamespacedAttributeMap(){ + this(new Map()); + } + /** + * A convenient shortcut for invoking #getObject(name, true) + **/ + protected virtual Object getObject(String name){ + return this.getObject(name, true); + } + /** + * + **/ + protected virtual Object getObject(String name, Boolean implyNamespace){ + String preferredValue = ((implyNamespace ? currentNamespace+'__' : '') + name).toLowerCase(); + if(values.containsKey(preferredValue)){ + return values.get(preferredValue); + }else if(implyNamespace){ + return values.get(name); + }else{ + return null; + } + } + public virtual Boolean containsKey(String name){ + return this.containsKey(name, true); + } + public virtual Boolean containsKey(String name, Boolean implyNamespace){ + String preferredValue = ((implyNamespace ? currentNamespace+'__' : '') + name).toLowerCase(); + return ( + values.containsKey(preferredValue) || + implyNamespace && values.containsKey(name) + ); + } + public virtual Integer size(){ + return values.size(); + } + public virtual Set keySet(){ + return values.keySet(); + } + } + + /** + * A subclass of NamespacedAttributeMap for handling the data returned by #Schema.DescribeSObjectResult.fields.getMap + **/ + public class FieldsMap extends NamespacedAttributeMap{ + + @testVisible + private FieldsMap(Map values){ + super(values); + } + + public Schema.SObjectField get(String name){ + return this.get(name, true); + } + public Schema.SObjectField get(String name, Boolean implyNamespace){ + return (Schema.SObjectField) this.getObject(name, implyNamespace); + } + public List values(){ + return (List) values.values(); + } + + } + /** + * A subclass of NamespacedAttributeMap for handling the data returned by #Schema.getGlobalDescribe + **/ + public class GlobalDescribeMap extends NamespacedAttributeMap{ + @testVisible + private GlobalDescribeMap(Map values){ + super(values); + } + + public Schema.SObjectType get(String name){ + return this.get(name, true); + } + public Schema.SObjectType get(String name, Boolean implyNamespace){ + return (Schema.SObjectType) this.getObject(name, implyNamespace); + } + public List values(){ + return (List) values.values(); + } + } + + + public abstract class DescribeException extends Exception{} + public class DuplicateDescribeException extends DescribeException{} //Test coverage for this requires APIv28's @testVisbile annotation to force exception cases. + public class InvalidDescribeException extends DescribeException{} +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_SObjectDescribe.cls-meta.xml b/rolluptool/src/classes/fflib_SObjectDescribe.cls-meta.xml new file mode 100644 index 00000000..b12420ea --- /dev/null +++ b/rolluptool/src/classes/fflib_SObjectDescribe.cls-meta.xml @@ -0,0 +1,5 @@ + + + 31.0 + Active + diff --git a/rolluptool/src/classes/fflib_SObjectDescribeTest.cls b/rolluptool/src/classes/fflib_SObjectDescribeTest.cls new file mode 100644 index 00000000..4cda2c7b --- /dev/null +++ b/rolluptool/src/classes/fflib_SObjectDescribeTest.cls @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2014, FinancialForce.com, inc + * 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 FinancialForce.com, inc 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. +**/ + +/** + This class adapted from https://github.com/capeterson/Apex-Util + Used under a BSD license: https://github.com/capeterson/Apex-Util/blob/master/LICENSE +**/ +@isTest +private class fflib_SObjectDescribeTest { + + @isTest + static void NamespacedAttributeMap_implementations(){ + fflib_SObjectDescribe.GlobalDescribeMap gdm = fflib_SObjectDescribe.getGlobalDescribe(); + Schema.SObjectType accountObjType = gdm.get('AccOunT'); + System.assertEquals(accountObjType, Account.SobjectType); + System.assertEquals(Schema.getGlobalDescribe().size(), gdm.size()); + + fflib_SObjectDescribe acccountDescribe = fflib_SObjectDescribe.getDescribe(accountObjType); + fflib_SObjectDescribe.FieldsMap fields = acccountDescribe.getFields(); + System.assert( fields.keySet().containsAll(acccountDescribe.getFieldsMap().keySet()) ); + + System.assertEquals(fields.get('name'), Account.SObjectType.fields.name); //behavior of FieldsMap is tested in another method + System.assertEquals(Schema.SObjectType.Account.fields.getMap().size(), fields.size()); + } + + @isTest + static void FieldsMap(){ + String fakeNamespace = 'fflib_test'; + Map fakeFieldData = new Map{ + 'name__c' => Contact.SObjectType.fields.name, //re-use stndard field types since we can't mock them + fakeNamespace+'__name__c' => Account.SObjectType.fields.name, + 'createddate' => Contact.SObjectType.fields.CreatedDate + }; + fflib_SObjectDescribe.FieldsMap fields = new fflib_SObjectDescribe.FieldsMap(fakeFieldData); + fields.currentNamespace = fakeNamespace; + System.assertEquals(true, fields.containsKey('name__c') ); + System.assertEquals(true, fields.containsKey(fakeNamespace+'__name__c') ); + System.assert(fields.get('name__c') === fields.get(fakeNamespace+'__name__c')); + + fields.currentNamespace = 'someOtherNamespace'; + System.assertNotEquals(fields.get('name__c'), fields.get(fakeNamespace+'__name__c')); + } + + @isTest + static void GlobalDescribeMap(){ + String fakeNamespace = 'fflib_test'; + Map fakeFieldData = new Map{ + 'name__c' => Contact.SObjectType, //re-use stndard object types since we can't mock them + fakeNamespace+'__name__c' => Account.SObjectType, + 'createddate' => Lead.SObjectType + }; + fflib_SObjectDescribe.GlobalDescribeMap gdm = new fflib_SObjectDescribe.GlobalDescribeMap(fakeFieldData); + gdm.currentNamespace = fakeNamespace; + System.assertEquals(true, gdm.containsKey('name__c') ); + System.assertEquals(true, gdm.containsKey(fakeNamespace+'__name__c') ); + System.assert(gdm.get('name__c') === gdm.get(fakeNamespace+'__name__c')); + + gdm.currentNamespace = 'someOtherNamespace'; + System.assertNotEquals(gdm.get('name__c'), gdm.get(fakeNamespace+'__name__c')); + } + + @isTest //Tests all forms of the getDescribe static + static void getAccountDescribes(){ + fflib_SObjectDescribe d = fflib_SObjectDescribe.getDescribe('Account'); + fflib_SObjectDescribe d2 = fflib_SObjectDescribe.getDescribe(Account.SObjectType); + fflib_SObjectDescribe d3 = fflib_SObjectDescribe.getDescribe(Schema.SObjectType.Account); + System.assertEquals('Account', d.getDescribe().getName()); + System.assert( (d === d2 && d2 === d3) ,'All three getDescribe calls should return the same cached instance.'); + } + + @isTest + static void simpleAccountFieldDescribe(){ + fflib_SObjectDescribe d = fflib_SObjectDescribe.getDescribe(Account.SObjectType); + Map fields; + for(integer i = 0; i < 10; i++){ + fields = d.getFieldsMap(); + } + System.assertEquals(false,fields.isEmpty()); + } + + @isTest + static void simpleAccountFieldSetDescribe(){ + fflib_SObjectDescribe d = fflib_SObjectDescribe.getDescribe(Account.SObjectType); + Map fields; + for(integer i = 0; i < 10; i++){ + fields = d.getFieldSetsMap(); + } + + // We need to assert something here... but what? + //no asserts on result size to avoid a requirement on field sets existing + } + + @isTest + static void simpleAccountGetNameField(){ + fflib_SObjectDescribe d = fflib_SObjectDescribe.getDescribe(Account.SObjectType); + Schema.SObjectField nameField = d.getNameField(); + System.assertEquals('Name', nameField.getDescribe().getName()); + } + + @isTest + static void flushCache(){ + fflib_SObjectDescribe d = fflib_SObjectDescribe.getDescribe('Account'); + fflib_SObjectDescribe.flushCache(); + fflib_SObjectDescribe d2 = fflib_SObjectDescribe.getDescribe('Account'); + System.assert(d !== d2, 'Second object should be a fresh instance after a cache flush.' ); + } + + @isTest + static void rawGlobalDescribeCheck(){ + Map systemGd = Schema.getGlobalDescribe(); + Map cachedGd = fflib_SObjectDescribe.getRawGlobalDescribe(); + System.assertEquals(systemGd.size(),cachedGd.size()); + } + +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_SObjectDescribeTest.cls-meta.xml b/rolluptool/src/classes/fflib_SObjectDescribeTest.cls-meta.xml new file mode 100644 index 00000000..b12420ea --- /dev/null +++ b/rolluptool/src/classes/fflib_SObjectDescribeTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 31.0 + Active + diff --git a/rolluptool/src/classes/fflib_SObjectDomain.cls b/rolluptool/src/classes/fflib_SObjectDomain.cls new file mode 100644 index 00000000..5c3c73f9 --- /dev/null +++ b/rolluptool/src/classes/fflib_SObjectDomain.cls @@ -0,0 +1,797 @@ +/** + * Copyright (c) 2012, FinancialForce.com, inc + * 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 FinancialForce.com, inc 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. +**/ + +/** + * Base class aiding in the implemetnation of a Domain Model around SObject collections + * + * Domain (software engineering). “a set of common requirements, terminology, and functionality + * for any software program constructed to solve a problem in that field”, + * http://en.wikipedia.org/wiki/Domain_(software_engineering) + * + * Domain Model, “An object model of the domain that incorporates both behavior and data.”, + * “At its worst business logic can be very complex. Rules and logic describe many different " + * "cases and slants of behavior, and it's this complexity that objects were designed to work with...” + * Martin Fowler, EAA Patterns + * http://martinfowler.com/eaaCatalog/domainModel.html + * + **/ +public virtual with sharing class fflib_SObjectDomain + implements fflib_ISObjectDomain +{ + /** + * Provides access to the data represented by this domain class + **/ + public List Records { get; private set;} + + /** + * Derived from the records provided during construction, provides the native describe for the standard or custom object + **/ + public Schema.DescribeSObjectResult SObjectDescribe {get; private set;} + + /** + * Exposes the configuration for this domain class instance + **/ + public Configuration Configuration {get; private set;} + + /** + * Useful during unit testign to assert at a more granular and robust level for errors raised during the various trigger events + **/ + public static ErrorFactory Errors {get; private set;} + + /** + * Useful during unit testing to access mock support for database inserts and udpates (testing without DML) + **/ + public static TestFactory Test {get; private set;} + + /** + * Retains instances of domain classes implementing trigger stateful + **/ + private static Map> TriggerStateByClass; + + static + { + Errors = new ErrorFactory(); + + Test = new TestFactory(); + + TriggerStateByClass = new Map>(); + } + + /** + * Constructs the domain class with the data on which to apply the behaviour implemented within + **/ + public fflib_SObjectDomain(List sObjectList) + { + // Ensure the domain class has its own copy of the data + Records = sObjectList.clone(); + // Capture SObjectType describe for this domain class + SObjectDescribe = Records.getSObjectType().getDescribe(); + // Configure the Domain object instance + Configuration = new Configuration(); + } + + /** + * Override this to apply defaults to the records, this is called by the handleBeforeInsert method + **/ + public virtual void onApplyDefaults() { } + + /** + * Override this to apply general validation to be performed during insert or update, called by the handleAfterInsert and handleAfterUpdate methods + **/ + public virtual void onValidate() { } + + /** + * Override this to apply validation to be performed during insert, called by the handleAfterUpdate method + **/ + public virtual void onValidate(Map existingRecords) { } + + /** + * Override this to perform processing during the before insert phase, this is called by the handleBeforeInsert method + **/ + public virtual void onBeforeInsert() { } + + /** + * Override this to perform processing during the before update phase, this is called by the handleBeforeUpdate method + **/ + public virtual void onBeforeUpdate(Map existingRecords) { } + + /** + * Override this to perform processing during the before delete phase, this is called by the handleBeforeDelete method + **/ + public virtual void onBeforeDelete() { } + + /** + * Override this to perform processing during the after insert phase, this is called by the handleAfterInsert method + **/ + public virtual void onAfterInsert() { } + + /** + * Override this to perform processing during the after update phase, this is called by the handleAfterUpdate method + **/ + public virtual void onAfterUpdate(Map existingRecords) { } + + /** + * Override this to perform processing during the after delete phase, this is called by the handleAfterDelete method + **/ + public virtual void onAfterDelete() { } + + /** + * Base handler for the Apex Trigger event Before Insert, calls the onApplyDefaults method, followed by onBeforeInsert + **/ + public virtual void handleBeforeInsert() + { + onApplyDefaults(); + onBeforeInsert(); + } + + /** + * Base handler for the Apex Trigger event Before Update, calls the onBeforeUpdate method + **/ + public void handleBeforeUpdate(Map existingRecords) + { + onBeforeUpdate(existingRecords); + } + + /** + * Base handler for the Apex Trigger event Before Delete, calls the onBeforeDelete method + **/ + public void handleBeforeDelete() + { + onBeforeDelete(); + } + + /** + * Base handler for the Apex Trigger event After Insert, checks object security and calls the onValidate and onAfterInsert methods + * + * @throws DomainException if the current user context is not able to create records + **/ + public void handleAfterInsert() + { + if(Configuration.EnforcingTriggerCRUDSecurity && !SObjectDescribe.isCreateable()) + throw new DomainException('Permission to create an ' + SObjectDescribe.getName() + ' denied.'); + + onValidate(); + onAfterInsert(); + } + + /** + * Base handler for the Apex Trigger event After Update, checks object security and calls the onValidate, onValidate(Map) and onAfterUpdate methods + * + * @throws DomainException if the current user context is not able to update records + **/ + public void handleAfterUpdate(Map existingRecords) + { + if(Configuration.EnforcingTriggerCRUDSecurity && !SObjectDescribe.isUpdateable()) + throw new DomainException('Permission to udpate an ' + SObjectDescribe.getName() + ' denied.'); + + if(Configuration.OldOnUpdateValidateBehaviour) + onValidate(); + onValidate(existingRecords); + onAfterUpdate(existingRecords); + } + + /** + * Base handler for the Apex Trigger event After Delete, checks object security and calls the onAfterDelete method + * + * @throws DomainException if the current user context is not able to delete records + **/ + public void handleAfterDelete() + { + if(Configuration.EnforcingTriggerCRUDSecurity && !SObjectDescribe.isDeletable()) + throw new DomainException('Permission to delete an ' + SObjectDescribe.getName() + ' denied.'); + + onAfterDelete(); + } + + /** + * Returns the SObjectType this Domain class represents + **/ + public SObjectType getSObjectType() + { + return SObjectDescribe.getSObjectType(); + } + + /** + * Returns the SObjectType this Domain class represents + **/ + public SObjectType sObjectType() + { + return getSObjectType(); + } + + /** + * Alternative to the Records property, provided to support mocking of Domain classes + **/ + public List getRecords() + { + return Records; + } + + /** + * Interface used to aid the triggerHandler in constructing instances of Domain classes + **/ + public interface IConstructable + { + fflib_SObjectDomain construct(List sObjectList); + } + + /** + * For Domain classes implementing the ITriggerStateful interface returns the instance + * of the domain class being shared between trigger invocations, returns null if + * the Domain class trigger has not yet fired or the given domain class does not implement + * the ITriggerStateful interface. Note this method is sensitive to recursion, meaning + * it will return the applicable domain instance for the level of recursion + **/ + public static fflib_SObjectDomain getTriggerInstance(Type domainClass) + { + List domains = TriggerStateByClass.get(domainClass); + if(domains==null || domains.size()==0) + return null; + return domains[domains.size()-1]; + } + + /** + * Method constructs the given Domain class with the current Trigger context + * before calling the applicable override methods such as beforeInsert, beforeUpdate etc. + **/ + public static void triggerHandler(Type domainClass) + { + // Process the trigger context + if(System.Test.isRunningTest() & Test.Database.hasRecords()) + { + // If in test context and records in the mock database delegate initially to the mock database trigger handler + Test.Database.testTriggerHandler(domainClass); + } + else + { + // Process the runtime Apex Trigger context + triggerHandler(domainClass, + Trigger.isBefore, + Trigger.isAfter, + Trigger.isInsert, + Trigger.isUpdate, + Trigger.isDelete, + Trigger.new, + Trigger.oldMap); + } + } + + /** + * Calls the applicable override methods such as beforeInsert, beforeUpdate etc. based on a Trigger context + **/ + private static void triggerHandler(Type domainClass, Boolean isBefore, Boolean isAfter, Boolean isInsert, Boolean isUpdate, Boolean isDelete, List newRecords, Map oldRecordsMap) + { + // After phase of trigger will reuse prior instance of domain class if ITriggerStateful implemented + fflib_SObjectDomain domainObject = isBefore ? null : popTriggerInstance(domainClass, isDelete ? oldRecordsMap.values() : newRecords); + if(domainObject==null) + { + // Construct the domain class constructor class + String domainClassName = domainClass.getName(); + Type constructableClass = domainClassName.endsWith('Constructor') ? Type.forName(domainClassName) : Type.forName(domainClassName+'.Constructor'); + IConstructable domainConstructor = (IConstructable) constructableClass.newInstance(); + + // Construct the domain class with the approprite record set + if(isInsert) domainObject = domainConstructor.construct(newRecords); + else if(isUpdate) domainObject = domainConstructor.construct(newRecords); + else if(isDelete) domainObject = domainConstructor.construct(oldRecordsMap.values()); + + // Should this instance be reused on the next trigger invocation? + if(domainObject.Configuration.TriggerStateEnabled) + // Push this instance onto the stack to be popped during the after phase + pushTriggerInstance(domainClass, domainObject); + } + + // Invoke the applicable handler + if(isBefore) + { + if(isInsert) domainObject.handleBeforeInsert(); + else if(isUpdate) domainObject.handleBeforeUpdate(oldRecordsMap); + else if(isDelete) domainObject.handleBeforeDelete(); + } + else + { + if(isInsert) domainObject.handleAfterInsert(); + else if(isUpdate) domainObject.handleAfterUpdate(oldRecordsMap); + else if(isDelete) domainObject.handleAfterDelete(); + } + } + + /** + * Pushes to the stack of domain classes per type a domain object instance + **/ + private static void pushTriggerInstance(Type domainClass, fflib_SObjectDomain domain) + { + List domains = TriggerStateByClass.get(domainClass); + if(domains==null) + TriggerStateByClass.put(domainClass, domains = new List()); + domains.add(domain); + } + + /** + * Pops from the stack of domain classes per type a domain object instance and updates the record set + **/ + private static fflib_SObjectDomain popTriggerInstance(Type domainClass, List records) + { + List domains = TriggerStateByClass.get(domainClass); + if(domains==null || domains.size()==0) + return null; + fflib_SObjectDomain domain = domains.remove(domains.size()-1); + domain.Records = records; + return domain; + } + + /** + * Fluent style Configuration system for Domain class creation + **/ + public class Configuration + { + /** + * Backwards compatability mode for handleAfterUpdate routing to onValidate() + **/ + public Boolean OldOnUpdateValidateBehaviour {get; private set;} + /** + * True if the base class is checking the users CRUD requirements before invoking trigger methods + **/ + public Boolean EnforcingTriggerCRUDSecurity {get; private set;} + + /** + * Enables reuse of the same Domain instance between before and after trigger phases (subject to recursive scenarios) + **/ + public Boolean TriggerStateEnabled {get; private set;} + + /** + * Default configuration + **/ + public Configuration() + { + EnforcingTriggerCRUDSecurity = true; // Default is true for backwards compatability + TriggerStateEnabled = false; + OldOnUpdateValidateBehaviour = false; // Breaking change, but felt to better practice + } + + /** + * See associated property + **/ + public Configuration enableTriggerState() + { + TriggerStateEnabled = true; + return this; + } + + /** + * See associated property + **/ + public Configuration disableTriggerState() + { + TriggerStateEnabled = false; + return this; + } + + /** + * See associated property + **/ + public Configuration enforceTriggerCRUDSecurity() + { + EnforcingTriggerCRUDSecurity = true; + return this; + } + + /** + * See associated property + **/ + public Configuration disableTriggerCRUDSecurity() + { + EnforcingTriggerCRUDSecurity = false; + return this; + } + + /** + * See associated property + **/ + public Configuration enableOldOnUpdateValidateBehaviour() + { + OldOnUpdateValidateBehaviour = true; + return this; + } + + /** + * See associated property + **/ + public Configuration disableOldOnUpdateValidateBehaviour() + { + OldOnUpdateValidateBehaviour = false; + return this; + } + } + + /** + * General exception class for the domain layer + **/ + public class DomainException extends Exception + { + } + + /** + * Ensures logging of errors in the Domain context for later assertions in tests + **/ + public String error(String message, SObject record) + { + return Errors.error(this, message, record); + } + + /** + * Ensures logging of errors in the Domain context for later assertions in tests + **/ + public String error(String message, SObject record, SObjectField field) + { + return Errors.error(this, message, record, field); + } + + /** + * Ensures logging of errors in the Domain context for later assertions in tests + **/ + public class ErrorFactory + { + private List errorList = new List(); + + private ErrorFactory() + { + + } + + public String error(String message, SObject record) + { + return error(null, message, record); + } + + private String error(fflib_SObjectDomain domain, String message, SObject record) + { + ObjectError objectError = new ObjectError(); + objectError.domain = domain; + objectError.message = message; + objectError.record = record; + errorList.add(objectError); + return message; + } + + public String error(String message, SObject record, SObjectField field) + { + return error(null, message, record, field); + } + + private String error(fflib_SObjectDomain domain, String message, SObject record, SObjectField field) + { + FieldError fieldError = new FieldError(); + fieldError.domain = domain; + fieldError.message = message; + fieldError.record = record; + fieldError.field = field; + errorList.add(fieldError); + return message; + } + + public List getAll() + { + return errorList.clone(); + } + + public void clearAll() + { + errorList.clear(); + } + } + + /** + * Ensures logging of errors in the Domain context for later assertions in tests + **/ + public class FieldError extends ObjectError + { + public SObjectField field; + + private FieldError() + { + + } + } + + /** + * Ensures logging of errors in the Domain context for later assertions in tests + **/ + public virtual class ObjectError extends Error + { + public SObject record; + + private ObjectError() + { + + } + } + + /** + * Ensures logging of errors in the Domain context for later assertions in tests + **/ + public abstract class Error + { + public String message; + public fflib_SObjectDomain domain; + } + + /** + * Provides test context mocking facilities to unit tests testing domain classes + **/ + public class TestFactory + { + public MockDatabase Database = new MockDatabase(); + + private TestFactory() + { + + } + } + + /** + * Class used during Unit testing of Domain classes, can be used (not exclusively) to speed up test execution and focus testing + **/ + public class MockDatabase + { + private Boolean isInsert = false; + private Boolean isUpdate = false; + private Boolean isDelete = false; + private List records = new List(); + private Map oldRecords = new Map(); + + private MockDatabase() + { + + } + + private void testTriggerHandler(Type domainClass) + { + // Mock Before + triggerHandler(domainClass, true, false, isInsert, isUpdate, isDelete, records, oldRecords); + + // Mock After + triggerHandler(domainClass, false, true, isInsert, isUpdate, isDelete, records, oldRecords); + } + + public void onInsert(List records) + { + this.isInsert = true; + this.isUpdate = false; + this.isDelete = false; + this.records = records; + } + + public void onUpdate(List records, Map oldRecords) + { + this.isInsert = false; + this.isUpdate = true; + this.isDelete = false; + this.records = records; + this.oldRecords = oldRecords; + } + + public void onDelete(Map records) + { + this.isInsert = false; + this.isUpdate = false; + this.isDelete = true; + this.oldRecords = records; + } + + public Boolean hasRecords() + { + return records!=null && records.size()>0 || oldRecords!=null && oldRecords.size()>0; + } + } + + /** + * Test domain class (ideally this would be in the test class, however Type.newInstance does not see such classes) + **/ + public with sharing class TestSObjectDomain extends fflib_SObjectDomain + { + private String someState; + + public TestSObjectDomain(List sObjectList) + { + // Domain classes are initialised with lists to enforce bulkification throughout + super(sObjectList); + } + + public override void onApplyDefaults() + { + // Not required in production code + super.onApplyDefaults(); + + // Apply defaults to Testfflib_SObjectDomain + for(Opportunity opportunity : (List) Records) + { + opportunity.CloseDate = System.today().addDays(30); + } + } + + public override void onValidate() + { + // Not required in production code + super.onValidate(); + + // Validate Testfflib_SObjectDomain + for(Opportunity opp : (List) Records) + { + if(opp.Type!=null && opp.Type.startsWith('Existing') && opp.AccountId == null) + { + opp.AccountId.addError( error('You must provide an Account for Opportunities for existing Customers.', opp, Opportunity.AccountId) ); + } + } + } + + public override void onValidate(Map existingRecords) + { + // Not required in production code + super.onValidate(existingRecords); + + // Validate changes to Testfflib_SObjectDomain + for(Opportunity opp : (List) Records) + { + Opportunity existingOpp = (Opportunity) existingRecords.get(opp.Id); + if(opp.Type != existingOpp.Type) + { + opp.Type.addError( error('You cannot change the Opportunity type once it has been created.', opp, Opportunity.Type) ); + } + } + } + + public override void onBeforeDelete() + { + // Not required in production code + super.onBeforeDelete(); + + // Validate changes to Testfflib_SObjectDomain + for(Opportunity opp : (List) Records) + { + opp.addError( error('You cannot delete this Opportunity.', opp) ); + } + } + + public override void onBeforeInsert() + { + // Assert this variable is null in the after insert (since this domain class is stateless) + someState = 'This should not survice the trigger after phase'; + } + + public override void onAfterInsert() + { + // This is a stateless domain class, so should not retain anything betweet before and after + System.assertEquals(null, someState); + } + } + + /** + * Typically an inner class to the domain class, supported here for test purposes + **/ + public class TestSObjectDomainConstructor implements fflib_SObjectDomain.IConstructable + { + public fflib_SObjectDomain construct(List sObjectList) + { + return new TestSObjectDomain(sObjectList); + } + } + + /** + * Test domain class (ideally this would be in the test class, however Type.newInstance does not see such classes) + **/ + public with sharing class TestSObjectStatefulDomain + extends fflib_SObjectDomain + { + public String someState; + + public TestSObjectStatefulDomain(List sObjectList) + { + super(sObjectList); + + // Ensure this instance is re-used in the after trigger phase (subject to recursive scenarios) + Configuration.enableTriggerState(); + } + + public override void onBeforeInsert() + { + // This must always be null, as we do not reuse domain instances within recursive scenarios (different record sets) + System.assertEquals(null, someState); + + // Process records + List newOpps = new List(); + for(Opportunity opp : (List) Records) + { + // Set some state sensitive to the incoming records + someState = 'Error on Record ' + opp.Name; + + // Create a new Opportunity record to trigger recursive code path? + if(opp.Name.equals('Test Recursive 1')) + newOpps.add(new Opportunity ( Name = 'Test Recursive 2', Type = 'Existing Account' )); + } + + // If testing recursiving emulate an insert + if(newOpps.size()>0) + { + // This will force recursion and thus validate via the above assert results in a new domain instance + fflib_SObjectDomain.Test.Database.onInsert(newOpps); + fflib_SObjectDomain.triggerHandler(fflib_SObjectDomain.TestSObjectStatefulDomainConstructor.class); + } + } + + public override void onAfterInsert() + { + // Use the state set in the before insert (since this is a stateful domain class) + if(someState!=null) + for(Opportunity opp : (List) Records) + opp.addError(error(someState, opp)); + } + } + + /** + * Typically an inner class to the domain class, supported here for test purposes + **/ + public class TestSObjectStatefulDomainConstructor implements fflib_SObjectDomain.IConstructable + { + public fflib_SObjectDomain construct(List sObjectList) + { + return new TestSObjectStatefulDomain(sObjectList); + } + } + + /** + * Test domain class (ideally this would be in the test class, however Type.newInstance does not see such classes) + **/ + public with sharing class TestSObjectOnValidateBehaviour + extends fflib_SObjectDomain + { + public TestSObjectOnValidateBehaviour(List sObjectList) + { + super(sObjectList); + + // Enable old behaviour based on the test Opportunity name passed in + if(sObjectList[0].Name == 'Test Enable Old Behaviour') + Configuration.enableOldOnUpdateValidateBehaviour(); + } + + public override void onValidate() + { + // Throw exception to give the test somethign to assert on + throw new DomainException('onValidate called'); + } + } + + /** + * Typically an inner class to the domain class, supported here for test purposes + **/ + public class TestSObjectOnValidateBehaviourConstructor implements fflib_SObjectDomain.IConstructable + { + public fflib_SObjectDomain construct(List sObjectList) + { + return new TestSObjectOnValidateBehaviour(sObjectList); + } + } +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_SObjectDomain.cls-meta.xml b/rolluptool/src/classes/fflib_SObjectDomain.cls-meta.xml new file mode 100644 index 00000000..b12420ea --- /dev/null +++ b/rolluptool/src/classes/fflib_SObjectDomain.cls-meta.xml @@ -0,0 +1,5 @@ + + + 31.0 + Active + diff --git a/rolluptool/src/classes/fflib_SObjectDomainTest.cls b/rolluptool/src/classes/fflib_SObjectDomainTest.cls new file mode 100644 index 00000000..df3125ae --- /dev/null +++ b/rolluptool/src/classes/fflib_SObjectDomainTest.cls @@ -0,0 +1,214 @@ +/** + * Copyright (c), FinancialForce.com, inc + * 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 FinancialForce.com, inc 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 with sharing class fflib_SObjectDomainTest +{ + @IsTest + private static void testValidationWithoutDML() + { + fflib_SObjectDomain.TestSObjectDomain opps = new fflib_SObjectDomain.TestSObjectDomain(new Opportunity[] { new Opportunity ( Name = 'Test', Type = 'Existing Account' ) } ); + opps.onValidate(); + System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size()); + System.assertEquals('You must provide an Account for Opportunities for existing Customers.', fflib_SObjectDomain.Errors.getAll()[0].message); + System.assertEquals(Opportunity.AccountId, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[0]).field); + } + + @IsTest + private static void testInsertValidationFailedWithoutDML() + { + Opportunity opp = new Opportunity ( Name = 'Test', Type = 'Existing Account' ); + System.assertEquals(false, fflib_SObjectDomain.Test.Database.hasRecords()); + fflib_SObjectDomain.Test.Database.onInsert(new Opportunity[] { opp } ); + System.assertEquals(true, fflib_SObjectDomain.Test.Database.hasRecords()); + fflib_SObjectDomain.triggerHandler(fflib_SObjectDomain.TestSObjectDomainConstructor.class); + System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size()); + System.assertEquals('You must provide an Account for Opportunities for existing Customers.', fflib_SObjectDomain.Errors.getAll()[0].message); + System.assertEquals(Opportunity.AccountId, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[0]).field); + } + + @IsTest + private static void testUpdateValidationFailedWithoutDML() + { + Opportunity oldOpp = (Opportunity) Opportunity.sObjectType.newSObject('006E0000006mkRQ'); + oldOpp.Name = 'Test'; + oldOpp.Type = 'Existing Account'; + Opportunity newOpp = (Opportunity) Opportunity.sObjectType.newSObject('006E0000006mkRQ'); + newOpp.Name = 'Test'; + newOpp.Type = 'New Account'; + System.assertEquals(false, fflib_SObjectDomain.Test.Database.hasRecords()); + fflib_SObjectDomain.Test.Database.onUpdate(new Opportunity[] { newOpp }, new Map { newOpp.Id => oldOpp } ); + System.assertEquals(true, fflib_SObjectDomain.Test.Database.hasRecords()); + fflib_SObjectDomain.triggerHandler(fflib_SObjectDomain.TestSObjectDomainConstructor.class); + System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size()); + System.assertEquals('You cannot change the Opportunity type once it has been created.', fflib_SObjectDomain.Errors.getAll()[0].message); + System.assertEquals(Opportunity.Type, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[0]).field); + } + + @IsTest + private static void testOnBeforeDeleteWithoutDML() + { + Opportunity opp = (Opportunity) Opportunity.sObjectType.newSObject('006E0000006mkRQ'); + opp.Name = 'Test'; + opp.Type = 'Existing Account'; + System.assertEquals(false, fflib_SObjectDomain.Test.Database.hasRecords()); + fflib_SObjectDomain.Test.Database.onDelete(new Map { opp.Id => opp } ); + System.assertEquals(true, fflib_SObjectDomain.Test.Database.hasRecords()); + fflib_SObjectDomain.triggerHandler(fflib_SObjectDomain.TestSObjectDomainConstructor.class); + System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size()); + System.assertEquals('You cannot delete this Opportunity.', fflib_SObjectDomain.Errors.getAll()[0].message); + } + + @IsTest + private static void testObjectSecurity() + { + // Create a user which will not have access to the test object type + User testUser = createChatterExternalUser(); + if(testUser==null) + return; // Abort the test if unable to create a user with low enough acess + System.runAs(testUser) + { + // Test Create object security + Opportunity opp = new Opportunity ( Name = 'Test', Type = 'Existing Account' ); + fflib_SObjectDomain.Test.Database.onInsert(new Opportunity[] { opp } ); + try { + fflib_SObjectDomain.triggerHandler(fflib_SObjectDomain.TestSObjectDomainConstructor.class); + System.assert(false, 'Expected access denied exception'); + } catch (Exception e) { + System.assertEquals('Permission to create an Opportunity denied.', e.getMessage()); + } + + // Test Update object security + Opportunity existingOpp = (Opportunity) Opportunity.sObjectType.newSObject('006E0000006mkRQ'); + existingOpp.Name = 'Test'; + existingOpp.Type = 'Existing Account'; + fflib_SObjectDomain.Test.Database.onUpdate(new List { opp }, new Map { opp.Id => opp } ); + try { + fflib_SObjectDomain.triggerHandler(fflib_SObjectDomain.TestSObjectDomainConstructor.class); + System.assert(false, 'Expected access denied exception'); + } catch (Exception e) { + System.assertEquals('Permission to udpate an Opportunity denied.', e.getMessage()); + } + + // Test Delete object security + fflib_SObjectDomain.Test.Database.onDelete(new Map { opp.Id => opp }); + try { + fflib_SObjectDomain.triggerHandler(fflib_SObjectDomain.TestSObjectDomainConstructor.class); + System.assert(false, 'Expected access denied exception'); + } catch (Exception e) { + System.assertEquals('Permission to delete an Opportunity denied.', e.getMessage()); + } + } + } + + @IsTest + private static void testErrorLogging() + { + // Test static helpers for raise none domain object instance errors + Opportunity opp = new Opportunity ( Name = 'Test', Type = 'Existing Account' ); + fflib_SObjectDomain.Errors.error('Error', opp); + fflib_SObjectDomain.Errors.error('Error', opp, Opportunity.Type); + System.assertEquals(2, fflib_SObjectDomain.Errors.getAll().size()); + System.assertEquals('Error', fflib_SObjectDomain.Errors.getAll()[0].message); + System.assertEquals('Error', fflib_SObjectDomain.Errors.getAll()[1].message); + System.assertEquals(Opportunity.Type, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[1]).field); + fflib_SObjectDomain.Errors.clearAll(); + System.assertEquals(0, fflib_SObjectDomain.Errors.getAll().size()); + } + + @IsTest + private static void testTriggerState() + { + Opportunity opp = new Opportunity ( Name = 'Test', Type = 'Existing Account' ); + fflib_SObjectDomain.Test.Database.onInsert(new Opportunity[] { opp } ); + fflib_SObjectDomain.triggerHandler(fflib_SObjectDomain.TestSObjectStatefulDomainConstructor.class); + System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size()); + System.assertEquals('Error on Record Test', fflib_SObjectDomain.Errors.getAll()[0].message); + } + + @IsTest + private static void testRecursiveTriggerState() + { + Opportunity opp = new Opportunity ( Name = 'Test Recursive 1', Type = 'Existing Account' ); + fflib_SObjectDomain.Test.Database.onInsert(new Opportunity[] { opp } ); + fflib_SObjectDomain.triggerHandler(fflib_SObjectDomain.TestSObjectStatefulDomainConstructor.class); + System.assertEquals(2, fflib_SObjectDomain.Errors.getAll().size()); + System.assertEquals('Error on Record Test Recursive 2', fflib_SObjectDomain.Errors.getAll()[0].message); + System.assertEquals('Error on Record Test Recursive 1', fflib_SObjectDomain.Errors.getAll()[1].message); + } + + @IsTest + private static void testOnValidateBehaviorDefault() + { + Opportunity oldOpp = (Opportunity) Opportunity.sObjectType.newSObject('006E0000006mkRQ'); + oldOpp.Name = 'Test Default Behaviour'; + oldOpp.Type = 'Existing Account'; + Opportunity newOpp = (Opportunity) Opportunity.sObjectType.newSObject('006E0000006mkRQ'); + newOpp.Name = 'Test Default Behaviour'; + newOpp.Type = 'New Account'; + fflib_SObjectDomain.Test.Database.onUpdate(new Opportunity[] { newOpp }, new Map { newOpp.Id => oldOpp } ); + fflib_SObjectDomain.triggerHandler(fflib_SObjectDomain.TestSObjectOnValidateBehaviourConstructor.class); + } + + @IsTest + private static void testOnValidateBehaviorOld() + { + Opportunity oldOpp = (Opportunity) Opportunity.sObjectType.newSObject('006E0000006mkRQ'); + oldOpp.Name = 'Test Enable Old Behaviour'; + oldOpp.Type = 'Existing Account'; + Opportunity newOpp = (Opportunity) Opportunity.sObjectType.newSObject('006E0000006mkRQ'); + newOpp.Name = 'Test Enable Old Behaviour'; + newOpp.Type = 'New Account'; + fflib_SObjectDomain.Test.Database.onUpdate(new Opportunity[] { newOpp }, new Map { newOpp.Id => oldOpp } ); + try { + fflib_SObjectDomain.triggerHandler(fflib_SObjectDomain.TestSObjectOnValidateBehaviourConstructor.class); + System.assert(false, 'Expected exception'); + } catch (Exception e) { + System.assertEquals('onValidate called', e.getMessage()); + } + } + + /** + * Create test user + **/ + private static User createChatterExternalUser() + { + // Can only proceed with test if we have a suitable profile - Chatter External license has no access to Opportunity + List testProfiles = [Select Id From Profile where UserLicense.Name='Chatter External' limit 1]; + if(testProfiles.size()!=1) + return null; + + // Can only proceed with test if we can successfully insert a test user + String testUsername = System.now().format('yyyyMMddhhmmss') + '@testorg.com'; + User testUser = new User(Alias = 'test1', Email='testuser1@testorg.com', EmailEncodingKey='UTF-8', LastName='Testing', LanguageLocaleKey='en_US', LocaleSidKey='en_US', ProfileId = testProfiles[0].Id, TimeZoneSidKey='America/Los_Angeles', UserName=testUsername); + try { + insert testUser; + } catch (Exception e) { + return null; + } + return testUser; + } +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_SObjectDomainTest.cls-meta.xml b/rolluptool/src/classes/fflib_SObjectDomainTest.cls-meta.xml new file mode 100644 index 00000000..b12420ea --- /dev/null +++ b/rolluptool/src/classes/fflib_SObjectDomainTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 31.0 + Active + diff --git a/rolluptool/src/classes/fflib_SObjectMocks.cls b/rolluptool/src/classes/fflib_SObjectMocks.cls new file mode 100644 index 00000000..f1b312a6 --- /dev/null +++ b/rolluptool/src/classes/fflib_SObjectMocks.cls @@ -0,0 +1,232 @@ +/** + * Copyright (c), FinancialForce.com, inc + * 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 FinancialForce.com, inc 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. +**/ + +/** + * This is a generated class with some manual modifications + * Use the Apex Mocks Generator, then re-apply the 'virtual' modifier + **/ +@IsTest +public class fflib_SObjectMocks +{ + public virtual class SObjectDomain implements fflib_ISObjectDomain + { + private fflib_ApexMocks mocks; + + public SObjectDomain(fflib_ApexMocks mocks) + { + this.mocks = mocks; + } + + public SObjectType sObjectType() + { + if (mocks.Verifying) + { + mocks.verifyMethodCall(this, 'sObjectType', new List {}); + } + else if (mocks.Stubbing) + { + mocks.prepareMethodReturnValue(this, 'sObjectType', new List {}); + return null; + } + else + { + mocks.recordMethod(this, 'sObjectType', new List {}); + + fflib_MethodReturnValue methodReturnValue = mocks.getMethodReturnValue(this, 'sObjectType', new List {}); + + if (methodReturnValue != null) + { + if (methodReturnValue.ReturnValue instanceof Exception) + { + throw ((Exception) methodReturnValue.ReturnValue); + } + + return (SObjectType) methodReturnValue.ReturnValue; + } + } + + return null; + } + + public List getRecords() + { + if (mocks.Verifying) + { + mocks.verifyMethodCall(this, 'getRecords', new List {}); + } + else if (mocks.Stubbing) + { + mocks.prepareMethodReturnValue(this, 'getRecords', new List {}); + return null; + } + else + { + mocks.recordMethod(this, 'getRecords', new List {}); + + fflib_MethodReturnValue methodReturnValue = mocks.getMethodReturnValue(this, 'getRecords', new List {}); + + if (methodReturnValue != null) + { + if (methodReturnValue.ReturnValue instanceof Exception) + { + throw ((Exception) methodReturnValue.ReturnValue); + } + + return (List) methodReturnValue.ReturnValue; + } + } + + return null; + } + } + + public virtual class SObjectSelector implements fflib_ISObjectSelector + { + private fflib_ApexMocks mocks; + + public SObjectSelector(fflib_ApexMocks mocks) + { + this.mocks = mocks; + } + + public Schema.SObjectType sObjectType() + { + if (mocks.Verifying) + { + mocks.verifyMethodCall(this, 'sObjectType', new List {}); + } + else if (mocks.Stubbing) + { + mocks.prepareMethodReturnValue(this, 'sObjectType', new List {}); + return null; + } + else + { + mocks.recordMethod(this, 'sObjectType', new List {}); + + fflib_MethodReturnValue methodReturnValue = mocks.getMethodReturnValue(this, 'sObjectType', new List {}); + + if (methodReturnValue != null) + { + if (methodReturnValue.ReturnValue instanceof Exception) + { + throw ((Exception) methodReturnValue.ReturnValue); + } + + return (Schema.SObjectType) methodReturnValue.ReturnValue; + } + } + + return null; + } + + public List selectSObjectsById(Set idSet) + { + if (mocks.Verifying) + { + mocks.verifyMethodCall(this, 'selectSObjectsById', new List {idSet}); + } + else if (mocks.Stubbing) + { + mocks.prepareMethodReturnValue(this, 'selectSObjectsById', new List {idSet}); + return null; + } + else + { + mocks.recordMethod(this, 'selectSObjectsById', new List {idSet}); + + fflib_MethodReturnValue methodReturnValue = mocks.getMethodReturnValue(this, 'selectSObjectsById', new List {idSet}); + + if (methodReturnValue != null) + { + if (methodReturnValue.ReturnValue instanceof Exception) + { + throw ((Exception) methodReturnValue.ReturnValue); + } + + return (List) methodReturnValue.ReturnValue; + } + } + + return null; + } + } + + public virtual class SObjectUnitOfWork implements fflib_ISObjectUnitOfWork + { + private fflib_ApexMocks mocks; + + public SObjectUnitOfWork(fflib_ApexMocks mocks) + { + this.mocks = mocks; + } + + public void registerNew(SObject record) + { + mocks.mockVoidMethod(this, 'registerNew', new List {record}); + } + + public void registerNew(List records) + { + mocks.mockVoidMethod(this, 'registerNew', new List {records}); + } + + public void registerNew(SObject record, Schema.sObjectField relatedToParentField, SObject relatedToParentRecord) + { + mocks.mockVoidMethod(this, 'registerNew', new List {record, relatedToParentField, relatedToParentRecord}); + } + + public void registerRelationship(SObject record, Schema.sObjectField relatedToField, SObject relatedTo) + { + mocks.mockVoidMethod(this, 'registerRelationship', new List {record, relatedToField, relatedTo}); + } + + public void registerDirty(SObject record) + { + mocks.mockVoidMethod(this, 'registerDirty', new List {record}); + } + + public void registerDirty(List records) + { + mocks.mockVoidMethod(this, 'registerDirty', new List {records}); + } + + public void registerDeleted(SObject record) + { + mocks.mockVoidMethod(this, 'registerDeleted', new List {record}); + } + + public void registerDeleted(List records) + { + mocks.mockVoidMethod(this, 'registerDeleted', new List {records}); + } + + public void commitWork() + { + mocks.mockVoidMethod(this, 'commitWork', new List {}); + } + } +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_SObjectMocks.cls-meta.xml b/rolluptool/src/classes/fflib_SObjectMocks.cls-meta.xml new file mode 100644 index 00000000..b12420ea --- /dev/null +++ b/rolluptool/src/classes/fflib_SObjectMocks.cls-meta.xml @@ -0,0 +1,5 @@ + + + 31.0 + Active + diff --git a/rolluptool/src/classes/fflib_SObjectSelector.cls b/rolluptool/src/classes/fflib_SObjectSelector.cls new file mode 100644 index 00000000..e2946a2a --- /dev/null +++ b/rolluptool/src/classes/fflib_SObjectSelector.cls @@ -0,0 +1,398 @@ +/** + * Copyright (c), FinancialForce.com, inc + * 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 FinancialForce.com, inc 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. +**/ + +/** + * Class providing common database query support for abstracting and encapsulating query logic + **/ +public abstract with sharing class fflib_SObjectSelector + implements fflib_ISObjectSelector +{ + /** + * This overrides the Multi Currency handling, preventing it from injecting the CurrencyIsoCode fie ld for certain System objects that don't ever support it + **/ + private static Set STANDARD_WITHOUT_CURRENCYISO = new Set { AsyncApexJob.SObjectType.getDescribe().getName() + , ApexClass.SObjectType.getDescribe().getName() + , Attachment.SObjectType.getDescribe().getName() + , RecordType.SObjectType.getDescribe().getName() }; + + /** + * Should this selector automatically include the FieldSet fields when building queries? + **/ + private Boolean m_includeFieldSetFields; + + /** + * Enforce FLS Security + **/ + private Boolean m_enforceFLS; + + /** + * Enforce CRUD Security + **/ + private Boolean m_enforceCRUD; + + /** + * Order by field + **/ + private String m_orderBy; + + /** + * Describe helper + **/ + private fflib_SObjectDescribe describeWrapper { + get { + if(describeWrapper == null) + describeWrapper = fflib_SObjectDescribe.getDescribe(getSObjectType()); + return describeWrapper; + } + set; + } + + /** + * Implement this method to inform the base class of the SObject (custom or standard) to be queried + **/ + abstract Schema.SObjectType getSObjectType(); + + /** + * Implement this method to inform the base class of the common fields to be queried or listed by the base class methods + **/ + abstract List getSObjectFieldList(); + + /** + * Constructs the Selector, defaults to not including any FieldSet fields automatically + **/ + public fflib_SObjectSelector() + { + this(false); + } + + /** + * Constructs the Selector + * + * @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well + **/ + public fflib_SObjectSelector(Boolean includeFieldSetFields) + { + this(includeFieldSetFields, true, false); + } + + /** + * Constructs the Selector + * + * @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well + **/ + public fflib_SObjectSelector(Boolean includeFieldSetFields, Boolean enforceCRUD, Boolean enforceFLS) + { + m_includeFieldSetFields = includeFieldSetFields; + m_enforceCRUD = enforceCRUD; + m_enforceFLS = enforceFLS; + } + + /** + * Override this method to provide a list of Fieldsets that can optionally drive inclusion of additional fields in the base queries + **/ + public virtual List getSObjectFieldSetList() + { + return null; + } + + /** + * Override this method to control the default ordering of records returned by the base queries, + * defaults to the name field of the object or CreatedDate if there is none + **/ + public virtual String getOrderBy() + { + if(m_orderBy == null) { + m_orderBy = 'CreatedDate'; + if(describeWrapper.getNameField() != null) { + m_orderBy = describeWrapper.getNameField().getDescribe().getName(); + } + } + return m_orderBy; + } + + /** + * Returns True if this Selector instance has been instructed by the caller to include Field Set fields + **/ + public Boolean isIncludeFieldSetFields() + { + return m_includeFieldSetFields; + } + + /** + * Returns True if this Selector is enforcing FLS + **/ + public Boolean isEnforcingFLS() + { + return m_enforceFLS; + } + + /** + * Returns True if this Selector is enforcing CRUD Security + **/ + public Boolean isEnforcingCRUD() + { + return m_enforceCRUD; + } + + /** + * Provides access to the builder containing the list of fields base queries are using, this is demand + * created if one has not already been defined via setFieldListBuilder + * + * @depricated See newQueryFactory + **/ + public fflib_StringBuilder.FieldListBuilder getFieldListBuilder() + { + List sObjectFields = new List(); + for(fflib_QueryFactory.QueryField queryField : newQueryFactory().getSelectedFields()) + sObjectFields.add(queryField.getBaseField()); + return new fflib_StringBuilder.FieldListBuilder(sObjectFields); + } + + /** + * Use this method to override the default FieldListBuilder (created on demand via getFieldListBuilder) with a custom one, + * warning, this will bypass anything getSObjectFieldList or getSObjectFieldSetList returns + * + * @depricated See newQueryFactory + **/ + public void setFieldListBuilder(fflib_StringBuilder.FieldListBuilder fieldListBuilder) + { + // TODO: Consider if given the known use cases for this (dynamic selector optomisation) if it's OK to leave this as a null operation + } + + /** + * Returns in string form a comma delimted list of fields as defined via getSObjectFieldList and optionally getSObjectFieldSetList + * + * @depricated See newQueryFactory + **/ + public String getFieldListString() + { + return getFieldListBuilder().getStringValue(); + } + + /** + * Returns in string form a comma delimted list of fields as defined via getSObjectFieldList and optionally getSObjectFieldSetList + * @param relation Will prefix fields with the given relation, e.g. MyLookupField__r + * + * @depricated See newQueryFactory + **/ + public String getRelatedFieldListString(String relation) + { + return getFieldListBuilder().getStringValue(relation + '.'); + } + + /** + * Returns the string representaiton of the SObject this selector represents + **/ + public String getSObjectName() + { + return describeWrapper.getDescribe().getName(); + } + + /** + * Performs a SOQL query, + * - Selecting the fields described via getSObjectFieldsList and getSObjectFieldSetList (if included) + * - From the SObject described by getSObjectType + * - Where the Id's match those provided in the set + * - Ordered by the fields returned via getOrderBy + * @returns A list of SObject's + **/ + public List selectSObjectsById(Set idSet) + { + return Database.query(buildQuerySObjectById()); + } + + /** + * Performs a SOQL query, + * - Selecting the fields described via getSObjectFieldsList and getSObjectFieldSetList (if included) + * - From the SObject described by getSObjectType + * - Where the Id's match those provided in the set + * - Ordered by the fields returned via getOrderBy + * @returns A QueryLocator (typically for use in a Batch Apex job) + **/ + public Database.QueryLocator queryLocatorById(Set idSet) + { + return Database.getQueryLocator(buildQuerySObjectById()); + } + + /** + * Throws an exception if the SObject indicated by getSObjectType is not accessible to the current user (read access) + * + * @depricated If you utilise the newQueryFactory method this is automatically done for you (unless disabled by the selector) + **/ + public void assertIsAccessible() + { + if(!getSObjectType().getDescribe().isAccessible()) + throw new fflib_SObjectDomain.DomainException( + 'Permission to access an ' + getSObjectType().getDescribe().getName() + ' denied.'); + } + + /** + * Public acccess for the getSObjectType during Mock registration + * (adding public to the existing method broken base class API backwards compatability) + **/ + public SObjectType getSObjectType2() + { + return getSObjectType(); + } + + /** + * Public acccess for the getSObjectType during Mock registration + * (adding public to the existing method broken base class API backwards compatability) + **/ + public SObjectType sObjectType() + { + return getSObjectType(); + } + + /** + * Returns a QueryFactory configured with the Selectors object, fields, fieldsets and default order by + **/ + public fflib_QueryFactory newQueryFactory() + { + return newQueryFactory(m_enforceCRUD, m_enforceFLS, true); + } + + /** + * Returns a QueryFactory configured with the Selectors object, fields, fieldsets and default order by + **/ + public fflib_QueryFactory newQueryFactory(Boolean includeSelectorFields) + { + return newQueryFactory(m_enforceCRUD, m_enforceFLS, includeSelectorFields); + } + + /** + * Returns a QueryFactory configured with the Selectors object, fields, fieldsets and default order by + * CRUD and FLS read security will be checked if the corresponding inputs are true (overrides that defined in the selector). + **/ + public fflib_QueryFactory newQueryFactory(Boolean assertCRUD, Boolean enforceFLS, Boolean includeSelectorFields) + { + // Construct QueryFactory around the given SObject + return configureQueryFactory( + new fflib_QueryFactory(getSObjectType2()), + assertCRUD, enforceFLS, includeSelectorFields); + } + + /** + * Adds the selectors fields to the given QueryFactory using the given relationship path as a prefix + * + * // TODO: This should be consistant (ideally) with configureQueryFactory below + **/ + public void configureQueryFactoryFields(fflib_QueryFactory queryFactory, String relationshipFieldPath) + { + // Add fields from selector prefixing the relationship path + for(SObjectField field : getSObjectFieldList()) + queryFactory.selectField(relationshipFieldPath + '.' + field.getDescribe().getName()); + // Automatically select the CurrencyIsoCode for MC orgs (unless the object is a known exception to the rule) + if(Userinfo.isMultiCurrencyOrganization() && + !STANDARD_WITHOUT_CURRENCYISO.contains(getSObjectType().getDescribe().getName())) + queryFactory.selectField(relationshipFieldPath+'.CurrencyIsoCode'); + } + + /** + * Adds a subselect QueryFactory based on this selector to the given QueryFactor, returns the parentQueryFactory + **/ + public fflib_QueryFactory addQueryFactorySubselect(fflib_QueryFactory parentQueryFactory) + { + return addQueryFactorySubselect(parentQueryFactory, true); + } + + /** + * Adds a subselect QueryFactory based on this selector to the given QueryFactor + **/ + public fflib_QueryFactory addQueryFactorySubselect(fflib_QueryFactory parentQueryFactory, Boolean includeSelectorFields) + { + fflib_QueryFactory subSelectQueryFactory = + parentQueryFactory.subselectQuery(getSObjectType2()); + return configureQueryFactory( + subSelectQueryFactory, + m_enforceCRUD, + m_enforceFLS, + includeSelectorFields); + } + + /** + * Constructs the default SOQL query for this selector, see selectSObjectsById and queryLocatorById + **/ + private String buildQuerySObjectById() + { + return newQueryFactory().setCondition('id in :idSet').toSOQL(); + } + + /** + * Configures a QueryFactory instance according to the configuration of this selector + **/ + private fflib_QueryFactory configureQueryFactory(fflib_QueryFactory queryFactory, Boolean assertCRUD, Boolean enforceFLS, Boolean includeSelectorFields) + { + // CRUD and FLS security required? + if (assertCRUD) + { + try { + // Leverage QueryFactory for CRUD checking + queryFactory.assertIsAccessible(); + } catch (fflib_SecurityUtils.CrudException e) { + // Marshal exception into DomainException for backwards compatability + throw new fflib_SObjectDomain.DomainException( + 'Permission to access an ' + getSObjectType().getDescribe().getName() + ' denied.'); + } + } + queryFactory.setEnforceFLS(enforceFLS); + + // Configure the QueryFactory with the Selector fields? + if(includeSelectorFields) + { + // select the Selector fields and Fieldsets and set order + queryFactory.selectFields(new Set(getSObjectFieldList())); + List fieldSetList = getSObjectFieldSetList(); + if(m_includeFieldSetFields && fieldSetList != null) + for(Schema.FieldSet fieldSet : fieldSetList) + queryFactory.selectFieldSet(fieldSet); + + // Automatically select the CurrencyIsoCode for MC orgs (unless the object is a known exception to the rule) + if(Userinfo.isMultiCurrencyOrganization() && + !STANDARD_WITHOUT_CURRENCYISO.contains(getSObjectType().getDescribe().getName())) + queryFactory.selectField('CurrencyIsoCode'); + } + + // Parse the getOrderBy() + for(String orderBy : getOrderBy().split(',')) + { + // TODO: Handle NULLS FIRST and NULLS LAST, http://www.salesforce.com/us/developer/docs/soql_sosl/Content/sforce_api_calls_soql_select_orderby.htm + List orderByParts = orderBy.trim().split(' '); + String fieldNamePart = orderByParts[0]; + String fieldSortOrderPart = orderByParts.size() > 1 ? orderByParts[1] : null; + fflib_QueryFactory.SortOrder fieldSortOrder = fflib_QueryFactory.SortOrder.ASCENDING; + if(fieldSortOrderPart==null) + fieldSortOrder = fflib_QueryFactory.SortOrder.ASCENDING; + else if(fieldSortOrderPart.equalsIgnoreCase('DESC')) + fieldSortOrder = fflib_QueryFactory.SortOrder.DESCENDING; + else if(fieldSortOrderPart.equalsIgnoreCase('ASC')) + fieldSortOrder = fflib_QueryFactory.SortOrder.ASCENDING; + queryFactory.addOrdering(fieldNamePart, fieldSortOrder); + } + + return queryFactory; + } +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_SObjectSelector.cls-meta.xml b/rolluptool/src/classes/fflib_SObjectSelector.cls-meta.xml new file mode 100644 index 00000000..b12420ea --- /dev/null +++ b/rolluptool/src/classes/fflib_SObjectSelector.cls-meta.xml @@ -0,0 +1,5 @@ + + + 31.0 + Active + diff --git a/rolluptool/src/classes/fflib_SObjectSelectorTest.cls b/rolluptool/src/classes/fflib_SObjectSelectorTest.cls new file mode 100644 index 00000000..251dc8f7 --- /dev/null +++ b/rolluptool/src/classes/fflib_SObjectSelectorTest.cls @@ -0,0 +1,261 @@ +/** + * Copyright (c), FinancialForce.com, inc + * 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 FinancialForce.com, inc 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 with sharing class fflib_SObjectSelectorTest +{ + + static testMethod void testGetFieldListString() + { + Testfflib_SObjectSelector selector = new Testfflib_SObjectSelector(); + String fieldListString = selector.getFieldListString(); + assertFieldListString(fieldListString, null); + String relatedFieldListString = selector.getRelatedFieldListString('myprefix'); + assertFieldListString(relatedFieldListString, 'myprefix'); + } + + static testMethod void testGetSObjectName() + { + Testfflib_SObjectSelector selector = new Testfflib_SObjectSelector(); + system.assertEquals(null, selector.getSObjectFieldSetList()); + system.assertEquals('Account',selector.getSObjectName()); + } + + static testMethod void testSelectSObjectsById() + { + // Inserting in reverse order so that we can test the order by of select + List accountList = new List { + new Account(Name='TestAccount2',AccountNumber='A2',AnnualRevenue=12345.67), + new Account(Name='TestAccount1',AccountNumber='A1',AnnualRevenue=76543.21) }; + insert accountList; + Set idSet = new Set(); + for(Account item : accountList) + idSet.add(item.Id); + + Test.startTest(); + Testfflib_SObjectSelector selector = new Testfflib_SObjectSelector(); + List result = (List) selector.selectSObjectsById(idSet); + Test.stopTest(); + + system.assertEquals(2,result.size()); + system.assertEquals('TestAccount2',result[0].Name); + system.assertEquals('A2',result[0].AccountNumber); + system.assertEquals(12345.67,result[0].AnnualRevenue); + system.assertEquals('TestAccount1',result[1].Name); + system.assertEquals('A1',result[1].AccountNumber); + system.assertEquals(76543.21,result[1].AnnualRevenue); + } + + static testMethod void testQueryLocatorById() + { + // Inserting in reverse order so that we can test the order by of select + List accountList = new List { + new Account(Name='TestAccount2',AccountNumber='A2',AnnualRevenue=12345.67), + new Account(Name='TestAccount1',AccountNumber='A1',AnnualRevenue=76543.21) }; + insert accountList; + Set idSet = new Set(); + for(Account item : accountList) + idSet.add(item.Id); + + Test.startTest(); + Testfflib_SObjectSelector selector = new Testfflib_SObjectSelector(); + Database.QueryLocator result = selector.queryLocatorById(idSet); + System.Iterator iteratorResult = result.iterator(); + Test.stopTest(); + + System.assert(true, iteratorResult.hasNext()); + Account account = (Account) iteratorResult.next(); + system.assertEquals('TestAccount2',account.Name); + system.assertEquals('A2',account.AccountNumber); + system.assertEquals(12345.67,account.AnnualRevenue); + System.assert(true, iteratorResult.hasNext()); + account = (Account) iteratorResult.next(); + system.assertEquals('TestAccount1',account.Name); + system.assertEquals('A1',account.AccountNumber); + system.assertEquals(76543.21,account.AnnualRevenue); + System.assertEquals(false, iteratorResult.hasNext()); + } + + static testMethod void testAssertIsAccessible() + { + List accountList = new List { + new Account(Name='TestAccount2',AccountNumber='A2',AnnualRevenue=12345.67), + new Account(Name='TestAccount1',AccountNumber='A1',AnnualRevenue=76543.21) }; + insert accountList; + Set idSet = new Set(); + for(Account item : accountList) + idSet.add(item.Id); + + // Create a user which will not have access to the test object type + User testUser = createChatterExternalUser(); + if(testUser==null) + return; // Abort the test if unable to create a user with low enough acess + System.runAs(testUser) + { + Testfflib_SObjectSelector selector = new Testfflib_SObjectSelector(); + try + { + List result = (List) selector.selectSObjectsById(idSet); + System.assert(false,'Expected exception was not thrown'); + } + catch(fflib_SObjectDomain.DomainException e) + { + System.assertEquals('Permission to access an Account denied.',e.getMessage()); + } + } + } + + static testMethod void testCRUDOff() + { + List accountList = new List { + new Account(Name='TestAccount2',AccountNumber='A2',AnnualRevenue=12345.67), + new Account(Name='TestAccount1',AccountNumber='A1',AnnualRevenue=76543.21) }; + insert accountList; + Set idSet = new Set(); + for(Account item : accountList) + idSet.add(item.Id); + + // Create a user which will not have access to the test object type + User testUser = createChatterExternalUser(); + if(testUser==null) + return; // Abort the test if unable to create a user with low enough acess + System.runAs(testUser) + { + Testfflib_SObjectSelector selector = new Testfflib_SObjectSelector(false, false, false); + try + { + List result = (List) selector.selectSObjectsById(idSet); + } + catch(fflib_SObjectDomain.DomainException e) + { + System.assert(false,'Did not expect an exception to be thrown'); + } + } + } + + static testMethod void testSOQL() + { + Testfflib_SObjectSelector selector = new Testfflib_SObjectSelector(); + String soql = selector.newQueryFactory().toSOQL(); + Pattern p = Pattern.compile('SELECT (.*) FROM Account ORDER BY Name DESC NULLS FIRST , AnnualRevenue ASC NULLS FIRST '); + Matcher m = p.matcher(soql); + System.assert(m.matches(), 'Generated SOQL does not match expected pattern. Here is the generated SOQL: ' + soql); + System.assertEquals(1, m.groupCount(), 'Unexpected number of groups captured.'); + String fieldListString = m.group(1); + assertFieldListString(fieldListString, null); + } + + static testMethod void testDefaultConfig() + { + Testfflib_SObjectSelector selector = new Testfflib_SObjectSelector(); + System.assertEquals(false, selector.isEnforcingFLS()); + System.assertEquals(true, selector.isEnforcingCRUD()); + System.assertEquals(false, selector.isIncludeFieldSetFields()); + + String fieldListString = selector.getFieldListString(); + assertFieldListString(fieldListString, null); + + String relatedFieldListString = selector.getRelatedFieldListString('LookupField__r'); + assertFieldListString(relatedFieldListString, 'LookupField__r'); + + System.assertEquals('Account', selector.getSObjectName()); + System.assertEquals(Account.SObjectType, selector.getSObjectType2()); + } + + private static void assertFieldListString(String fieldListString, String prefix) { + String prefixString = (!String.isBlank(prefix)) ? prefix + '.' : ''; + List fieldList = fieldListString.split(',{1}\\s?'); + System.assertEquals(UserInfo.isMultiCurrencyOrganization() ? 5 : 4, fieldList.size()); + Set fieldSet = new Set(); + fieldSet.addAll(fieldList); + String expected = prefixString + 'AccountNumber'; + System.assert(fieldSet.contains(expected), expected + ' missing from field list string: ' + fieldListString); + expected = prefixString + 'AnnualRevenue'; + System.assert(fieldSet.contains(expected), expected + ' missing from field list string: ' + fieldListString); + expected = prefixString + 'Id'; + System.assert(fieldSet.contains(expected), expected + ' missing from field list string: ' + fieldListString); + expected = prefixString + 'Name'; + System.assert(fieldSet.contains(expected), expected + ' missing from field list string: ' + fieldListString); + if (UserInfo.isMultiCurrencyOrganization()) { + expected = prefixString + 'CurrencyIsoCode'; + System.assert(fieldSet.contains(expected), expected + ' missing from field list string: ' + fieldListString); + } + } + + private class Testfflib_SObjectSelector extends fflib_SObjectSelector + { + public Testfflib_SObjectSelector() + { + super(); + } + + public Testfflib_SObjectSelector(Boolean includeFieldSetFields, Boolean enforceCRUD, Boolean enforceFLS) + { + super(includeFieldSetFields, enforceCRUD, enforceFLS); + } + + public List getSObjectFieldList() + { + return new List { + Account.Name, + Account.Id, + Account.AccountNumber, + Account.AnnualRevenue + }; + } + + public Schema.SObjectType getSObjectType() + { + return Account.sObjectType; + } + + public override String getOrderBy() + { + return 'Name DESC, AnnualRevenue ASC'; + } + } + + /** + * Create test user + **/ + private static User createChatterExternalUser() + { + // Can only proceed with test if we have a suitable profile - Chatter External license has no access to Opportunity + List testProfiles = [Select Id From Profile where UserLicense.Name='Chatter External' limit 1]; + if(testProfiles.size()!=1) + return null; + + // Can only proceed with test if we can successfully insert a test user + String testUsername = System.now().format('yyyyMMddhhmmss') + '@testorg.com'; + User testUser = new User(Alias = 'test1', Email='testuser1@testorg.com', EmailEncodingKey='UTF-8', LastName='Testing', LanguageLocaleKey='en_US', LocaleSidKey='en_US', ProfileId = testProfiles[0].Id, TimeZoneSidKey='America/Los_Angeles', UserName=testUsername); + try { + insert testUser; + } catch (Exception e) { + return null; + } + return testUser; + } +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_SObjectSelectorTest.cls-meta.xml b/rolluptool/src/classes/fflib_SObjectSelectorTest.cls-meta.xml new file mode 100644 index 00000000..b12420ea --- /dev/null +++ b/rolluptool/src/classes/fflib_SObjectSelectorTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 31.0 + Active + diff --git a/rolluptool/src/classes/fflib_SObjectUnitOfWork.cls b/rolluptool/src/classes/fflib_SObjectUnitOfWork.cls new file mode 100644 index 00000000..058bef9a --- /dev/null +++ b/rolluptool/src/classes/fflib_SObjectUnitOfWork.cls @@ -0,0 +1,350 @@ +/** + * Copyright (c), FinancialForce.com, inc + * 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 FinancialForce.com, inc 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. +**/ + +/** + * Provides an implementation of the Enterprise Application Architecture Unit Of Work, as defined by Martin Fowler + * http://martinfowler.com/eaaCatalog/unitOfWork.html + * + * "When you're pulling data in and out of a database, it's important to keep track of what you've changed; otherwise, + * that data won't be written back into the database. Similarly you have to insert new objects you create and + * remove any objects you delete." + * + * "You can change the database with each change to your object model, but this can lead to lots of very small database calls, + * which ends up being very slow. Furthermore it requires you to have a transaction open for the whole interaction, which is + * impractical if you have a business transaction that spans multiple requests. The situation is even worse if you need to + * keep track of the objects you've read so you can avoid inconsistent reads." + * + * "A Unit of Work keeps track of everything you do during a business transaction that can affect the database. When you're done, + * it figures out everything that needs to be done to alter the database as a result of your work." + * + * In an Apex context this pattern provides the following specific benifits + * - Applies bulkfication to DML operations, insert, update and delete + * - Manages a business transaction around the work and ensures a rollback occurs (even when exceptions are later handled by the caller) + * - Honours dependency rules between records and updates dependent relationships automatically during the commit + * + * Please refer to the testMethod's in this class for example usage + * + * TODO: Need to complete the 100% coverage by covering parameter exceptions in tests + * TODO: Need to add some more test methods for more complex use cases and some unexpected (e.g. registerDirty and then registerDeleted) + * + **/ +public virtual class fflib_SObjectUnitOfWork + implements fflib_ISObjectUnitOfWork +{ + private List m_sObjectTypes = new List(); + + private Map> m_newListByType = new Map>(); + + private Map> m_dirtyMapByType = new Map>(); + + private Map> m_deletedMapByType = new Map>(); + + private Map m_relationships = new Map(); + + private List m_workList = new List(); + + private SendEmailWork m_emailWork = new SendEmailWork(); + + private IDML m_dml; + + /** + * Interface describes work to be performed during the commitWork method + **/ + public interface IDoWork + { + void doWork(); + } + + public interface IDML + { + void dmlInsert(List objList); + void dmlUpdate(List objList); + void dmlDelete(List objList); + } + + public class SimpleDML implements IDML + { + public void dmlInsert(List objList){ + insert objList; + } + public void dmlUpdate(List objList){ + update objList; + } + public void dmlDelete(List objList){ + delete objList; + } + } + /** + * Constructs a new UnitOfWork to support work against the given object list + * + * @param sObjectList A list of objects given in dependency order (least dependent first) + */ + public fflib_SObjectUnitOfWork(List sObjectTypes) + { + this(sObjectTypes,new SimpleDML()); + } + + + public fflib_SObjectUnitOfWork(List sObjectTypes, IDML dml) + { + m_sObjectTypes = sObjectTypes.clone(); + + for(Schema.SObjectType sObjectType : m_sObjectTypes) + { + m_newListByType.put(sObjectType.getDescribe().getName(), new List()); + m_dirtyMapByType.put(sObjectType.getDescribe().getName(), new Map()); + m_deletedMapByType.put(sObjectType.getDescribe().getName(), new Map()); + m_relationships.put(sObjectType.getDescribe().getName(), new Relationships()); + } + + m_workList.add(m_emailWork); + + m_dml = dml; + } + + /** + * Register a generic peace of work to be invoked during the commitWork phase + **/ + public void registerWork(IDoWork work) + { + m_workList.add(work); + } + + /** + * Registers the given email to be sent during the commitWork + **/ + public void registerEmail(Messaging.Email email) + { + m_emailWork.registerEmail(email); + } + + /** + * Register a newly created SObject instance to be inserted when commitWork is called + * + * @param record A newly created SObject instance to be inserted during commitWork + **/ + public void registerNew(SObject record) + { + registerNew(record, null, null); + } + + /** + * Register a list of newly created SObject instances to be inserted when commitWork is called + * + * @param records A list of newly created SObject instances to be inserted during commitWork + **/ + public void registerNew(List records) + { + for(SObject record : records) + { + registerNew(record, null, null); + } + } + + /** + * Register a newly created SObject instance to be inserted when commitWork is called, + * you may also provide a reference to the parent record instance (should also be registered as new separatly) + * + * @param record A newly created SObject instance to be inserted during commitWork + * @param relatedToParentField A SObjectField reference to the child field that associates the child record with its parent + * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separatly) + **/ + public void registerNew(SObject record, Schema.sObjectField relatedToParentField, SObject relatedToParentRecord) + { + if(record.Id != null) + throw new UnitOfWorkException('Only new records can be registered as new'); + String sObjectType = record.getSObjectType().getDescribe().getName(); + if(!m_newListByType.containsKey(sObjectType)) + throw new UnitOfWorkException(String.format('SObject type {0} is not supported by this unit of work', new String[] { sObjectType })); + m_newListByType.get(sObjectType).add(record); + if(relatedToParentRecord!=null && relatedToParentField!=null) + registerRelationship(record, relatedToParentField, relatedToParentRecord); + } + + /** + * Register a relationship between two records that have yet to be inserted to the database. This information will be + * used during the commitWork phase to make the references only when related records have been inserted to the database. + * + * @param record An existing or newly created record + * @param relatedToField A SObjectField referene to the lookup field that relates the two records together + * @param relatedTo A SOBject instance (yet to be commited to the database) + */ + public void registerRelationship(SObject record, Schema.sObjectField relatedToField, SObject relatedTo) + { + String sObjectType = record.getSObjectType().getDescribe().getName(); + if(!m_newListByType.containsKey(sObjectType)) + throw new UnitOfWorkException(String.format('SObject type {0} is not supported by this unit of work', new String[] { sObjectType })); + m_relationships.get(sObjectType).add(record, relatedToField, relatedTo); + } + + /** + * Register an existing record to be updated during the commitWork method + * + * @param record An existing record + **/ + public void registerDirty(SObject record) + { + if(record.Id == null) + throw new UnitOfWorkException('New records cannot be registered as dirty'); + String sObjectType = record.getSObjectType().getDescribe().getName(); + if(!m_dirtyMapByType.containsKey(sObjectType)) + throw new UnitOfWorkException(String.format('SObject type {0} is not supported by this unit of work', new String[] { sObjectType })); + m_dirtyMapByType.get(sObjectType).put(record.Id, record); + } + + /** + * Register a list of existing records to be updated during the commitWork method + * + * @param records A list of existing records + **/ + public void registerDirty(List records) + { + for(SObject record : records) + { + this.registerDirty(record); + } + } + + /** + * Register an existing record to be deleted during the commitWork method + * + * @param record An existing record + **/ + public void registerDeleted(SObject record) + { + if(record.Id == null) + throw new UnitOfWorkException('New records cannot be registered for deletion'); + String sObjectType = record.getSObjectType().getDescribe().getName(); + if(!m_deletedMapByType.containsKey(sObjectType)) + throw new UnitOfWorkException(String.format('SObject type {0} is not supported by this unit of work', new String[] { sObjectType })); + m_deletedMapByType.get(sObjectType).put(record.Id, record); + } + + /** + * Register a list of existing records to be deleted during the commitWork method + * + * @param records A list of existing records + **/ + public void registerDeleted(List records) + { + for(SObject record : records) + { + this.registerDeleted(record); + } + } + + /** + * Takes all the work that has been registered with the UnitOfWork and commits it to the database + **/ + public void commitWork() + { + // Wrap the work in its own transaction + Savepoint sp = Database.setSavePoint(); + try + { + // Insert by type + for(Schema.SObjectType sObjectType : m_sObjectTypes) + { + m_relationships.get(sObjectType.getDescribe().getName()).resolve(); + m_dml.dmlInsert(m_newListByType.get(sObjectType.getDescribe().getName())); + } + // Update by type + for(Schema.SObjectType sObjectType : m_sObjectTypes) + m_dml.dmlUpdate(m_dirtyMapByType.get(sObjectType.getDescribe().getName()).values()); + // Delete by type (in reverse dependency order) + Integer objectIdx = m_sObjectTypes.size() - 1; + while(objectIdx>=0) + m_dml.dmlDelete(m_deletedMapByType.get(m_sObjectTypes[objectIdx--].getDescribe().getName()).values()); + // Generic work + for(IDoWork work : m_workList) + work.doWork(); + } + catch (Exception e) + { + // Rollback + Database.rollback(sp); + // Throw exception on to caller + throw e; + } + } + + private class Relationships + { + private List m_relationships = new List(); + + public void resolve() + { + // Resolve relationships + for(Relationship relationship : m_relationships) + relationship.Record.put(relationship.RelatedToField, relationship.RelatedTo.Id); + } + + public void add(SObject record, Schema.sObjectField relatedToField, SObject relatedTo) + { + // Relationship to resolve + Relationship relationship = new Relationship(); + relationship.Record = record; + relationship.RelatedToField = relatedToField; + relationship.RelatedTo = relatedTo; + m_relationships.add(relationship); + } + } + + private class Relationship + { + public SObject Record; + public Schema.sObjectField RelatedToField; + public SObject RelatedTo; + } + + /** + * UnitOfWork Exception + **/ + public class UnitOfWorkException extends Exception {} + + /** + * Internal implementation of Messaging.sendEmail, see outer class registerEmail method + **/ + private class SendEmailWork implements IDoWork + { + private List emails; + + public SendEmailWork() + { + this.emails = new List(); + } + + public void registerEmail(Messaging.Email email) + { + this.emails.add(email); + } + + public void doWork() + { + if(emails.size() > 0) Messaging.sendEmail(emails); + } + } +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_SObjectUnitOfWork.cls-meta.xml b/rolluptool/src/classes/fflib_SObjectUnitOfWork.cls-meta.xml new file mode 100644 index 00000000..b12420ea --- /dev/null +++ b/rolluptool/src/classes/fflib_SObjectUnitOfWork.cls-meta.xml @@ -0,0 +1,5 @@ + + + 31.0 + Active + diff --git a/rolluptool/src/classes/fflib_SObjectUnitOfWorkTest.cls b/rolluptool/src/classes/fflib_SObjectUnitOfWorkTest.cls new file mode 100644 index 00000000..1f569797 --- /dev/null +++ b/rolluptool/src/classes/fflib_SObjectUnitOfWorkTest.cls @@ -0,0 +1,167 @@ +/** + * Copyright (c), FinancialForce.com, inc + * 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 FinancialForce.com, inc 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 with sharing class fflib_SObjectUnitOfWorkTest +{ + // SObjects (in order of dependency) used by UnitOfWork in tests bellow + private static List MY_SOBJECTS = + new Schema.SObjectType[] { + Product2.SObjectType, + PricebookEntry.SObjectType, + Opportunity.SObjectType, + OpportunityLineItem.SObjectType }; + + @isTest + private static void testUnitOfWorkNewDirtyDelete() + { + // Insert Opporunities with UnitOfWork + { + fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(MY_SOBJECTS); + for(Integer o=0; o<10; o++) + { + Opportunity opp = new Opportunity(); + opp.Name = 'UoW Test Name ' + o; + opp.StageName = 'Open'; + opp.CloseDate = System.today(); + uow.registerNew(new List{opp}); + for(Integer i=0; i{product}); + PricebookEntry pbe = new PricebookEntry(); + pbe.UnitPrice = 10; + pbe.IsActive = true; + pbe.UseStandardPrice = false; + pbe.Pricebook2Id = Test.getStandardPricebookId(); + uow.registerNew(pbe, PricebookEntry.Product2Id, product); + OpportunityLineItem oppLineItem = new OpportunityLineItem(); + oppLineItem.Quantity = 1; + oppLineItem.TotalPrice = 10; + uow.registerRelationship(oppLineItem, OpportunityLineItem.PricebookEntryId, pbe); + uow.registerNew(oppLineItem, OpportunityLineItem.OpportunityId, opp); + } + } + uow.commitWork(); + } + + // Assert Results + assertResults('UoW'); + // TODO: Need to re-instate this check with a better approach, as it is not possible when + // product triggers contribute to DML (e.g. in sample app Opportunity trigger) + // System.assertEquals(5 /* Oddly a setSavePoint consumes a DML */, Limits.getDmlStatements()); + + // Records to update + List opps = [select Id, Name, (Select Id from OpportunityLineItems) from Opportunity where Name like 'UoW Test Name %' order by Name]; + + // Update some records with UnitOfWork + { + fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(MY_SOBJECTS); + Opportunity opp = opps[0]; + opp.Name = opp.Name + ' Changed'; + uow.registerDirty(new List{opp}); + Product2 product = new Product2(); + product.Name = opp.Name + ' : New Product'; + uow.registerNew(new List{product}); + PricebookEntry pbe = new PricebookEntry(); + pbe.UnitPrice = 10; + pbe.IsActive = true; + pbe.UseStandardPrice = false; + pbe.Pricebook2Id = Test.getStandardPricebookId(); + uow.registerNew(pbe, PricebookEntry.Product2Id, product); + OpportunityLineItem newOppLineItem = new OpportunityLineItem(); + newOppLineItem.Quantity = 1; + newOppLineItem.TotalPrice = 10; + uow.registerRelationship(newOppLineItem, OpportunityLineItem.PricebookEntryId, pbe); + uow.registerNew(newOppLineItem, OpportunityLineItem.OpportunityId, opp); + OpportunityLineItem existingOppLine = opp.OpportunityLineItems[0]; + // Test that operations on the same object can be daisy chained, and the same object registered as dirty more than once + // This verifies that using a Map to back the dirty records collection prevents duplicate registration. + existingOppLine.Quantity = 2; + uow.registerDirty(new List{existingOppLine}); + existingOppLine.TotalPrice = 20; + uow.registerDirty(new List{existingOppLine}); + uow.commitWork(); + } + + // Assert Results + // TODO: Need to re-instate this check with a better approach, as it is not possible when + // product triggers contribute to DML (e.g. in sample app Opportunity trigger) + // System.assertEquals(11, Limits.getDmlStatements()); + opps = [select Id, Name, (Select Id, PricebookEntry.Product2.Name, Quantity, TotalPrice from OpportunityLineItems Order By PricebookEntry.Product2.Name) from Opportunity where Name like 'UoW Test Name %' order by Name]; + System.assertEquals(10, opps.size()); + System.assertEquals('UoW Test Name 0 Changed', opps[0].Name); + System.assertEquals(2, opps[0].OpportunityLineItems.size()); + // Verify that both fields were updated properly + System.assertEquals(2, opps[0].OpportunityLineItems[0].Quantity); + System.assertEquals(20, opps[0].OpportunityLineItems[0].TotalPrice); + System.assertEquals('UoW Test Name 0 Changed : New Product', opps[0].OpportunityLineItems[1].PricebookEntry.Product2.Name); + + // Delete some records with the UnitOfWork + { + fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(MY_SOBJECTS); + uow.registerDeleted(new List{opps[0].OpportunityLineItems[1].PricebookEntry.Product2}); // Delete PricebookEntry Product + uow.registerDeleted(new List{opps[0].OpportunityLineItems[1].PricebookEntry}); // Delete PricebookEntry + uow.registerDeleted(new List{opps[0].OpportunityLineItems[1]}); // Delete OpportunityLine Item + // Register the same deletions more than once. + // This verifies that using a Map to back the deleted records collection prevents duplicate registration. + uow.registerDeleted(new List{opps[0].OpportunityLineItems[1].PricebookEntry.Product2}); // Delete PricebookEntry Product + uow.registerDeleted(new List{opps[0].OpportunityLineItems[1].PricebookEntry}); // Delete PricebookEntry + uow.registerDeleted(new List{opps[0].OpportunityLineItems[1]}); // Delete OpportunityLine Item + uow.commitWork(); + } + + // Assert Results + // TODO: Need to re-instate this check with a better approach, as it is not possible when + // product triggers contribute to DML (e.g. in sample app Opportunity trigger) + // System.assertEquals(15, Limits.getDmlStatements()); + opps = [select Id, Name, (Select Id, PricebookEntry.Product2.Name, Quantity from OpportunityLineItems Order By PricebookEntry.Product2.Name) from Opportunity where Name like 'UoW Test Name %' order by Name]; + List prods = [Select Id from Product2 where Name = 'UoW Test Name 0 Changed : New Product']; + System.assertEquals(10, opps.size()); + System.assertEquals('UoW Test Name 0 Changed', opps[0].Name); + System.assertEquals(1, opps[0].OpportunityLineItems.size()); // Should have deleted OpportunityLineItem added above + System.assertEquals(0, prods.size()); // Should have deleted Product added above + } + + private static void assertResults(String prefix) + { + // Standard Assertions on tests data inserted by tests + String filter = prefix + ' Test Name %'; + List opps = [select Id, Name, (Select Id from OpportunityLineItems) from Opportunity where Name like :filter order by Name]; + System.assertEquals(10, opps.size()); + System.assertEquals(1, opps[0].OpportunityLineItems.size()); + System.assertEquals(2, opps[1].OpportunityLineItems.size()); + System.assertEquals(3, opps[2].OpportunityLineItems.size()); + System.assertEquals(4, opps[3].OpportunityLineItems.size()); + System.assertEquals(5, opps[4].OpportunityLineItems.size()); + System.assertEquals(6, opps[5].OpportunityLineItems.size()); + System.assertEquals(7, opps[6].OpportunityLineItems.size()); + System.assertEquals(8, opps[7].OpportunityLineItems.size()); + System.assertEquals(9, opps[8].OpportunityLineItems.size()); + System.assertEquals(10, opps[9].OpportunityLineItems.size()); + } +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_SObjectUnitOfWorkTest.cls-meta.xml b/rolluptool/src/classes/fflib_SObjectUnitOfWorkTest.cls-meta.xml new file mode 100644 index 00000000..b12420ea --- /dev/null +++ b/rolluptool/src/classes/fflib_SObjectUnitOfWorkTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 31.0 + Active + diff --git a/rolluptool/src/classes/fflib_SecurityUtils.cls b/rolluptool/src/classes/fflib_SecurityUtils.cls new file mode 100644 index 00000000..2aa075f7 --- /dev/null +++ b/rolluptool/src/classes/fflib_SecurityUtils.cls @@ -0,0 +1,348 @@ +/** + * Copyright (c) 2014, FinancialForce.com, inc + * 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 FinancialForce.com, inc 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. +**/ + +/** + * Utility class for checking FLS/CRUD. NOTE: all "check" methods will throw a SecurityException (or subclass) if the + * user does not have the proper security granted. + **/ +public class fflib_SecurityUtils +{ + @testVisible + private Enum OperationType { CREATE, READ, MODIFY, DEL } //UPDATE and DELETE are reserved words + + /** + * SecurityException is never be thrown directly by fflib_SecurityUtils, instead all + * forms of CRUD and FLD violations throw subclasses of it. It is provided as a conveneience + * in the event you wish to handle CRUD and FLS violations the same way (e.g. die and display an error) + **/ + public virtual class SecurityException extends Exception { + protected OperationType m_operation; + protected Schema.SObjectType m_objectType; + } + + /** + * CrudException represents a running user's lack of read/create/update/delete access at a profile (or permission set) + * level. Sharing and field level security issues will never cause this. + **/ + public class CrudException extends SecurityException{ + + private CrudException(OperationType operation, Schema.SObjectType objectType){ + this.m_operation = operation; + this.m_objectType = objectType; + if(operation == OperationType.CREATE) + this.setMessage(System.Label.fflib_security_error_object_not_insertable); + else if(operation == OperationType.READ) + this.setMessage(System.Label.fflib_security_error_object_not_readable); + else if(operation == OperationType.MODIFY) + this.setMessage(System.Label.fflib_security_error_object_not_updateable); + else if(operation == OperationType.DEL) + this.setMessage(System.Label.fflib_security_error_object_not_deletable); + + this.setMessage( + String.format( + this.getMessage(), + new List{ + objectType.getDescribe().getName() + } + ) + ); + } + } + /** + * FlsException represents a running user's lack of field level security to a specific field at a profile (or permission set) level + * Sharing and CRUD security issues will never cause this to be thrown. + **/ + public class FlsException extends SecurityException{ + private Schema.SObjectField m_fieldToken; + + private FlsException(OperationType operation, Schema.SObjectType objectType, Schema.SObjectField fieldToken){ + this.m_operation = operation; + this.m_objectType = objectType; + this.m_fieldToken = fieldToken; + if(operation == OperationType.CREATE) + this.setMessage(System.Label.fflib_security_error_field_not_insertable); + else if(operation == OperationType.READ) + this.setMessage(System.Label.fflib_security_error_field_not_readable); + else if(operation == OperationType.MODIFY) + this.setMessage(System.Label.fflib_security_error_field_not_updateable); + + this.setMessage( + String.format( + this.getMessage(), + new List{ + objectType.getDescribe().getName(), + fieldToken.getDescribe().getName() + } + ) + ); + } + } + + /** + * If set to true all check methods will always return void, and never throw exceptions. + * This should really only be set to true if an app-wide setting to disable in-apex + * FLS and CRUD checks exists and is enabled. + * Per security best practices setting BYPASS should be an a opt-in, and not the default behavior. + **/ + public static boolean BYPASS_INTERNAL_FLS_AND_CRUD = false; + + /** + * Check{Insert,Read,Update} methods check both FLS and CRUD + **/ + + /** + * Checks both insert FLS and CRUD for the specified object type and fields. + * @exception FlsException if the running user does not have insert rights to any fields in {@code fieldNames}. + * @exception CrudException if the running user does not have insert rights to {@code objType} + **/ + public static void checkInsert(SObjectType objType, List fieldNames) + { + checkObjectIsInsertable(objType); + for (String fieldName : fieldNames) + { + checkFieldIsInsertable(objType, fieldName); + } + } + + /** + * Identical to {@link #checkInsert(SObjectType,List)}, except with SObjectField instead of String field references. + * @exception FlsException if the running user does not have insert rights to any fields in {@code fieldTokens}. + * @exception CrudException if the running user does not have insert rights to {@code objType} + **/ + public static void checkInsert(SObjectType objType, List fieldTokens) + { + checkObjectIsInsertable(objType); + for (SObjectField fieldToken : fieldTokens) + { + checkFieldIsInsertable(objType, fieldToken); + } + } + + /** + * Checks both read FLS and CRUD for the specified object type and fields. + * @exception FlsException if the running user does not have read rights to any fields in {@code fieldNames}. + * @exception CrudException if the running user does not have read rights to {@code objType} + **/ + public static void checkRead(SObjectType objType, List fieldNames) + { + checkObjectIsReadable(objType); + for (String fieldName : fieldNames) + { + checkFieldIsReadable(objType, fieldName); + } + } + + /** + * Identical to {@link #checkRead(SObjectType,List)}, except with SObjectField instead of String field references. + * @exception FlsException if the running user does not have read rights to any fields in {@code fieldTokens}. + * @exception CrudException if the running user does not have read rights to {@code objType} + **/ + public static void checkRead(SObjectType objType, List fieldTokens) + { + checkObjectIsReadable(objType); + for (SObjectField fieldToken : fieldTokens) + { + checkFieldIsReadable(objType, fieldToken); + } + } + + /** + * Checks both update FLS and CRUD for the specified object type and fields. + * @exception FlsException if the running user does not have update rights to any fields in {@code fieldNames}. + * @exception CrudException if the running user does not have update rights to {@code objType} + **/ + public static void checkUpdate(SObjectType objType, List fieldNames) + { + checkObjectIsUpdateable(objType); + for (String fieldName : fieldNames) + { + checkFieldIsUpdateable(objType, fieldName); + } + } + + /** + * Identical to {@link #checkUpdate(SObjectType,List)}, except with SObjectField instead of String field references. + * @exception FlsException if the running user does not have update rights to any fields in {@code fieldTokens}. + * @exception CrudException if the running user does not have update rights to {@code objType} + **/ + public static void checkUpdate(SObjectType objType, List fieldTokens) + { + checkObjectIsUpdateable(objType); + for (SObjectField fieldToken : fieldTokens) + { + checkFieldIsUpdateable(objType, fieldToken); + } + } + + /** + * CheckFieldIs* method check only FLS + **/ + + /** + * Checks insert field level security only (no CRUD) for the specified fields on {@code objType} + * @exception FlsException if the running user does not have insert rights to the {@code fieldName} field. + **/ + public static void checkFieldIsInsertable(SObjectType objType, String fieldName) + { + checkFieldIsInsertable(objType, fflib_SObjectDescribe.getDescribe(objType).getField(fieldName)); + } + + /** + * Identical to {@link #checkFieldIsInsertable(SObjectType,String)}, except with SObjectField instead of String field reference. + * @exception FlsException if the running user does not have insert rights to the {@code fieldName} field. + **/ + public static void checkFieldIsInsertable(SObjectType objType, SObjectField fieldToken) + { + checkFieldIsInsertable(objType, fieldToken.getDescribe()); + } + + /** + * Identical to {@link #checkFieldIsInsertable(SObjectType,String)}, except with DescribeFieldResult instead of String field reference. + * @exception FlsException if the running user does not have insert rights to the {@code fieldName} field. + **/ + public static void checkFieldIsInsertable(SObjectType objType, DescribeFieldResult fieldDescribe) + { + if (BYPASS_INTERNAL_FLS_AND_CRUD) + return; + if (!fieldDescribe.isCreateable()) + throw new FlsException(OperationType.CREATE, objType, fieldDescribe.getSObjectField()); + } + + /** + * Checks read field level security only (no CRUD) for the specified fields on {@code objType} + * @exception FlsException if the running user does not have read rights to the {@code fieldName} field. + **/ + public static void checkFieldIsReadable(SObjectType objType, String fieldName) + { + checkFieldIsReadable(objType, fflib_SObjectDescribe.getDescribe(objType).getField(fieldName)); + } + + /** + * Identical to {@link #checkFieldIsReadable(SObjectType,String)}, except with SObjectField instead of String field reference. + * @exception FlsException if the running user does not have read rights to the {@code fieldName} field. + **/ + public static void checkFieldIsReadable(SObjectType objType, SObjectField fieldToken) + { + checkFieldIsReadable(objType, fieldToken.getDescribe()); + } + + /** + * Identical to {@link #checkFieldIsReadable(SObjectType,String)}, except with DescribeFieldResult instead of String field reference. + * @exception FlsException if the running user does not have read rights to the {@code fieldName} field. + **/ + public static void checkFieldIsReadable(SObjectType objType, DescribeFieldResult fieldDescribe) + { + if (BYPASS_INTERNAL_FLS_AND_CRUD) + return; + if (!fieldDescribe.isAccessible()) + throw new FlsException(OperationType.READ, objType, fieldDescribe.getSObjectField()); + } + + + /** + * Checks update field level security only (no CRUD) for the specified fields on {@code objType} + * @exception FlsException if the running user does not have update rights to the {@code fieldName} field. + **/ + public static void checkFieldIsUpdateable(SObjectType objType, String fieldName) + { + checkFieldIsUpdateable(objType, fflib_SObjectDescribe.getDescribe(objType).getField(fieldName)); + } + + /** + * Identical to {@link #checkFieldIsUpdateable(SObjectType,String)}, except with SObjectField instead of String field reference. + * @exception FlsException if the running user does not have update rights to the {@code fieldName} field. + **/ + public static void checkFieldIsUpdateable(SObjectType objType, SObjectField fieldToken) + { + checkFieldIsUpdateable(objType, fieldToken.getDescribe()); + } + + /** + * Identical to {@link #checkFieldIsUpdateable(SObjectType,String)}, except with DescribeFieldResult instead of String field reference. + * @exception FlsException if the running user does not have update rights to the {@code fieldName} field. + **/ + public static void checkFieldIsUpdateable(SObjectType objType, DescribeFieldResult fieldDescribe) + { + if (BYPASS_INTERNAL_FLS_AND_CRUD) + return; + if (!fieldDescribe.isUpdateable()) + throw new FlsException(OperationType.MODIFY, objType, fieldDescribe.getSObjectField()); + } + + /** + * CheckObjectIs* methods check only CRUD + **/ + + /** + * Checks insert CRUD for the specified object type. + * @exception CrudException if the running uder does not have insert rights to the {@code objType} SObject. + **/ + public static void checkObjectIsInsertable(SObjectType objType) + { + if (BYPASS_INTERNAL_FLS_AND_CRUD) + return; + if (!objType.getDescribe().isCreateable()) + { + throw new CrudException(OperationType.CREATE, objType); + } + } + + /** + * Checks read CRUD for the specified object type. + * @exception CrudException if the running uder does not have read rights to the {@code objType} SObject. + **/ + public static void checkObjectIsReadable(SObjectType objType) + { + if (BYPASS_INTERNAL_FLS_AND_CRUD) + return; + if (!objType.getDescribe().isAccessible()) + throw new CrudException(OperationType.READ, objType); + } + + /** + * Checks update CRUD for the specified object type. + * @exception CrudException if the running uder does not have update rights to the {@code objType} SObject. + **/ + public static void checkObjectIsUpdateable(SObjectType objType) + { + if (BYPASS_INTERNAL_FLS_AND_CRUD) + return; + if (!objType.getDescribe().isUpdateable()) + throw new CrudException(OperationType.MODIFY, objType); + } + + /** + * Checks delete CRUD for the specified object type. + * @exception CrudException if the running uder does not have delete rights to the {@code objType} SObject. + **/ + public static void checkObjectIsDeletable(SObjectType objType) + { + if (BYPASS_INTERNAL_FLS_AND_CRUD) + return; + if (!objType.getDescribe().isDeletable()) + throw new CrudException(OperationType.DEL, objType); + } +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_SecurityUtils.cls-meta.xml b/rolluptool/src/classes/fflib_SecurityUtils.cls-meta.xml new file mode 100644 index 00000000..b12420ea --- /dev/null +++ b/rolluptool/src/classes/fflib_SecurityUtils.cls-meta.xml @@ -0,0 +1,5 @@ + + + 31.0 + Active + diff --git a/rolluptool/src/classes/fflib_SecurityUtilsTest.cls b/rolluptool/src/classes/fflib_SecurityUtilsTest.cls new file mode 100644 index 00000000..65d072a8 --- /dev/null +++ b/rolluptool/src/classes/fflib_SecurityUtilsTest.cls @@ -0,0 +1,267 @@ +/** + * Copyright (c) 2014, FinancialForce.com, inc + * 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 FinancialForce.com, inc 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 fflib_SecurityUtilsTest { + static User setupTestUser(String profileName){ + //username global uniqueness is still enforced in tests + //make sure we get something unique to avoid issues with parallel tests + String uniqueness = DateTime.now()+':'+Math.random(); + try{ + throw new NullPointerException(); + }catch(Exception e){ + uniqueness += e.getStackTraceString(); //includes the top level test method name without having to pass it + } + Profile p = [SELECT id, Name FROM Profile WHERE Name = :profileName]; + User result = new User( + username=UserInfo.getUserId()+'.'+uniqueness.HashCode()+'@'+UserInfo.getOrganizationId()+'.sfdcOrg', + alias = 'testExec', + email='apextests@example.com', + emailencodingkey='UTF-8', + lastname='Testing', + languagelocalekey='en_US', + localesidkey='en_US', + profileid = p.Id, + timezonesidkey='America/Los_Angeles' + ); + insert result; + return result; + } + + @isTest + static void readonly_field_access() { + User testUser = setupTestUser('Read Only'); + System.runAs(testUser){ + { + fflib_SecurityUtils.SecurityException ex; + try{ + fflib_SecurityUtils.checkFieldIsInsertable(Account.SObjectType, 'naMe'); + }catch(fflib_SecurityUtils.SecurityException e){ + ex = e; + } + System.assertNotEquals(null, ex, 'Read only profile should not be able to insert Account.Name'); + System.assert(ex instanceof fflib_SecurityUtils.FlsException, 'Expected an FlsException, got '+ex.getTypeName()); + } + { + fflib_SecurityUtils.SecurityException ex; + try{ + fflib_SecurityUtils.checkFieldIsReadable(Contact.SObjectType, 'LastNAME'); + }catch(fflib_SecurityUtils.SecurityException e){ + ex = e; + } + System.assertEquals(null, ex, 'Read only profile should be able to read Contact.LastName'); + } + { + fflib_SecurityUtils.SecurityException ex; + try{ + fflib_SecurityUtils.checkFieldIsUpdateable(Lead.SObjectType, 'cOMPANY'); + }catch(fflib_SecurityUtils.SecurityException e){ + ex = e; + } + System.assertNotEquals(null, ex, 'Read only profile should not be able to update Lead.Company'); + System.assert(ex instanceof fflib_SecurityUtils.FlsException, 'Expected an FlsException, got '+ex.getTypeName()); + } + + fflib_SecurityUtils.BYPASS_INTERNAL_FLS_AND_CRUD = true; + { //no exceptions, despite no rights + fflib_SecurityUtils.checkFieldIsInsertable(Account.SObjectType, 'naMe'); + fflib_SecurityUtils.checkFieldIsReadable(Contact.SObjectType, 'LastNAME'); + fflib_SecurityUtils.checkFieldIsUpdateable(Lead.SObjectType, 'cOMPANY'); + } + } + } + + @isTest + static void readonly_object_access() { + User testUser = setupTestUser('Read Only'); + System.runAs(testUser){ + { + fflib_SecurityUtils.SecurityException ex; + try{ + fflib_SecurityUtils.checkObjectIsInsertable(Account.SObjectType); + }catch(fflib_SecurityUtils.SecurityException e){ + ex = e; + } + System.assertNotEquals(null, ex, 'Read only profile should not be able to insert Account'); + System.assert(ex instanceof fflib_SecurityUtils.CrudException, 'Expected an CrudException, got '+ex.getTypeName()); + } + { + fflib_SecurityUtils.SecurityException ex; + try{ + fflib_SecurityUtils.checkObjectIsReadable(Contact.SObjectType); + }catch(fflib_SecurityUtils.SecurityException e){ + ex = e; + } + System.assertEquals(null, ex, 'Read only profile should be able to read Contact'); + } + { + fflib_SecurityUtils.SecurityException ex; + try{ + fflib_SecurityUtils.checkObjectIsUpdateable(Lead.SObjectType); + }catch(fflib_SecurityUtils.SecurityException e){ + ex = e; + } + System.assertNotEquals(null, ex, 'Read only profile should not be able to update Lead'); + System.assert(ex instanceof fflib_SecurityUtils.CrudException, 'Expected an CrudException, got '+ex.getTypeName()); + } + { + fflib_SecurityUtils.SecurityException ex; + try{ + fflib_SecurityUtils.checkObjectIsDeletable(Opportunity.SObjectType); + }catch(fflib_SecurityUtils.SecurityException e){ + ex = e; + } + System.assertNotEquals(null, ex, 'Read only profile should not be able to delete Opportunity'); + System.assert(ex instanceof fflib_SecurityUtils.CrudException, 'Expected an CrudException, got '+ex.getTypeName()); + } + + fflib_SecurityUtils.BYPASS_INTERNAL_FLS_AND_CRUD = true; + { //no exceptions, despite no rights + fflib_SecurityUtils.checkObjectIsInsertable(Account.SObjectType); + fflib_SecurityUtils.checkObjectIsReadable(Contact.SObjectType); + fflib_SecurityUtils.checkObjectIsUpdateable(Lead.SObjectType); + fflib_SecurityUtils.checkObjectIsDeletable(Opportunity.SObjectType); + } + } + } + + @isTest + static void readonly_objectAndField_access() { + User testUser = setupTestUser('Read Only'); + System.runAs(testUser){ + { + fflib_SecurityUtils.SecurityException ex; + try{ + fflib_SecurityUtils.checkInsert( + Account.SObjectType, + new List{ + 'Name', + 'ParentId', + 'ownerId' + } + ); + }catch(fflib_SecurityUtils.SecurityException e){ + ex = e; + } + System.assertNotEquals(null, ex, 'Read only profile should not be able to insert Account'); + System.assert(ex instanceof fflib_SecurityUtils.CrudException, 'Expected an CrudException, got '+ex.getTypeName()); + } + { + fflib_SecurityUtils.SecurityException ex; + try{ + fflib_SecurityUtils.checkRead( + Contact.SObjectType, + new List{ + 'LastName', + 'accountId', + 'ownerId' + } + ); + }catch(fflib_SecurityUtils.SecurityException e){ + ex = e; + } + System.assertEquals(null, ex, 'Read only profile should be able to read Contact'); + } + { + fflib_SecurityUtils.SecurityException ex; + try{ + fflib_SecurityUtils.checkUpdate( + Lead.SObjectType, + new List{ + 'LastName', + 'FirstNAMe', + 'cOMPANY' + } + ); + }catch(fflib_SecurityUtils.SecurityException e){ + ex = e; + } + System.assertNotEquals(null, ex, 'Read only profile should not be able to update Lead'); + System.assert(ex instanceof fflib_SecurityUtils.CrudException, 'Expected an CrudException, got '+ex.getTypeName()); + } + + fflib_SecurityUtils.BYPASS_INTERNAL_FLS_AND_CRUD = true; + { //no exceptions, despite no rights + fflib_SecurityUtils.checkInsert( + Account.SObjectType, + new List{ + 'Name', + 'Type', + 'ownerId' + } + ); + fflib_SecurityUtils.checkRead( + Contact.SObjectType, + new List{ + 'LastName', + 'accountId', + 'ownerId' + } + ); + fflib_SecurityUtils.checkUpdate( + Lead.SObjectType, + new List{ + 'LastName', + 'FirstNAMe', + 'cOMPANY' + } + ); + } + } + } + + @isTest + static void sysadmin_objectAndField_access() { + User testUser = setupTestUser('System Administrator'); + System.runAs(testUser){ + fflib_SecurityUtils.checkInsert( + Account.SObjectType, + new List{ + Account.SObjectType.fields.Name, + Account.SObjectType.fields.ParentId, + Account.SObjectType.fields.ownerId + } + ); + fflib_SecurityUtils.checkRead( + Contact.SObjectType, + new List{ + Contact.SObjectType.fields.LastName, + Contact.SObjectType.fields.accountId, + Contact.SObjectType.fields.ownerId + } + ); + fflib_SecurityUtils.checkUpdate( + Lead.SObjectType, + new List{ + Lead.SObjectType.fields.LastName, + Lead.SObjectType.fields.FirstNAMe, + Lead.SObjectType.fields.cOMPANY + } + ); + } + } + +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_SecurityUtilsTest.cls-meta.xml b/rolluptool/src/classes/fflib_SecurityUtilsTest.cls-meta.xml new file mode 100644 index 00000000..b12420ea --- /dev/null +++ b/rolluptool/src/classes/fflib_SecurityUtilsTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 31.0 + Active + diff --git a/rolluptool/src/classes/fflib_StringBuilder.cls b/rolluptool/src/classes/fflib_StringBuilder.cls new file mode 100644 index 00000000..a20bea3d --- /dev/null +++ b/rolluptool/src/classes/fflib_StringBuilder.cls @@ -0,0 +1,157 @@ +/** + * Copyright (c), FinancialForce.com, inc + * 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 FinancialForce.com, inc 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. +**/ + +/** + * Helper class, roughly based on the Java version, but subclassed to assist in a number of use cases in this library + * + * NOTE: Aspects of this where developed before recent improvements to String handling, as such could likely be enhanced at this stage. + **/ +public virtual class fflib_StringBuilder +{ + protected List buffer = new List(); + + /** + * Construct an empty StringBuilder + **/ + public fflib_StringBuilder() {} + + /** + * Construct a StringBuilder with the given values + **/ + public fflib_StringBuilder(List values) + { + add(values); + } + + /** + * Add the given values to the StringBuilder + **/ + public virtual void add(List values) + { + buffer.addAll(values); + } + + /** + * Add the given value to the StringBuilder + **/ + public virtual void add(String value) + { + buffer.add(value); + } + + public virtual override String toString() + { + return String.join(buffer, ''); + } + + /** + * Return the state of the StringBuilder + **/ + public virtual String getStringValue() + { + return toString(); + } + + /** + * Subclasses the StringBuilder to produce a comma delimited contactination of strings + **/ + public virtual with sharing class CommaDelimitedListBuilder extends fflib_StringBuilder + { + String itemPrefix = ''; + String delimiter = ','; + + public CommaDelimitedListBuilder() {} + + public CommaDelimitedListBuilder(List values) + { + super(values); + } + + public void setItemPrefix(String itemPrefix) + { + this.itemPrefix = itemPrefix; + } + + public void setDelimiter(String delimiter) + { + this.delimiter = delimiter; + } + + public String getStringValue(String itemPrefix) + { + setItemPrefix(itemPrefix); + return toString(); + } + + public override String toString() + { + return itemPrefix + String.join(buffer, delimiter + itemPrefix); + } + } + + /** + * Subclasses the StringCommaDelimitedBuilder to accept native SObjectField tokens and optional FieldSet definitions to concatinate when building queries + **/ + public virtual with sharing class FieldListBuilder extends CommaDelimitedListBuilder + { + public FieldListBuilder(List values) + { + this(values, null); + } + + public FieldListBuilder(List values, List fieldSets) + { + // Create a distinct set of fields (or field paths) to select + for(Schema.SObjectField value : values) + add(String.valueOf(value)); // Alternative to value.getDescribe().getName() + + if(fieldSets!=null) + for(Schema.Fieldset fieldSet : fieldSets) + for(Schema.FieldSetMember fieldSetMember : fieldSet.getFields()) + add(fieldSetMember.getFieldPath()); + } + } + + /** + * Subclasses the FieldListBuilder to auto sense and include when needed the CurrencyIsoCode field in the field list + **/ + public with sharing class MultiCurrencyFieldListBuilder extends FieldListBuilder + { + public MultiCurrencyFieldListBuilder(List values) + { + this(values, null); + } + + public MultiCurrencyFieldListBuilder(List values, List fieldSets) + { + super(values, fieldSets); + + // Dynamically add CurrencyIsoCode field for mult-currency organisations + if(Userinfo.isMultiCurrencyOrganization()) + add('CurrencyIsoCode'); + } + } +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_StringBuilder.cls-meta.xml b/rolluptool/src/classes/fflib_StringBuilder.cls-meta.xml new file mode 100644 index 00000000..b12420ea --- /dev/null +++ b/rolluptool/src/classes/fflib_StringBuilder.cls-meta.xml @@ -0,0 +1,5 @@ + + + 31.0 + Active + diff --git a/rolluptool/src/classes/fflib_StringBuilderTest.cls b/rolluptool/src/classes/fflib_StringBuilderTest.cls new file mode 100644 index 00000000..4b48e377 --- /dev/null +++ b/rolluptool/src/classes/fflib_StringBuilderTest.cls @@ -0,0 +1,115 @@ +/** + * Copyright (c), FinancialForce.com, inc + * 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 FinancialForce.com, inc 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 with sharing class fflib_StringBuilderTest +{ + static testMethod void testfflib_StringBuilder1() + { + fflib_StringBuilder sb = new fflib_StringBuilder(); + sb.add('this is a string'); + sb.add(new List{', which is made',' up from\r ','a number of smaller strings', '. 5 in this case!'}); + system.assertEquals(sb.getStringValue(),'this is a string, which is made up from\r a number of smaller strings. 5 in this case!'); + } + + static testMethod void testfflib_StringBuilder2() + { + fflib_StringBuilder sb = new fflib_StringBuilder(new List{'apples',' and ','pears',': stairs. '}); + sb.add('this is a string'); + sb.add(new List{', which is made',' up from\r ','a number of smaller strings', '. 5 in this case!'}); + system.assertEquals(sb.getStringValue(),'apples and pears: stairs. this is a string, which is made up from\r a number of smaller strings. 5 in this case!'); + } + + static testMethod void testCommaDelimitedBuilder1() + { + fflib_StringBuilder.CommaDelimitedListBuilder sb = new fflib_StringBuilder.CommaDelimitedListBuilder(); + sb.add('a'); + sb.add(new List{'b','c','d'}); + system.assertEquals(sb.getStringValue(),'a,b,c,d'); + } + + static testMethod void testCommaDelimitedBuilder2() + { + fflib_StringBuilder.CommaDelimitedListBuilder sb = new fflib_StringBuilder.CommaDelimitedListBuilder(new List{'x','y'}); + sb.add('a'); + sb.add(new List{'b','c','d'}); + system.assertEquals(sb.getStringValue(),'x,y,a,b,c,d'); + } + + static testMethod void testCommanDelimitedBuilderWithItemPrefix() + { + fflib_StringBuilder.CommaDelimitedListBuilder sb = new fflib_StringBuilder.CommaDelimitedListBuilder(new List{'x','y'}); + sb.add('a'); + sb.add(new List{'b','c','d'}); + system.assertEquals(sb.getStringValue('$'),'$x,$y,$a,$b,$c,$d'); + } + + static testMethod void testCommanDelimitedBuilderWithAlternativeDelimiter() + { + fflib_StringBuilder.CommaDelimitedListBuilder sb = new fflib_StringBuilder.CommaDelimitedListBuilder(new List{'x','y'}); + sb.setDelimiter(';'); + sb.add('a'); + sb.add(new List{'b','c','d'}); + system.assertEquals(sb.getStringValue(),'x;y;a;b;c;d'); + } + + static testMethod void testCommanDelimitedBuilderWithAlternativeDelimiterAndPrefix() + { + fflib_StringBuilder.CommaDelimitedListBuilder sb = new fflib_StringBuilder.CommaDelimitedListBuilder(new List{'x','y'}); + sb.setItemPrefix('#'); + sb.setDelimiter(':'); + sb.add('a'); + sb.add(new List{'b','c','d'}); + system.assertEquals(sb.getStringValue(),'#x:#y:#a:#b:#c:#d'); + } + + static testMethod void testFieldListBuilder() + { + List fields = new List { Account.Name, Account.Id, Account.AccountNumber, Account.AccountNumber, Account.AnnualRevenue }; + fflib_StringBuilder.FieldListBuilder sb = new fflib_StringBuilder.FieldListBuilder(fields); + List fieldList = sb.getStringValue().split(','); + Set fieldSet = new Set(fieldList); + system.assertEquals(4, fieldSet.size()); + system.assert(fieldSet.contains('Name')); + system.assert(fieldSet.contains('Id')); + system.assert(fieldSet.contains('AccountNumber')); + system.assert(fieldSet.contains('AnnualRevenue')); + } + + static testMethod void testMultiCurrencyFieldListBuilder() + { + List fields = new List { Account.Name, Account.Id, Account.AccountNumber, Account.AnnualRevenue }; + fflib_StringBuilder.MultiCurrencyFieldListBuilder sb = new fflib_StringBuilder.MultiCurrencyFieldListBuilder(fields); + List fieldList = sb.getStringValue().split(','); + Set fieldSet = new Set(fieldList); + system.assert(fieldSet.contains('Name')); + system.assert(fieldSet.contains('Id')); + system.assert(fieldSet.contains('AccountNumber')); + system.assert(fieldSet.contains('AnnualRevenue')); + if(UserInfo.isMultiCurrencyOrganization()) + system.assert(fieldSet.contains('CurrencyIsoCode')); + } +} \ No newline at end of file diff --git a/rolluptool/src/classes/fflib_StringBuilderTest.cls-meta.xml b/rolluptool/src/classes/fflib_StringBuilderTest.cls-meta.xml new file mode 100644 index 00000000..b12420ea --- /dev/null +++ b/rolluptool/src/classes/fflib_StringBuilderTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 31.0 + Active + diff --git a/rolluptool/src/package.xml b/rolluptool/src/package.xml index 7e40077f..23151719 100644 --- a/rolluptool/src/package.xml +++ b/rolluptool/src/package.xml @@ -1,6 +1,34 @@ + fflib_ApexMocks + fflib_ApexMocksTest + fflib_Application + fflib_IDGenerator + fflib_IDGeneratorTest + fflib_ISObjectDomain + fflib_ISObjectSelector + fflib_ISObjectUnitOfWork + fflib_MethodCountRecorder + fflib_MethodReturnValue + fflib_MethodReturnValueRecorder + fflib_Mocks + fflib_MyList + fflib_QueryFactory + fflib_QueryFactoryTest + fflib_SObjectDescribe + fflib_SObjectDescribeTest + fflib_SObjectDomain + fflib_SObjectDomainTest + fflib_SObjectMocks + fflib_SObjectSelector + fflib_SObjectSelectorTest + fflib_SObjectUnitOfWork + fflib_SObjectUnitOfWorkTest + fflib_SecurityUtils + fflib_SecurityUtilsTest + fflib_StringBuilder + fflib_StringBuilderTest ApexClassesSelector ApexTriggersSelector AsyncApexJobsSelector diff --git a/rolluptool/src/triggers/RollupSummariesTrigger.trigger b/rolluptool/src/triggers/RollupSummariesTrigger.trigger index e548da4c..aeb4b87d 100644 --- a/rolluptool/src/triggers/RollupSummariesTrigger.trigger +++ b/rolluptool/src/triggers/RollupSummariesTrigger.trigger @@ -27,5 +27,5 @@ trigger RollupSummariesTrigger on LookupRollupSummary__c (after delete, after insert, after update, before delete, before insert, before update) { - SObjectDomain.triggerHandler(RollupSummaries.class); + fflib_SObjectDomain.triggerHandler(RollupSummaries.class); } \ No newline at end of file