Ascend'16 - Load and use data from Google Contacts in EPiServer

  • Dec 11, 2016
  • EPiServer
  • ContentProvider
  • Ascend
  • |

In my previous blog, I gave a recap of my presentation in Stockholm. This is the second blog about that session. In the first blog, I explained on the hand of some requirements how we can extend the UI. Let me start by given a short summary of that blog.

 

In the example, the editors are able to manage contact items in the CMS. I've created a custom gadget where all the contact items are displayed. This is actually the starting point of the editor for managing contacts. The gadget has the same look and feel as the blocks/media gadget. I'm reusing the HierarchicalList widget that EPiServer internally uses for the blocks/media gadget. I could also create my own widget, but it's of course more efficient to use something that's already there. When editors select a contact item all the detail information is shown in a custom view. In this view editors can send a message to that contact item directly with a form. Also the location of the contact item is shown on a Google Map. I've created a class (ContactData) which inherits from IContent. There are different benefits when inheriting from IContent. For instance, it's easy to create gadgets, custom views and data is automatically stored in the database. Since I'm using a class that inherits from IContent, editors can manage contact items the same way as how they do with pages and blocks. Read the full blog here.

 

New requirements

 

In the second part of my presentation, the specification changes a bit. Instead of managing the data in EPiServer, we now want to manage the contact items in Google Contacts. That means we need to write an integration, but still keep in mind that we want to use that data in EPiServer. So what's changing in the current solution? First of all, we'll reuse the ContactData class. The custom view for showing the detail information of the selected contact also remains the same. We do need to create a new gadget for showing the items from Google Contacts. For the integration part, we're going to create a custom content provider. This content provider will retrieve the data via the Google People API.

 

Content provider

Default, EPiServer uses a content provider for loading and saving pages and blocks. Whenever you would like to load data from an external data source and want use that data within EPiServer, you can create a custom content provider. For instance, you could create a content provider that loads/saves data from an XML file. You need to convert that data to an object that inherits from IContent so EPiServer can work with that data internally, for example showing it in the page tree. 

 

When configuring a custom content provider there are some configuration that needs be set, let me highlight two of them. First, we can set the entry point. Whenever EPiServer needs to load the children of a content item, it will match the id of that content item with the entry points of the registered content providers. When there is a match it will use that content provider for loading the children. Beside of the entry point, we can also set the capabilities of a content provider. Possible values are: create, edit, delete, move, etc. This setting tells what a content provider can do.

 

Google Contact content provider

For the new requirements, I've created a custom content provider that will get all data via the Google People API and convert that data to an object of ContactData(IContent). To create a custom content provider you need to create a class that inherits from ContentProvider. In my GoogleContactsProvider I override two methods, LoadChildren and LoadContent.

 

The LoadChildren method is called when EPiServer tries to load the children of a content node. In this method, I first get all contact items by calling the Google People API. For each contact item, I'm creating an external identifier. The external identifier can be constructed by calling the helper method ConstructExternalIdentifier of the MappedIdentity class. The identifier is a combination of the key of the custom content provider and the unique identifier in Google Contacts. Next step is to get a MappedIdentity object via the Get method of the IdentityMappingService. This method accepts two parameters, the external identifier and a bool that indicates whether a mapped identity needs to be created in the database when it does not already exist. Mapped identities are created in the table tblMappedIdentity. Below a screenshot of the tblMappedIdentity.

 

 

 

The Provider column contains the key of the content provider, in this example the key of my google contacts content provider. The unique identifier of Google Contacts is stored in the ProviderUniqueId column. The values of the pkID and ContentGuid columns are generated by EPiServer. A MappedIdentity is based on a row of this table. The MappedIdentity contains a ContentReference property. The pkID is used as the ID and the Provider as the ProviderName property of the ContentReference. Since the ContentReference holds the key of the content provider, EPiServer knows it needs to call my custom content provider for loading the IContent (ContactData) object for that ContentReference.

 

The last step of the LoadChildren is returning a list of ContentReferences.

 

The second method I'm overriding is the LoadContent method. This method is called with a ContentReference as a parameter. EPiServer knows to call my custom content provider because the ContentReference holds the key of the content provider in the ProviderName. First I'm getting the MappedIdentity by the ContentReference via the IdentityService. The MappedIdentity contains the external identifier and on the hand of that identifier, I can request all contact information via the Google People API. I'm converting the retrieved data (Person object) to a ContactData object and return that. Below the code for the content provider.

 

