In our previous article, we introduced you to content fragments. Today, we let you roll up your sleeves and hard code your way through the intricacies of AEM. We’ll start by defining the Video and Event schema models described in the previous article and then explore brightcove AEM video sync.

Video Schema Model

/conf/site-com/settings/dam/cfm/models/video:

<!--?xml version="1.0" encoding="UTF-8"?-->
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" jcr:primarytype="cq:Template" allowedpaths="[/content/entities(/.*)?]" ranking="{Long}100">
    <jcr:content cq:scaffolding="/conf/site-com/settings/dam/cfm/models/video/jcr:content/model" cq:templatetype="/libs/settings/dam/cfm/model-types/fragment" jcr:primarytype="cq:PageContent" jcr:title="Video" sling:resourcesupertype="dam/cfm/models/console/components/data/entity" sling:resourcetype="dam/cfm/models/console/components/data/entity/default">
        <model cq:targetpath="/content/entities" jcr:primarytype="cq:PageContent" sling:resourcetype="wcm/scaffolding/components/scaffolding" datatypesconfig="/mnt/overlay/settings/dam/cfm/models/formbuilderconfig/datatypes" maxgeneratedorder="20">
            <cq:dialog jcr:primarytype="nt:unstructured" sling:resourcetype="cq/gui/components/authoring/dialog">
                <content jcr:primarytype="nt:unstructured" sling:resourcetype="granite/ui/components/coral/foundation/fixedcolumns">
                    <items jcr:primarytype="nt:unstructured" maxgeneratedorder="20">
                        <brc_title jcr:primarytype="nt:unstructured" sling:resourcetype="granite/ui/components/coral/foundation/form/textfield" fieldlabel="Name" listorder="1" maxlength="255" required="on" metatype="text-single" name="brc_title" renderreadonly="false" showemptyinreadonly="true" valuetype="string">
                        <brc_poster jcr:primarytype="nt:unstructured" sling:resourcetype="dam/cfm/models/editor/components/contentreference" fieldlabel="Poster" filter="hierarchy" listorder="8" metatype="reference" name="brc_poster" namesuffix="contentReference" renderreadonly="false" rootpath="/content/dam" showemptyinreadonly="true" validation="cfm.validation.contenttype.image" valuetype="string/reference">                      
                        <brc_duration jcr:primarytype="nt:unstructured" sling:resourcetype="granite/ui/components/coral/foundation/form/textfield" disabled="{Boolean}true" fieldlabel="Duration" listorder="1" maxlength="255" metatype="text-single" name="brc_duration" renderreadonly="false" showemptyinreadonly="true" valuetype="string">
                        <brc_created_at jcr:primarytype="nt:unstructured" sling:resourcetype="granite/ui/components/coral/foundation/form/datepicker" disabled="{Boolean}true" displayedformat="YYYY-MM-DD HH:mm" emptytext="YYYY-MM-DD HH:mm" fieldlabel="Created" listorder="5" metatype="date" name="brc_created_at" renderreadonly="false" showemptyinreadonly="true" type="datetime" valueformat="YYYY-MM-DD[T]HH:mm:ss.000Z" valuetype="calendar">
                            <granite:data jcr:primarytype="nt:unstructured" typehint="Date">
                        </granite:data></brc_created_at>
                        <brc_id jcr:primarytype="nt:unstructured" sling:resourcetype="granite/ui/components/coral/foundation/form/hidden" fieldlabel="Id" listorder="1" maxlength="255" metatype="text-single" name="brc_id" renderreadonly="false" showemptyinreadonly="true" valuetype="string">
                    </brc_id></brc_duration></brc_poster></brc_title></items>
                </content>
            </cq:dialog>
        </model>
    </jcr:content>
</jcr:root>
See more See less

Event Schema Model

This schema will serve both for upcoming Event and On-Demand webinars /conf/site-com/settings/dam/cfm/models/event:

<!--?xml version="1.0" encoding="UTF-8"?-->
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" jcr:primarytype="cq:Template" allowedpaths="[/content/entities(/.*)?]" ranking="{Long}100">
    <jcr:content cq:scaffolding="/conf/site-com/settings/dam/cfm/models/event/jcr:content/model" cq:templatetype="/libs/settings/dam/cfm/model-types/fragment" jcr:primarytype="cq:PageContent" jcr:title="Event" sling:resourcesupertype="dam/cfm/models/console/components/data/entity" sling:resourcetype="dam/cfm/models/console/components/data/entity/default">
        <model cq:targetpath="/content/entities" jcr:primarytype="cq:PageContent" sling:resourcetype="wcm/scaffolding/components/scaffolding" datatypesconfig="/mnt/overlay/settings/dam/cfm/models/formbuilderconfig/datatypes" maxgeneratedorder="20">
            <cq:dialog jcr:primarytype="nt:unstructured" sling:resourcetype="cq/gui/components/authoring/dialog">
                <content jcr:primarytype="nt:unstructured" sling:resourcetype="granite/ui/components/coral/foundation/fixedcolumns">
                    <items jcr:primarytype="nt:unstructured" maxgeneratedorder="20">
                        <name jcr:primarytype="nt:unstructured" sling:resourcetype="granite/ui/components/coral/foundation/form/textfield" fieldlabel="Event Name" listorder="1" maxlength="255" metatype="text-single" name="eventName" renderreadonly="false" required="on" showemptyinreadonly="true" valuetype="string">
                        <start_date jcr:primarytype="nt:unstructured" sling:resourcetype="granite/ui/components/coral/foundation/form/datepicker" displayedformat="YYYY-MM-DD HH:mm" displaytimezonemessage="{Boolean}true" emptytext="YYYY-MM-DD HH:mm" fieldlabel="Start Date" listorder="5" metatype="date" name="startDate" renderreadonly="false" required="on" showemptyinreadonly="true" type="[datetime,datetime]" valueformat="YYYY-MM-DD[T]HH:mm:ss.000Z" valuetype="calendar/datetime">
                            <granite:data jcr:primarytype="nt:unstructured" typehint="Date">
                        </granite:data></start_date>
                        <end_date jcr:primarytype="nt:unstructured" sling:resourcetype="granite/ui/components/coral/foundation/form/datepicker" displayedformat="YYYY-MM-DD HH:mm" displaytimezonemessage="{Boolean}true" emptytext="YYYY-MM-DD HH:mm" fieldlabel="”End" date"="" listorder="5" metatype="date" name="endDate" renderreadonly="false" showemptyinreadonly="true" type="[datetime,datetime]" valueformat="YYYY-MM-DD[T]HH:mm:ss.000Z" valuetype="calendar/datetime">
                            <granite:data jcr:primarytype="nt:unstructured" typehint="Date">
                        </granite:data></end_date>
                        <duration jcr:primarytype="nt:unstructured" sling:resourcetype="granite/ui/components/coral/foundation/form/textfield" fielddescription="Please provide event duration in minutes." fieldlabel="Duration, minutes" listorder="1" maxlength="255" metatype="text-single" name="duration" renderreadonly="false" showemptyinreadonly="true" valuetype="string">
                        <form_id jcr:primarytype="nt:unstructured" sling:resourcetype="granite/ui/components/coral/foundation/form/textfield" fieldlabel="FORM - ID" listorder="1" maxlength="255" metatype="text-single" name="formID" renderreadonly="false" showemptyinreadonly="true" valuetype="string">      
                        <webinarvideoreference jcr:primarytype="nt:unstructured" sling:resourcetype="dam/cfm/models/editor/components/fragmentreference" allownew="{Boolean}true" fieldlabel="Webinar Video" filter="hierarchy" fragmentmodelreference="/conf/site-com/settings/dam/cfm/models/video" listorder="21" metatype="fragment-reference" name="webinarVideoReference" namesuffix="contentReference" renderreadonly="false" rootpath="/content/dam/site-com/content-fragments/${psCFRegion}/webinars" showemptyinreadonly="true" subtype="content-fragment" valuetype="string/content-fragment">
                            <field jcr:primarytype="nt:unstructured" rootpath="/content/dam/site-com/content-fragments/${psCFRegion}/webinars">
                            <granite:data jcr:primarytype="nt:unstructured">
                        </granite:data></field></webinarvideoreference>
                    </form_id></duration></name></items>
                </content>
            </cq:dialog>
        </model>
    </jcr:content>
</jcr:root>
See more See less

Video Sync Process: Using AEM Brightcove Connector

We’re using Brightcove AEM for videos, so now we’ll set a video sync-up using Adobe AEM Brightcove Connector (the Adobe-AEM-Brightcove Connector allows you to manage Brightcove Video Cloud videos and players within AEM and easily embed videos in AEM pages).

We’ll start by setting up the sync into ”/content/dam/brightcove/en” and then create a process that will create video content fragments based on synced videos.

First of all, we add a pom.xml configuration:

<dependency>
       <groupid>com.coresecure.brightcove.cq5</groupid>
       <artifactid>brightcove_connector</artifactid>
       <type>zip</type>
       <version>5.6</version>
   </dependency>
See more See less

And the sync config /apps/site-com/osgiconfig/config.author/com.coresecure.brightcove.wrapper.sling.BrcServiceImpl~b1d15603-3b49-459c-844d-a15120ddebd8.cfg.json:

{
    "client_secret":"{Enter your Brightcove client Secret API}",
    "accountAlias":"{Enter a name for the account to be displayed in the Connector}",
    "client_id":"{Enter your Brightcove client ID from the Brightcove API Authentication page }",
    "allowed_groups":["{Specify the group that will access the Connector (required) be sure that it is a group you are included in}"],
    "key":"{Default Video Player Key}",
    "asset_integration_path":"/content/dam/brightcove/en"
}
See more See less

And /apps/site-com/osgiconfig/config.author/com.coresecure.brightcove.wrapper.webservices.BrcReplicationHandler.cfg.json:

{
  "target_directory":"/content/dam/brightcove/en"
}
See more See less

The Brightcove Import Listener to create or update a video content fragment based on video assets from Brightcove AEM (a simplified code with all non-null checks skipped):

.....
    @Activate
    public void activate() {
        try {
            session = repository.loginService(SERVICE_NAME, null);
 
            JackrabbitEventFilter ef = new JackrabbitEventFilter()
                    .setAbsPath("/content/dam/brightcove/en")
                    .setNodeTypes(new String[]{JcrConstants.NT_UNSTRUCTURED})
                    .setEventTypes(Event.PROPERTY_CHANGED | Event.PROPERTY_ADDED)
                    .setIsDeep(true)
                    .setNoExternal(true);
            JackrabbitObservationManager om = (JackrabbitObservationManager) session.getWorkspace().getObservationManager();
            om.addEventListener (this, ef);
 
        } catch (RepositoryException e) {
            LOG.error("Unable to register session", e);
        }
    }
.......
 
    @Override
    public void onEvent(EventIterator events) {
        try (ResourceResolver resourceResolver = getResourceResolverFromSession(session, resourceResolverFactory)) {      
            while (events.hasNext()) {
                Event event = events.nextEvent();
                String path = event.getPath();
                if (!path.endsWith(ConnectorField.LAST_SYNC.getFieldName())) {
                    continue;
                }
                String metadataRelPath = joinAsPath(JCR_CONTENT, METADATA_FOLDER);
                String targetPath = StringUtils.substringBeforeLast(path, "/" + metadataRelPath);
 
                WorkflowSession workflowSession = resourceResolver.adaptTo(WorkflowSession.class);
                WorkflowModel workflowModel = workflowSession.getModel(“{brightcove sync metadata workflow path}”);
 
                Map<string, object=""> properties = new HashMap<>();
                properties.put(JOB_PARAMETER_PATH, targetPath);
                properties.put(JOB_PARAMETER_WF_ID, WF_MODEL_AUTHOR_ID);
                jobManager.addJob(TOPIC, properties);
            }
 
        } catch (WorkflowException | RepositoryException | RuntimeException ex) {
            LOG.error("Exception", ex);
        }
    }
</string,>
See more See less

And the corresponding Workflow Metadata Sync Process – this will create a blueprint as well as localized content fragments for everything at our site locales:

@Override
    public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap metaData) throws WorkflowException {
        ResourceResolver resourceResolver = getResourceResolver(workflowHelper, workflowSession);
        String sourcePath = getSourcePath(workItem);
        BrightcoveConfigService.BrightcoveConfig config = configService.getConfig();
 
        try {
 
            Resource metadata = getMetadataResource(resourceResolver, sourcePath);
 
            ValueMap assetValueMap = metadata.getValueMap();
            String title = StringUtils.trim(assetValueMap.get(DC_TITLE, StringUtils.EMPTY));
            String id = ConnectorField.ID.getValue(assetValueMap);
            String type = getTypeFieldValue(metadata);
 
            String userId = workItem.getWorkflow().getInitiator();
            FragmentTemplate template =
                    Optional.ofNullable(getFragmentTemplateResource(resourceResolver, "/conf/site-com/settings/dam/cfm/models/video"))
                    .map(resource -> resource.adaptTo(FragmentTemplate.class))
                    .orElse(null);
 
            String contentFragmentTargetPath = getCFTargetFolder(config, type, getYear(assetValueMap));
            createTargetFolder(resourceResolver, contentFragmentTargetPath);
 
            String cfPath = getCFPath(id, type, getYear(assetValueMap), configService.getConfig());
            ContentFragment contentFragment = getOrCreateFragment(resourceResolver.getResource(contentFragmentTargetPath), template, id, title);
            setContentElements(cfPath, id, contentFragment, resourceResolver, metadata, userId);
 
            for (Locale locale : Locale.values()) {
                setContentElements(getRegionalCFPath(resourceResolver, cfPath, locale), id, contentFragment, resourceResolver, metadata, userId);
            }
 
            if (resourceResolver.hasChanges()) {
                resourceResolver.commit();
            }
        } catch (ContentFragmentException | PersistenceException | WorkflowException | RuntimeException e) {
            LOG.error("Failed to sync video metadata [{}], path '{}'", e.getMessage(), sourcePath, e);
            workflowSession.terminateWorkflow(workItem.getWorkflow());
        }
    }
 
    private void setContentElements(String path, String id, ContentFragment cf, ResourceResolver resolver,  Resource metadata,
                                    Set<string> excludedFields, String userId) throws ContentFragmentException, WorkflowException {
 
        ModifiableValueMap cfValueMap = CFMUtils.getVariationStructuredDataProps(path, resolver);
 
        ValueMap customFieldsMap = Optional.ofNullable(metadata.getChild(ConnectorField.CUSTOM_FIELDS.getFieldName()))
                .map(Resource::getValueMap)
                .orElse(null);
 
        ValueMap assetValueMap = metadata.getValueMap();
        String title = assetValueMap.get(DC_TITLE, String.class);
        cf.setTitle(title);
 
        for (Iterator<contentelement> i = cf.getElements(); i.hasNext(); ) {
            ContentElement contentElement = i.next();
            addValueForContentElementIfPresent(contentElement, assetValueMap, cfValueMap);
            addValueForContentElementIfPresent(contentElement, customFieldsMap, cfValueMap);
        }
        ModifiableValueMap cfProps = Optional.ofNullable(resolver.getResource(joinAsPath(path, JCR_CONTENT)))
                .map(propsResource -> propsResource.adaptTo(ModifiableValueMap.class))
                .orElse(null);
 
       cfProps.put(JcrConstants.JCR_LASTMODIFIED,                      
              DateUtils.convertLocalDateTimeToCalendar(LocalDateTime.now()));
       cfProps.put(JcrConstants.JCR_LAST_MODIFIED_BY, userId);
 
    }
 
    private void addValueForContentElementIfPresent(ContentElement contentElement,
                        ValueMap map, ModifiableValueMap cfValueMap) {
        if (map.containsKey(damElementName)) {
            cfValueMap.put(contentElement.getName(), map.get(contentElement.getName()));
        }
    }
 
</contentelement></string>
See more See less

And a simple enum for Brightcove AEM field mapping:

package com.wcm.site.model.brightcove;
 
import com.adobe.cq.dam.cfm.ContentFragment;
import com.wcm.site.util.CFMUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.ValueMap;
 
import java.util.Optional;
 
public enum ConnectorField {
    ID("brc_id"),
    TITLE("brc_title"),
    TEXT_TRACKS("brc_text_tracks"),
    CUSTOM_FIELDS("brc_custom_fields"),
    BRC_TAGS("brc_tags"),
    TAGS("tags"),
    PAGE_NAME_HINT("pageNameHint"),
    DETAILS("details"),
    IS_AEM_EXPOSED("isaemexposed"),
    TYPE("type"),
    DESCRIPTION("brc_description"),
    LONG_DESCRIPTION("brc_long_description"),
    CREATED_AT("brc_created_at"),
    UPDATED_AT("brc_updated_at"),
    DURATION("brc_duration"),
    TEXT("text"),
    POSTER("brc_poster"),
    STAGE("stage"),
    LAST_SYNC("brc_lastsync"),
    THUMBNAIL("brc_thumbnail");
 
 
    private String fieldName;
 
    ConnectorField(String fieldName) {
        this.fieldName = fieldName;
    }
 
    public String getContent(Optional<contentfragment> contentFragment) {
        return CFMUtils.getContent(contentFragment, this.fieldName);
    }
 
    public <t> T getValue(Optional<contentfragment> contentFragment, Class<t> type) {
        return CFMUtils.getValue(contentFragment, this.fieldName, type);
    }
 
    public boolean getBooleanValue(Optional<contentfragment> contentFragment) {
        return BooleanUtils.toBoolean(CFMUtils.getValue(contentFragment, this.fieldName));
    }
 
    public <t> T getValue(ValueMap valueMap, Class<t> type) {
        return valueMap.get(this.getFieldName(), type);
    }
 
    public String getValue(Optional<contentfragment> contentFragment) {
        return Optional.ofNullable(getValue(contentFragment, String.class))
                .orElse(StringUtils.EMPTY);
    }
 
    public String getValue(ValueMap valueMap) {
        return Optional.ofNullable(getValue(valueMap, String.class))
                .orElse(StringUtils.EMPTY);
    }
 
    public boolean getBooleanValue(ValueMap valueMap) {
        return BooleanUtils.toBoolean(this.getValue(valueMap));
    }
 
    public String getFieldName() {
        return fieldName;
    }
}
</contentfragment></t></t></contentfragment></t></contentfragment></t></contentfragment>
See more See less

Stay tuned for the next articles on this subject. We’ll be showing how to manage Event content fragments and corresponding pages next. Should you have any questions or need AEM development services, reach out to us. We work wonders with AEM!