public class GoogleContactsProvider : ContentProvider
        {
            public const string Key = "google-contacts-provider";
    
            protected override IList<GetChildrenReferenceResult> LoadChildrenReferencesAndTypes(ContentReference contentLink, string languageID, out bool languageSpecific)
            {
                var identityMappingService = ServiceLocator.Current.GetInstance<IdentityMappingService>();
                var googleContactsService = ServiceLocator.Current.GetInstance<GoogleContactsService>();
    
                var contacts = googleContactsService.GetContacts();
    
                languageSpecific = false;
    
                var result = contacts.Result.Select(p =>
                   new GetChildrenReferenceResult()
                   {
                       // MappedIdentity.ConstructExternalIdentifier(ProviderKey, p.ResourceName.Split('/')[1]): epi.cms.identity://google-contacts-provider/c5792122681440133761
                       ContentLink = identityMappingService.Get(MappedIdentity.ConstructExternalIdentifier(ProviderKey, p.ResourceName.Split('/')[1]), true).ContentLink,
                       ModelType = typeof(ContactData)
                   }).ToList();
                return result;
            }
    
            protected override IContent LoadContent(ContentReference contentLink, ILanguageSelector languageSelector)
            {
                // contentLink: Id: 319, ProviderName = "google-contacts-provider"
    
                var identityMappingService = ServiceLocator.Current.GetInstance<IdentityMappingService>();
                var googleContactsService = ServiceLocator.Current.GetInstance<GoogleContactsService>();
    
                // tblMappedIdentity
                var mappedIdentity = identityMappingService.Get(contentLink); 
    
                string resourceName = mappedIdentity.ExternalIdentifier.Segments[1]; // c5792122681440133761
    
                return Convert(googleContactsService.GetContact("people/" + resourceName).Result);
            }
            
            
            protected override void SetCacheSettings(ContentReference contentReference, IEnumerable<GetChildrenReferenceResult> children, CacheSettings cacheSettings)
            {
                cacheSettings.CancelCaching = true;
    
                base.SetCacheSettings(contentReference, children, cacheSettings);
            }
            
            private ContactData Convert(Person person)
            {
                if (person != null)
                {
                    var contentTypeRepository = ServiceLocator.Current.GetInstance<IContentTypeRepository>();
                    var identityMappingService = ServiceLocator.Current.GetInstance<IdentityMappingService>();
    
                    var contentFactory = ServiceLocator.Current.GetInstance<IContentFactory>();
                    ContentType type = contentTypeRepository.Load(typeof(ContactData));
    
                    ContactData contactData = contentFactory.CreateContent(type, new BuildingContext(type)
                    {
                        Parent = DataFactory.Instance.Get<ContentFolder>(EntryPoint),
                    }) as ContactData;
    
                    Uri externalId = MappedIdentity.ConstructExternalIdentifier(ProviderKey, person.ResourceName.Split('/')[1]);
                    MappedIdentity mappedContent = identityMappingService.Get(externalId, true);
    
                    contactData.ContentLink = mappedContent.ContentLink;
                    contactData.ContentGuid = mappedContent.ContentGuid;
                    contactData.Status = VersionStatus.Published;
                    contactData.IsPendingPublish = false;
                    contactData.StartPublish = DateTime.Now.Subtract(TimeSpan.FromDays(1));
    
                    var name = (person.Names != null && person.Names.FirstOrDefault() != null ? person.Names.FirstOrDefault().DisplayName : string.Empty);
                    var email = (person.EmailAddresses != null && person.EmailAddresses.FirstOrDefault() != null ? person.EmailAddresses.FirstOrDefault().Value : string.Empty);
                    var telephonenumber = (person.PhoneNumbers != null && person.PhoneNumbers.FirstOrDefault() != null ? person.PhoneNumbers.FirstOrDefault().Value : string.Empty);
                    
                    contactData.Name = name;
                    contactData.FullName = name;
                    contactData.Email = email;
                    contactData.Phonenumber = telephonenumber;
    
                    var address = (person.Addresses != null && person.Addresses.FirstOrDefault() != null ? person.Addresses.FirstOrDefault() : null);
    
                    if (address != null)
                    {
                        contactData.StreetAddress = address.StreetAddress;
                        contactData.PostalCode = address.PostalCode;
                        contactData.PoBox = address.PoBox;
                        contactData.City = address.City;
                        contactData.Region = address.Region;
                        contactData.Country = address.Country;
                    }
    
                    if (person.Organizations != null && person.Organizations.FirstOrDefault() != null)
                    {
                        var organization = person.Organizations.FirstOrDefault();
    
                        contactData.Company = organization.Name;
                        contactData.Function = organization.Title;
                    }
    
                    contactData.MakeReadOnly();
    
                    return contactData;
                }
                return null;
            }
        }

 

Registering a content provider

There are two ways for registering a content provider. This can be done in the web.config or in an initialization module. I've decided to do the last option. First I'm creating a content folder, which will be the root. Underneath that content folder, all the contact items will be displayed. Next, I'm initializing the google contacts provider with the entry point (id of content folder) and capabilities settings. Lastly, I add the provider via the IContentProviderManager. 

 

[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
        public class GoogleContactsGadgetInitialization : IInitializableModule
        {
            private ContentRootService _contentRootService;
            private IContentSecurityRepository _contentSecurityRepository;
    
            private const string GoogleContactsRootName = "Google contacts";
            private static readonly Guid GoogleContactsRootGuid = new Guid("{F551E3B6-36A0-49A3-B055-A2CA706C4117}");
    
            public static ContentReference GoogleContactsRoot;
            
            public void Initialize(InitializationEngine context)
            {
                _contentRootService = ServiceLocator.Current.GetInstance<ContentRootService>();
                _contentSecurityRepository = ServiceLocator.Current.GetInstance<IContentSecurityRepository>();
    
                InitializeGoogleContactsProvider(context);
            }
            
            public void Uninitialize(InitializationEngine context)
            {
    
            }
    
            private void InitializeGoogleContactsProvider(InitializationEngine context)
            {
                GoogleContactsRoot = CreateRootFolder(GoogleContactsRootName, GoogleContactsRootGuid);
    
                var providerValues = new NameValueCollection();
                providerValues.Add(ContentProviderElement.EntryPointString, GoogleContactsRoot.ToString());
                providerValues.Add(ContentProviderElement.CapabilitiesString, "Search");
    
                var googleContactsProvider = new GoogleContactsProvider();
                googleContactsProvider.Initialize(GoogleContactsProvider.Key, providerValues);
    
                var providerManager = context.Locate.Advanced.GetInstance<IContentProviderManager>();
                providerManager.ProviderMap.AddProvider(googleContactsProvider);
            }
            
            private ContentReference CreateRootFolder(string rootName, Guid rootGuid)
            {
                _contentRootService.Register<ContentFolder>(rootName, rootGuid, ContentReference.RootPage);
    
                var fieldRoot = _contentRootService.Get(rootName);
    
                var securityDescriptor = _contentSecurityRepository.Get(fieldRoot).CreateWritableClone() as IContentSecurityDescriptor;
    
                if (securityDescriptor != null)
                {
                    securityDescriptor.IsInherited = false;
    
                    var everyoneEntry = securityDescriptor.Entries.FirstOrDefault(e => e.Name.Equals("everyone", StringComparison.InvariantCultureIgnoreCase));
    
                    if (everyoneEntry != null)
                    {
                        securityDescriptor.RemoveEntry(everyoneEntry);
                        _contentSecurityRepository.Save(fieldRoot, securityDescriptor, SecuritySaveType.Replace);
                    }
                }
                return fieldRoot;
            }
        }

 

Gadget showing Google Contacts

I already explained in my previous blog how you can create a custom gadget. For the new requirements, I created a gadget for showing the contact items that are retrieved via the Google People API. During initialization, I'm creating a content folder which acts as the root for the contact items. I'm setting the id of the content folder in the EntryPoint configuration of my custom content provider. That means that when EPiServer tries to load the children of the content folder it will use my custom content provider (LoadChildren).

When the editor selects a contact item the custom view is displayed with all the detail information. At this moment my custom content provider (LoadContent) is called by a ContentReference to load the specific contact item. When the editor switch to the all properties view, you'll see that all properties are set to read-only. This is because the capabilities for my content provider are set to 'search' only.

 

Below the solution of the Google Contact integration solution.

 

 

 

 
You can read the previous blog here, the source code can be found on my Github account.

 

Comments