SlideShare a Scribd company logo
CIRCUIT – An Adobe Developer Event
Presented by ICF Interactive
Content API’s
for AEM
Bryan Williams
ICF Interactive
Bryan.Williams@icfi.com
@brywilliams
Bryan Williams
ICF Interactive (10+ years)
Working with CQ/AEM over 6 years
AEM Developer Certified (Beta)
Prize Question #1
?	
  
CIRCUIT 2015 - Content API's For AEM Sites
What do I mean by Content API?
•  Read only
•  Controlled
•  Possibly public but not necessarily
•  Usually an afterthought
•  Disclaimer: Not production code
CIRCUIT 2015 - Content API's For AEM Sites
Why not use something else?
•  Not saying you shouldn’t
•  Security : Control of who can access your
data
•  Encapsulation : Granular control of what data
is exposed
•  Simplicity : The easier for the consumer to
understand the better
•  Conformity : Maybe you need to conform to a
particular spec
•  Aggregation : Some data might be coming
from outside the repository
•  Versioning : Backwards compatibility
Technologies
•  Bedrock
–  https://github.com/Citytechinc/bedrock
•  CQ Component (Maven) Plugin
–  https://github.com/Citytechinc/cq-component-maven-plugin
•  Sling Models
–  https://sling.apache.org/documentation/bundles/models.html
•  Jackson
–  https://github.com/FasterXML/jackson
•  Prosper
–  https://github.com/Citytechinc/prosper
•  Groovy
–  http://www.groovy-lang.org/
What are Bedrock, CQCP and Prosper?
•  Bedrock : Open source library that contains common
utilities, decorators, abstract classes, tag libraries and
Javascript modules for bootstrapping and simplifying
AEM projects
•  CQ Component (Maven) Plugin : Generates
many of the artifacts necessary for the creation of a CQ
component based on the information provided by the
component’s backing Java class
•  Prosper : An integration testing library for AEM
projects using Spock (a Groovy-based testing
framework)
Sling Models
•  Automates mapping of Sling objects such
as resources, request, etc. to POJOs
•  Available out of the box in AEM 6
Why Jackson?
•  Popular
•  Able to produce JSON or XML
•  Lots of features
•  We were already using it
Groovy
•  An object-oriented programming language
for the Java platform
•  Dynamic in nature
•  Reduced syntax
•  Traits
Prize Question #2
?:
Layers of a Content API
•  Component Models : Referring to both page and
content components and their corresponding backing
beans
•  Servlet : Takes the request and calls the appropriate
service based on selectors
•  Service : Responsible for getting the appropriate data
and possibly caching
•  Query Builder : API for making queries to the
repository
•  Filters : Last minute cleanup of outgoing data
(externalize URLs, etc.)
Non-Page Components
•  Children of stories
•  Returned in getBody() of Story model
•  Custom and OOTB components must
have backing bean
•  Models identified by path convention
circuit2015/groovy/components/page/stories/article
=
com.bryanw.conferences.circuit2015.groovy.components.page.stories.article.Article
Sample Article Page
Article
@Model(adaptables = Resource, adapters = [Article, Story], defaultInjectionStrategy =
DefaultInjectionStrategy.OPTIONAL)
@Component(value = "Article",
name = "article",
actions = ["text:Article", "-", "edit"],
group = '.hidden',
path = 'groovy/components/page/stories',
resourceSuperType = 'circuit2015/groovy/components/page/global',
disableTargeting = true,
tabs = [@Tab(title = PROPERTIES_LABEL), @Tab(title = MAIN_IMAGE_LABEL)])
@AutoInstantiate(instanceName = "article")
@Slf4j("LOG")
class Article extends AbstractStoryComponent implements AbstractStoryRequiredImage,
SeoReadyStory {
@JsonView(JacksonViews.DetailView)
List<AbstractCircuit2015Component> getBody() {
Optional<ComponentNode> mainParNode = getComponentNode(MAIN_PAR)
if (mainParNode.present) {
List<AbstractCircuit2015Component> components =
mainParNode.get().componentNodes.collect {
it.resource.adaptTo(AbstractCircuit2015Component)
} - null
return components
}
[]
}
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
AbstractStoryComponent
abstract class AbstractStoryComponent extends AbstractCircuit2015Component implements Story {
protected static final String MAIN_PAR = "mainpar"
@Inject
private PageManager pageManager
@Inject @Named('jcr:title')
@DialogField(fieldLabel = "Title", name = './jcr:title', required = true, tab = PROPERTIES_INDEX)
@TextField
String title
@Inject @Named('jcr:description')
@DialogField(fieldLabel = "Description", name = './jcr:description', tab = PROPERTIES_INDEX)
@TextArea
@JsonView(JacksonViews.DetailView)
String description
@Inject
@DialogField(fieldLabel = "Published date", name = './publishedDate', required = true, tab = PROPERTIES_INDEX)
@DateTime
Date publishedDate
@Inject
@DialogField(fieldLabel = "Author Bio Path", name = "./authorBioPath", tab = PROPERTIES_INDEX)
@PathField
@JsonIgnore
String authorBioPath
@JsonView(JacksonViews.DetailView)
Link getStoryLink() {
pageManager.getContainingPage(resource).adaptTo(ComponentNode).link
}
@JsonView(JacksonViews.ListView)
String getStoryHref() {
pageManager.getContainingPage(resource).adaptTo(ComponentNode).href
}
@JsonView(JacksonViews.DetailView)
Bio getAuthorBio() {
pageManager.getPage(authorBioPath)?.getContentResource()?.adaptTo(Bio)
}
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
AbstractStoryComponent
abstract class AbstractStoryComponent extends AbstractCircuit2015Component implements Story {
protected static final String MAIN_PAR = "mainpar"
@Inject
private PageManager pageManager
@Inject @Named('jcr:title')
@DialogField(fieldLabel = "Title", name = './jcr:title', required = true,
tab = PROPERTIES_INDEX)
@TextField
String title
@Inject @Named('jcr:description')
@DialogField(fieldLabel = "Description", name = './jcr:description', tab = PROPERTIES_INDEX)
@TextArea
@JsonView(JacksonViews.DetailView)
String description
@Inject
@DialogField(fieldLabel = "Published date", name = './publishedDate', required = true,
tab = PROPERTIES_INDEX)
@DateTime
Date publishedDate
...
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
AbstractStoryComponent
…
@Inject
@DialogField(fieldLabel = "Author Bio Path", name = "./authorBioPath",
tab = PROPERTIES_INDEX)
@PathField
@JsonIgnore
String authorBioPath
@JsonView(JacksonViews.DetailView)
Link getStoryLink() {
pageManager.getContainingPage(resource).adaptTo(ComponentNode).link
}
@JsonView(JacksonViews.ListView)
String getStoryHref() {
pageManager.getContainingPage(resource).adaptTo(ComponentNode).href
}
@JsonView(JacksonViews.DetailView)
Bio getAuthorBio() {
pageManager.getPage(authorBioPath)?.getContentResource()?.adaptTo(Bio)
}
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
AbstractStoryComponent
abstract class AbstractStoryComponent extends AbstractCircuit2015Component implements Story {
protected static final String MAIN_PAR = "mainpar"
@Inject
private PageManager pageManager
@Inject @Named('jcr:title')
@DialogField(fieldLabel = "Title", name = './jcr:title', required = true, tab = PROPERTIES_INDEX)
@TextField
String title
@Inject @Named('jcr:description')
@DialogField(fieldLabel = "Description", name = './jcr:description', tab = PROPERTIES_INDEX)
@TextArea
@JsonView(JacksonViews.DetailView)
String description
@Inject
@DialogField(fieldLabel = "Published date", name = './publishedDate', required = true, tab = PROPERTIES_INDEX)
@DateTime
Date publishedDate
@Inject
@DialogField(fieldLabel = "Author Bio Path", name = "./authorBioPath", tab = PROPERTIES_INDEX)
@PathField
@JsonIgnore
String authorBioPath
@JsonView(JacksonViews.DetailView)
Link getStoryLink() {
pageManager.getContainingPage(resource).adaptTo(ComponentNode).link
}
@JsonView(JacksonViews.ListView)
String getStoryHref() {
pageManager.getContainingPage(resource).adaptTo(ComponentNode).href
}
@JsonView(JacksonViews.DetailView)
Bio getAuthorBio() {
pageManager.getPage(authorBioPath)?.getContentResource()?.adaptTo(Bio)
}
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
Paul Michelotti Blog
h;p://citytechinc.com/us/en/blog/2015/03/groovy-­‐component-­‐composiHon-­‐with-­‐traits.html	
  
Groovy	
  Component	
  ComposiHon	
  With	
  Traits	
  
	
  
AbstractStoryOptionalImage
trait AbstractStoryRequiredImage implements ComponentNode {
@Inject @Named('mainImageCaption')
@DialogField(fieldLabel = 'Main Image Caption', name = 'mainImageCaption',
fieldName = 'mainImageCaption', tab = MAIN_IMAGE_INDEX, ranking = 200D,
additionalProperties = [@Property(name = 'name', value = ’.mainImageCaption')])
@TextField
private String mainImageCaption
@Inject @ImageInject(path = 'mainImage')
@DialogField(fieldLabel = 'Main Image', name = 'mainImage', fieldName =
'mainImage', tab = MAIN_IMAGE_INDEX, required = true, ranking = 201D,
additionalProperties = [@Property(name = "name", value = './mainImage')])
@Html5SmartImage(allowUpload = false, name = "mainImage", tab = false,
height = 400)
private Image mainImage
public String getMainImageCaption() {
mainImageCaption
}
public Circuit2015Image getMainImage() {
mainImage ? new Circuit2015Image(src: mainImage.src) : null
}
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
AbstractStoryRequiredImage
trait AbstractStoryRequiredImage implements ComponentNode {
@Inject @Named('mainImageCaption')
@DialogField(fieldLabel = 'Main Image Caption', name = 'mainImageCaption',
fieldName = 'mainImageCaption', tab = MAIN_IMAGE_INDEX, ranking = 200D,
required = true, additionalProperties = [@Property(name = 'name', value =
'./mainImageCaption')])
@TextField
private String mainImageCaption
@Inject @ImageInject(path = 'mainImage')
@DialogField(fieldLabel = 'Main Image', name = 'mainImage', fieldName =
'mainImage', tab = MAIN_IMAGE_INDEX, required = true, ranking = 201D,
additionalProperties = [@Property(name = "name", value = './mainImage')])
@Html5SmartImage(allowUpload = false, name = "mainImage", tab = false,
height = 400)
private Image mainImage
public String getMainImageCaption() {
mainImageCaption
}
public Circuit2015Image getMainImage() {
mainImage ? new Circuit2015Image(src: mainImage.src) : null
}
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
maven-scr-plugin
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-scr-plugin</artifactId>
<executions>
<execution>
<id>generate-scr-scrdescriptor</id>
<goals>
<goal>scr</goal>
</goals>
</execution>
</executions>
<configuration>
<scanClasses>true</scanClasses>
<excludes>
com/bryanw/conferences/circuit2015/groovy/story/
AbstractStoryOptionalImage*,
com/bryanw/conferences/circuit2015/groovy/story/
AbstractStoryRequired*,
com/bryanw/conferences/circuit2015/groovy/story/
SeoReadyStory*
</excludes>
</configuration>
</plugin>
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
Content API Servlet
•  Accepts page paths only
•  Accepts XML/JSON extension
•  Accepts “story” and “stories” selector
•  Constructs search parameters
•  Passes current path and search
parameters on to service
ContentApiServlet
@SlingServlet(resourceTypes = [ NameConstants.NT_PAGE ], selectors = [ "stories", "story" ],
extensions = [ EXTENSION_JSON, "xml" ], methods = [ "GET" ])
@Slf4j("LOG")
public class ContentApiServlet extends XmlOrJsonResponseServlet {
@Reference
ContentApiService contentApiService
@Override
protected final void doGet(final SlingHttpServletRequest slingRequest, final SlingHttpServletResponse slingResponse) {
RequestPathInfo requestPathInfo = slingRequest.requestPathInfo
slingResponse.setHeader("Cache-Control", "no-cache, no-store, must-revalidate")
slingResponse.setHeader("Expires", "0");
StorySearchParameters storySearchParameters = buildStorySearchParameters(slingRequest)
if (requestPathInfo.selectors.contains('stories')) {
writeResponse(slingResponse, requestPathInfo.extension, JacksonViews.ListView, contentApiService.getStories(storySearchParameters))
} else {
writeResponse(slingResponse, requestPathInfo.extension, JacksonViews.DetailView, contentApiService.getStory(slingRequest.resource))
}
}
static private StorySearchParameters buildStorySearchParameters(final SlingHttpServletRequest slingHttpServletRequest) {
StorySearchParameters storySearchParameters = new StorySearchParameters()
storySearchParameters.setBaseResource(slingHttpServletRequest.resource)
slingHttpServletRequest.parameterMap.each { key, value ->
if (key == 'type') {
storySearchParameters.setType(resolveStoryTypeClass(slingHttpServletRequest.getParameter('type')))
}
else {
storySearchParameters[key as String] = (value as String[])[0]
}
}
storySearchParameters
}
static private Class<? extends Story> resolveStoryTypeClass(String type) {
Class<? extends Story> storyTypeClass
storyTypeClass = type ? Class.forName("com.bryanw.conferences.circuit2015.groovy.components.page.stories.${type}.$
{type.capitalize()}").asSubclass(Story) : Story
storyTypeClass
}
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
ContentApiServlet.doGet()
@Override
protected final void doGet(final SlingHttpServletRequest slingRequest,
final SlingHttpServletResponse slingResponse) {
RequestPathInfo requestPathInfo = slingRequest.requestPathInfo
slingResponse.setHeader("Cache-Control", "no-cache, no-store, must-revalidate")
slingResponse.setHeader("Expires", "0");
StorySearchParameters storySearchParameters =
buildStorySearchParameters(slingRequest)
if (requestPathInfo.selectors.contains('stories')) {
writeResponse(slingResponse, requestPathInfo.extension,
JacksonViews.ListView,
contentApiService.getStories(storySearchParameters))
} else {
writeResponse(slingResponse, requestPathInfo.extension,
JacksonViews.DetailView,
contentApiService.getStory(slingRequest.resource))
}
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
ContentApiServlet.buildStorySearchParameters()
static private StorySearchParameters buildStorySearchParameters(
final SlingHttpServletRequest slingRequest) {
StorySearchParameters searchParams = new StorySearchParameters()
storySearchParameters.setBaseResource(slingRequest.resource)
slingRequest.parameterMap.each { key, value ->
if (key == 'type') {
String typeParam = slingRequest.getParameter('type')
searchParams.setType(resolveStoryTypeClass(typeParam))
}
else {
searchParams[key as String] = (value as String[])[0]
}
}
searchParams
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
ContentApiServlet.resolveStoryTypeClass()
private Class<? extends Story>
resolveStoryTypeClass(String type) {
Class<? extends Story> storyTypeClass
String packagePrefix =
'com.bryanw.conferences.circuit2015.groovy.components.page.stories'
storyTypeClass = type ? Class
.forName("${packagePrefix}.${type}.${type.capitalize()}")
.asSubclass(Story) : Story
storyTypeClass
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
StorySearchParameters
@EqualsAndHashCode
class StorySearchParameters {
Class<Story> type
Resource baseResource
String text
String start
String limit
Map<String, String> searchables = [:]
def propertyMissing(String name, value) {
searchables[name] = value
}
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
XmlOrJsonResponseServlet
@Slf4j("LOG")
class XmlOrJsonResponseServlet extends SlingAllMethodsServlet {
public static final String DEFAULT_DATE_FORMAT = "MM/dd/yyyy hh:mm aaa z";
private static final DateFormat MAPPER_DATE_FORMAT = new SimpleDateFormat(DEFAULT_DATE_FORMAT, Locale.US)
private static final XmlFactory XML_FACTORY = new XmlFactory()
public void writeResponse(final SlingHttpServletResponse response, final String extension,
final Class<JacksonViews.View> view, final Object object) {
if ("xml" == extension) {
XmlMapper xmlMapper = new XmlMapper().setDateFormat(MAPPER_DATE_FORMAT) as XmlMapper
writeXmlResponse(response, xmlMapper, view, object)
} else {
ObjectMapper jsonMapper = new ObjectMapper().setDateFormat(MAPPER_DATE_FORMAT)
writeJsonResponse(response, jsonMapper, view, object)
}
}
protected static void writeXmlResponse(final SlingHttpServletResponse response, final XmlMapper xmlMapper,
final Class<JacksonViews.View> view, final Object object) {
MediaType mediaType = MediaType.XML_UTF_8
response.setContentType(mediaType.withoutParameters().toString())
response.setCharacterEncoding(mediaType.charset().get().name())
final ToXmlGenerator generator = XML_FACTORY.createGenerator(response.getWriter())
xmlMapper.writerWithView(view).writeValue(generator, object)
}
protected void writeJsonResponse(final SlingHttpServletResponse response, final ObjectMapper mapper,
final Class<JacksonViews.View> view, final Object object) throws IOException {
response.setContentType(MediaType.JSON_UTF_8.withoutParameters().toString());
response.setCharacterEncoding(MediaType.JSON_UTF_8.charset().get().name());
final JsonGenerator generator = new JsonFactory().disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET)
.createGenerator(response.getWriter());
mapper.writerWithView(view).writeValue(generator, object);
}
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
XmlOrJsonResponseServlet.writeResponse()
public void writeResponse(
final SlingHttpServletResponse response,
final String extension,
final Class<JacksonViews.View> view,
final Object object) {
if ("xml" == extension) {
XmlMapper xmlMapper =
new XmlMapper()
.setDateFormat(MAPPER_DATE_FORMAT) as XmlMapper
writeXmlResponse(response, xmlMapper, view, object)
} else {
ObjectMapper jsonMapper =
new ObjectMapper()
.setDateFormat(MAPPER_DATE_FORMAT)
writeJsonResponse(response, jsonMapper, view, object)
}
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
XmlOrJsonResponseServlet.writeXmlResponse()
protected void writeXmlResponse(
final SlingHttpServletResponse response,
final XmlMapper xmlMapper,
final Class<JacksonViews.View> view,
final Object object) {
MediaType mediaType = MediaType.XML_UTF_8
response.setContentType(
mediaType.withoutParameters().toString())
response.setCharacterEncoding(
mediaType.charset().get().name())
ToXmlGenerator generator = XML_FACTORY
.createGenerator(response.getWriter())
xmlMapper.writerWithView(view)
.writeValue(generator, object)
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
XmlOrJsonResponseServlet.writeJsonResponse()
protected void writeJsonResponse(
final SlingHttpServletResponse response,
final ObjectMapper mapper,
final Class<JacksonViews.View> view,
final Object object){
MediaType mediaType = MediaType.JSON_UTF_8
response.setContentType(
mediaType.withoutParameters().toString());
response.setCharacterEncoding(
mediaType.charset().get().name());
JsonGenerator generator = new JsonFactory()
.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET)
.createGenerator(response.getWriter());
mapper.writerWithView(view)
.writeValue(generator, object);
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
Content API Service
•  Calls repository layer
•  (Guava) caching
DefaultContentApiService
@Component
@Service(ContentApiService)
@Slf4j("LOG")
class DefaultContentApiService extends AbstractCacheService
implements ContentApiService {
@Reference
private ContentApiRepository contentApiRepository
@Override
StorySearchResult getStories(StorySearchParameters storySearchParameters) {
// Caching should go here
contentApiRepository.search(storySearchParameters)
}
@Override
Story getStory(Resource storyResource) {
storyResource?.getChild(JcrConstants.JCR_CONTENT)?.adaptTo(Story)
}
@Override
protected Logger getLogger() {
LOG
}
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
Content API Repository Layer
•  Constructs proper QueryBuilder query
•  Instantiates appropriate models from
results
DefaultContentApiRepository
@Component
@Service(ContentApiRepository)
@Slf4j("LOG")
class DefaultContentApiRepository implements ContentApiRepository {
@Reference
private QueryBuilder queryBuilder
@Override
public StorySearchResult search(StorySearchParameters storySearchParameters) {
Session session = storySearchParameters.baseResource.resourceResolver.adaptTo(Session)
String start = storySearchParameters.start ? storySearchParameters.start : '0'
String limit = storySearchParameters.limit ? storySearchParameters.limit : '100'
PredicateGroup mainGroup = new PredicateGroup();
mainGroup.add(new Predicate("path").set("path", storySearchParameters.baseResource.path))
mainGroup.add(new Predicate("type").set("type", "cq:PageContent"))
mainGroup.add(new Predicate("property").set(Predicate.ORDER_BY, "publishedDate"))
mainGroup.add(new Predicate("property").set("${Predicate.PARAM_SORT}.${Predicate.PARAM_SORT}", Predicate.SORT_DESCENDING))
mainGroup.add(new Predicate("property").set(Predicate.PARAM_OFFSET, start))
mainGroup.add(new Predicate("property").set(Predicate.PARAM_LIMIT, limit))
if (storySearchParameters.text) {
mainGroup.add(new Predicate("fulltext").set("fulltext", storySearchParameters.text))
}
mainGroup.add(createResourceTypePredicate(storySearchParameters))
storySearchParameters.searchables.each { key, value ->
// Add whatever other search predicates you want to allow here
}
Query query = queryBuilder.createQuery(mainGroup, session);
SearchResult searchResult = query.getResult();
StorySearchResult storyResult = new StorySearchResult();
storyResult.stories = searchResult.hits.collect {
it.resource.adaptTo(Story)
} - null
storyResult.setTotalResults(searchResult.totalMatches);
storyResult.setStart(start as Long);
storyResult
}
private Predicate createResourceTypePredicate(StorySearchParameters storySearchParameters) {
Predicate resourceTypePredicate = new Predicate("property")
resourceTypePredicate.set("property", SLING_RESOURCE_TYPE_PROPERTY)
if (!storySearchParameters.type || storySearchParameters.type.name == Story.name) {
resourceTypePredicate.set("operation", "like")
resourceTypePredicate.set("value", "circuit2015/groovy/components/page/stories/%")
} else {
//String typeName = storySearchParameters.baseResource.class.simpleName.toLowerCase()
String typeName = storySearchParameters.type.simpleName
resourceTypePredicate.set("value", "circuit2015/groovy/components/page/stories/${typeName.toLowerCase()}")
}
resourceTypePredicate
}
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
DefaultContentApiRepository.search()
@Override
public StorySearchResult search(StorySearchParameters storySearchParameters) {
Session session = storySearchParameters.baseResource.resourceResolver.adaptTo(Session)
String start = storySearchParameters.start ? storySearchParameters.start : '0'
String limit = storySearchParameters.limit ? storySearchParameters.limit : '100'
PredicateGroup mainGroup = new PredicateGroup();
mainGroup.add(new Predicate("path").set("path", storySearchParameters.baseResource.path))
mainGroup.add(new Predicate("type").set("type", "cq:PageContent"))
mainGroup.add(new Predicate("property").set(Predicate.ORDER_BY, "publishedDate"))
mainGroup.add(new Predicate("property").set("${Predicate.PARAM_SORT}.${Predicate.PARAM_SORT}",
Predicate.SORT_DESCENDING))
mainGroup.add(new Predicate("property").set(Predicate.PARAM_OFFSET, start))
mainGroup.add(new Predicate("property").set(Predicate.PARAM_LIMIT, limit))
if (storySearchParameters.text) {
mainGroup.add(new Predicate("fulltext").set("fulltext", storySearchParameters.text))
}
mainGroup.add(createResourceTypePredicate(storySearchParameters))
storySearchParameters.searchables.each { key, value ->
// Add whatever other search predicates you want to allow here
}
Query query = queryBuilder.createQuery(mainGroup, session);
SearchResult searchResult = query.getResult();
StorySearchResult storyResult = new StorySearchResult();
storyResult.stories = searchResult.hits.collect {
it.resource.adaptTo(Story)
} - null
storyResult.setTotalResults(searchResult.totalMatches);
storyResult.setStart(start as Long);
storyResult
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
DefaultContentApiRepository.createResourceTypePredicate()
private Predicate createResourceTypePredicate(
StorySearchParameters storySearchParameters) {
Predicate resourceTypePredicate = new Predicate("property")
resourceTypePredicate.set("property”,SLING_RESOURCE_TYPE_PROPERTY)
if (!storySearchParameters.type ||
storySearchParameters.type.name == Story.name) {
resourceTypePredicate.set("operation", "like")
resourceTypePredicate.set("value",
"circuit2015/groovy/components/page/stories/%")
} else {
String typeName = storySearchParameters.type.simpleName
resourceTypePredicate.set("value",
"circuit2015/groovy/components/page/stories/$
{typeName.toLowerCase()}")
}
resourceTypePredicate
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
StorySearchResult
@EqualsAndHashCode
@XmlRootElement(name = "result")
class StorySearchResult {
List<Story> stories
long totalResults
long start
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
ResourceTypeImplementationPicker
@Component
@Service(ImplementationPicker)
@Property(name = Constants.SERVICE_RANKING, intValue = 1000)
@Slf4j("LOG")
public class ResourceTypeImplementationPicker implements ImplementationPicker {
public Class pick(Class adapterType, Class[] implTypes, Object adaptable) {
Class pickedClass = null
if (adaptable instanceof Resource) {
Resource resource = adaptable as Resource
String resourceType = resource.getResourceType()
pickedClass = implTypes.find {
if (it instanceof AbstractCircuit2015Component) {
(it as AbstractCircuit2015Component).conventionalResourceType() == resourceType
}
}
}
pickedClass
}
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
ClassNameImplementationPicker
@Component
@Service(ImplementationPicker)
@Property(name = Constants.SERVICE_RANKING, intValue = 1001)
@Slf4j("LOG")
class ClassNameImplementationPicker implements ImplementationPicker {
public Class pick(Class adapterType, Class[] implTypes, Object adaptable) {
Class classNameClass = null
if (adaptable instanceof Resource) {
Resource resource = adaptable as Resource
String className = resource.properties?.get("className")
classNameClass = implTypes.find {
it.name == className
}
}
classNameClass
}
}
Bedrock 	
   	
  CQ	
  Component	
  Plugin	
  
Sling	
  Models 	
   	
  Jackson	
  
http://mydomain.com/circuit-2015-demo.stories.json
{
"stories": [
{
"title": "CIRCUIT Promo Video",
"publishedDate": "08/12/2015 10:49 AM CDT",
"seoTitle": "CIRCUIT Promo Video",
"seoDescription": "Maximas vero virtutes iacere omnis necesse",
"mainImage":
{
"src": "/content/dam/circuit-2015-demo/images/promo.png"
},
"mainImageCaption": "CIRCUIT Promo Video Image",
"index": 0,
"storyHref": "/content/circuit-2015-demo/circuit-promo-video.html"
”videoHref": "https://www.youtube.com/watch?v=r2mFb1dIiug"
},
{
"title": "Article 1",
"publishedDate": "08/06/2015 01:21 AM CDT",
"seoTitle": "Article One",
"seoDescription": "Maximas vero virtutes iacere omnis necesse",
"mainImage":
{
"src": "/content/dam/circuit-2015-demo/images/article1banner.jpg"
},
"mainImageCaption": "Article 1 Banner Image",
"index": 0,
"storyHref": "/content/circuit-2015-demo/article-1.html"
}
],
"totalResults": 2,
"start": 0
}
http://mydomain.com/circuit-2015-demo.stories.json?type=article
{
"stories": [
{
"title": "Article 1",
"publishedDate": "08/06/2015 01:21 AM CDT",
"seoTitle": "Article One",
"seoDescription": "Maximas vero virtutes iacere omnis necesse",
"mainImage":
{
"src": "/content/dam/circuit-2015-demo/images/article1banner.jpg"
},
"mainImageCaption": "Article 1 Banner Image",
"index": 0,
"storyHref": "/content/circuit-2015-demo/article-1.html"
}
],
"totalResults": 1,
"start": 0
}
http://mydomain.com/circuit-2015-demo/article-1.story.json
{
"title": "Article 1",
"description": "Maximas vero virtutes iacere omnis necesse",
"publishedDate": "08/06/2015 01:21 AM CDT",
"seoTitle": "Article One",
"seoDescription": "Maximas vero virtutes iacere omnis necesse",
"body":
[
{
"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.n",
"index": 0
}
],
"mainImage":
{
"src": "/content/dam/circuit-2015-demo/images/article1banner.jpg"
},
"mainImageCaption": "Article 1 Banner Image",
"index": 0,
"authorBio":
{
"firstName": "Bryan",
"lastName": "Williams",
"twitter": "@brywilliams",
"description": "Maximas vero virtutes iacere omnis necesse",
"mainImage":
{
"src": "/content/dam/circuit-2015-demo/images/bryanw-profile.png"
},
"mainImageCaption": "Bryan Williams Bio Image",
"index": 0
},
"storyLink":
{
"path": "/content/circuit-2015-demo/article-1",
"extension": "html",
"suffix": "",
"href": "/content/circuit-2015-demo/article-1.html",
"selectors": [ ],
"queryString": "",
"external": false,
"target": "_self",
"title": "",
"properties": { },
"empty": false
}
}
Versioning
•  Like I said, not OOTB for Jackson
•  Need something like @Since in GSON
•  Jackson Filters
Filters
•  Encoding
•  Externalizing URLs
Testing with Prosper
•  Integration testing using Spock/Groovy
•  Specifically for AEM testing
•  Builders for Nodes/Pages
•  Uses Sling Mocks
Publish vs Author
•  May want internal apps to access author
•  replicatedDate is not what it seems
Conclusions
•  Bedrock
–  https://github.com/Citytechinc/bedrock
–  Provided us with useful classes, annotations and model injectors
•  CQ Component (Maven) Plugin
–  https://github.com/Citytechinc/cq-component-maven-plugin
–  Allowed us to create dialogs without writing a single XML file
•  Sling Models
–  https://sling.apache.org/documentation/bundles/models.html
–  Wired up our backing beans for us
•  Jackson
–  https://github.com/FasterXML/jackson
–  Let us define the details of bean to JSON/XML conversion
•  Prosper
–  https://github.com/Citytechinc/prosper
–  Simplified tests
•  Groovy
–  http://www.groovy-lang.org/
–  Less code
Bryan Williams
ICF Interactive
Bryan.Williams@icfi.com
@brywilliams

More Related Content

PDF
AEM 6.1 User Interface Customization
PDF
User Interface customization for AEM 6
PPTX
UI Customization in AEM 6.0
PDF
AEM Best Practices for Component Development
PPTX
AEM 6.0 - Author UI Customization & Features
PPTX
User interface customization for aem6 circuit
PDF
CIRCUIT 2015 - UI Customization in AEM 6.1
PPTX
Introduction to Sightly and Sling Models
AEM 6.1 User Interface Customization
User Interface customization for AEM 6
UI Customization in AEM 6.0
AEM Best Practices for Component Development
AEM 6.0 - Author UI Customization & Features
User interface customization for aem6 circuit
CIRCUIT 2015 - UI Customization in AEM 6.1
Introduction to Sightly and Sling Models

What's hot (20)

PDF
Adobe Experience Manager Core Components
PDF
AEM Sightly Template Language
PPTX
Build single page applications using AngularJS on AEM
POTX
Thoughts on Component Resuse
PPTX
Adobe AEM core components
PPTX
AEM and Sling
PDF
AEM Sightly Deep Dive
PDF
12 hot features to engage and save time with aem 6.2
PDF
Sling Dynamic Include
PPTX
Dynamic components using SPA concepts in AEM
PPTX
Extra aem development tools by Justin Edelson
PDF
Three WEM Dev Tricks
PPTX
Sling Models Overview
PPTX
AEM 6.0 Touch-optimized UI
PDF
SPA Editing with Sling to the rescue - adaptTo() 2021
PDF
10 reasons to migrate from AEM 5 to 6.1
PDF
Do more with LESS, Handlebars, Coffeescript and other Web Resources in AEM
PDF
Dynamic Components using Single-Page-Application Concepts in AEM/CQ
PPTX
Bridging the Gap: Single-Page Apps and AEM
PDF
Build single page applications using AngularJS on AEM
Adobe Experience Manager Core Components
AEM Sightly Template Language
Build single page applications using AngularJS on AEM
Thoughts on Component Resuse
Adobe AEM core components
AEM and Sling
AEM Sightly Deep Dive
12 hot features to engage and save time with aem 6.2
Sling Dynamic Include
Dynamic components using SPA concepts in AEM
Extra aem development tools by Justin Edelson
Three WEM Dev Tricks
Sling Models Overview
AEM 6.0 Touch-optimized UI
SPA Editing with Sling to the rescue - adaptTo() 2021
10 reasons to migrate from AEM 5 to 6.1
Do more with LESS, Handlebars, Coffeescript and other Web Resources in AEM
Dynamic Components using Single-Page-Application Concepts in AEM/CQ
Bridging the Gap: Single-Page Apps and AEM
Build single page applications using AngularJS on AEM
Ad

Viewers also liked (12)

PPTX
When dispatcher caching is not enough...
PDF
PDF
Introducing Apache Jackrabbit OAK
PDF
(Re)discover your AEM
PDF
Aem authentication vs idp
PPTX
AEM - Client Libraries
PDF
REST in AEM
PDF
CIRCUIT 2015 - Hybrid App Development with AEM Apps
PDF
CIRCUIT 2015 - AEM Infrastructure Automation with Chef Cookbooks
PDF
Adobe AEM Commerce with hybris
PDF
Master Chef class: learn how to quickly cook delightful CQ/AEM infrastructures
PDF
AEM (CQ) eCommerce Framework
When dispatcher caching is not enough...
Introducing Apache Jackrabbit OAK
(Re)discover your AEM
Aem authentication vs idp
AEM - Client Libraries
REST in AEM
CIRCUIT 2015 - Hybrid App Development with AEM Apps
CIRCUIT 2015 - AEM Infrastructure Automation with Chef Cookbooks
Adobe AEM Commerce with hybris
Master Chef class: learn how to quickly cook delightful CQ/AEM infrastructures
AEM (CQ) eCommerce Framework
Ad

Similar to CIRCUIT 2015 - Content API's For AEM Sites (20)

PDF
Spark IT 2011 - Simplified Web Development using Java Server Faces 2.0
PDF
Hyperproductive JSF 2.0 @ JavaOne Brazil 2010
PPTX
Developing your first application using FI-WARE
PDF
Toms introtospring mvc
PDF
JavaScript for NET Developers 1st Edition Ovais Mehboob Ahmed Khan
PDF
Learning To Run - XPages for Lotus Notes Client Developers
PDF
Download Complete JavaScript for NET Developers 1st Edition Ovais Mehboob Ahm...
PPTX
Integration of Backbone.js with Spring 3.1
PDF
Spring first in Magnolia CMS - Spring I/O 2015
PDF
Spring design-juergen-qcon
PDF
OSGi and Spring Data for simple (Web) Application Development - Christian Bar...
PDF
OSGi and Spring Data for simple (Web) Application Development
PPTX
Prairie DevCon 2015 - Crafting Evolvable API Responses
PPTX
Modern android development
PPT
Intorduction of Playframework
PDF
Spring 3 - An Introduction
PPTX
Customizing the Document Library
PDF
React on Rails - RailsConf 2017 (Phoenix)
PDF
[React Native Tutorial] Lecture 6: Component, Props, and Network
PPTX
WRStmlDSQUmUrZpQ0tFJ4Q_a36bc57fe1a24dd8bc5ba549736e406f_C2-Week2.pptx
Spark IT 2011 - Simplified Web Development using Java Server Faces 2.0
Hyperproductive JSF 2.0 @ JavaOne Brazil 2010
Developing your first application using FI-WARE
Toms introtospring mvc
JavaScript for NET Developers 1st Edition Ovais Mehboob Ahmed Khan
Learning To Run - XPages for Lotus Notes Client Developers
Download Complete JavaScript for NET Developers 1st Edition Ovais Mehboob Ahm...
Integration of Backbone.js with Spring 3.1
Spring first in Magnolia CMS - Spring I/O 2015
Spring design-juergen-qcon
OSGi and Spring Data for simple (Web) Application Development - Christian Bar...
OSGi and Spring Data for simple (Web) Application Development
Prairie DevCon 2015 - Crafting Evolvable API Responses
Modern android development
Intorduction of Playframework
Spring 3 - An Introduction
Customizing the Document Library
React on Rails - RailsConf 2017 (Phoenix)
[React Native Tutorial] Lecture 6: Component, Props, and Network
WRStmlDSQUmUrZpQ0tFJ4Q_a36bc57fe1a24dd8bc5ba549736e406f_C2-Week2.pptx

More from ICF CIRCUIT (10)

PDF
CIRCUIT 2015 - Monitoring AEM
PDF
CIRCUIT 2015 - Akamai: Caching and Beyond
PDF
CIRCUIT 2015 - Free Beer and Testing
PDF
CIRCUIT 2015 - Responsive Websites & Grid-Based Layouts
PDF
CIRCUIT 2015 - Glimpse of perceptual diff
PDF
CIRCUIT 2015 - Orchestrate your story with interactive video and web content
PDF
How to migrate from any CMS (thru the front-door)
PDF
Maximize the power of OSGi in AEM
PDF
CIRCUIT 2015 - 10 Things Apache Sling Can Do
PDF
Circuit 2015 Keynote - Carsten Ziegeler
CIRCUIT 2015 - Monitoring AEM
CIRCUIT 2015 - Akamai: Caching and Beyond
CIRCUIT 2015 - Free Beer and Testing
CIRCUIT 2015 - Responsive Websites & Grid-Based Layouts
CIRCUIT 2015 - Glimpse of perceptual diff
CIRCUIT 2015 - Orchestrate your story with interactive video and web content
How to migrate from any CMS (thru the front-door)
Maximize the power of OSGi in AEM
CIRCUIT 2015 - 10 Things Apache Sling Can Do
Circuit 2015 Keynote - Carsten Ziegeler

Recently uploaded (20)

PDF
Profit Center Accounting in SAP S/4HANA, S4F28 Col11
PDF
KodekX | Application Modernization Development
PDF
Agricultural_Statistics_at_a_Glance_2022_0.pdf
PDF
Empathic Computing: Creating Shared Understanding
PDF
Building Integrated photovoltaic BIPV_UPV.pdf
PDF
Chapter 3 Spatial Domain Image Processing.pdf
PDF
Network Security Unit 5.pdf for BCA BBA.
PDF
Electronic commerce courselecture one. Pdf
PDF
Advanced methodologies resolving dimensionality complications for autism neur...
PDF
Architecting across the Boundaries of two Complex Domains - Healthcare & Tech...
PDF
MIND Revenue Release Quarter 2 2025 Press Release
PPTX
Big Data Technologies - Introduction.pptx
PDF
Unlocking AI with Model Context Protocol (MCP)
PDF
Per capita expenditure prediction using model stacking based on satellite ima...
PPTX
sap open course for s4hana steps from ECC to s4
DOCX
The AUB Centre for AI in Media Proposal.docx
PDF
Approach and Philosophy of On baking technology
PPTX
Effective Security Operations Center (SOC) A Modern, Strategic, and Threat-In...
PDF
How UI/UX Design Impacts User Retention in Mobile Apps.pdf
PDF
Optimiser vos workloads AI/ML sur Amazon EC2 et AWS Graviton
Profit Center Accounting in SAP S/4HANA, S4F28 Col11
KodekX | Application Modernization Development
Agricultural_Statistics_at_a_Glance_2022_0.pdf
Empathic Computing: Creating Shared Understanding
Building Integrated photovoltaic BIPV_UPV.pdf
Chapter 3 Spatial Domain Image Processing.pdf
Network Security Unit 5.pdf for BCA BBA.
Electronic commerce courselecture one. Pdf
Advanced methodologies resolving dimensionality complications for autism neur...
Architecting across the Boundaries of two Complex Domains - Healthcare & Tech...
MIND Revenue Release Quarter 2 2025 Press Release
Big Data Technologies - Introduction.pptx
Unlocking AI with Model Context Protocol (MCP)
Per capita expenditure prediction using model stacking based on satellite ima...
sap open course for s4hana steps from ECC to s4
The AUB Centre for AI in Media Proposal.docx
Approach and Philosophy of On baking technology
Effective Security Operations Center (SOC) A Modern, Strategic, and Threat-In...
How UI/UX Design Impacts User Retention in Mobile Apps.pdf
Optimiser vos workloads AI/ML sur Amazon EC2 et AWS Graviton

CIRCUIT 2015 - Content API's For AEM Sites

  • 1. CIRCUIT – An Adobe Developer Event Presented by ICF Interactive Content API’s for AEM Bryan Williams ICF Interactive
  • 2. Bryan.Williams@icfi.com @brywilliams Bryan Williams ICF Interactive (10+ years) Working with CQ/AEM over 6 years AEM Developer Certified (Beta)
  • 5. What do I mean by Content API? •  Read only •  Controlled •  Possibly public but not necessarily •  Usually an afterthought •  Disclaimer: Not production code
  • 7. Why not use something else? •  Not saying you shouldn’t •  Security : Control of who can access your data •  Encapsulation : Granular control of what data is exposed •  Simplicity : The easier for the consumer to understand the better •  Conformity : Maybe you need to conform to a particular spec •  Aggregation : Some data might be coming from outside the repository •  Versioning : Backwards compatibility
  • 8. Technologies •  Bedrock –  https://github.com/Citytechinc/bedrock •  CQ Component (Maven) Plugin –  https://github.com/Citytechinc/cq-component-maven-plugin •  Sling Models –  https://sling.apache.org/documentation/bundles/models.html •  Jackson –  https://github.com/FasterXML/jackson •  Prosper –  https://github.com/Citytechinc/prosper •  Groovy –  http://www.groovy-lang.org/
  • 9. What are Bedrock, CQCP and Prosper? •  Bedrock : Open source library that contains common utilities, decorators, abstract classes, tag libraries and Javascript modules for bootstrapping and simplifying AEM projects •  CQ Component (Maven) Plugin : Generates many of the artifacts necessary for the creation of a CQ component based on the information provided by the component’s backing Java class •  Prosper : An integration testing library for AEM projects using Spock (a Groovy-based testing framework)
  • 10. Sling Models •  Automates mapping of Sling objects such as resources, request, etc. to POJOs •  Available out of the box in AEM 6
  • 11. Why Jackson? •  Popular •  Able to produce JSON or XML •  Lots of features •  We were already using it
  • 12. Groovy •  An object-oriented programming language for the Java platform •  Dynamic in nature •  Reduced syntax •  Traits
  • 14. Layers of a Content API •  Component Models : Referring to both page and content components and their corresponding backing beans •  Servlet : Takes the request and calls the appropriate service based on selectors •  Service : Responsible for getting the appropriate data and possibly caching •  Query Builder : API for making queries to the repository •  Filters : Last minute cleanup of outgoing data (externalize URLs, etc.)
  • 15. Non-Page Components •  Children of stories •  Returned in getBody() of Story model •  Custom and OOTB components must have backing bean •  Models identified by path convention circuit2015/groovy/components/page/stories/article = com.bryanw.conferences.circuit2015.groovy.components.page.stories.article.Article
  • 17. Article @Model(adaptables = Resource, adapters = [Article, Story], defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL) @Component(value = "Article", name = "article", actions = ["text:Article", "-", "edit"], group = '.hidden', path = 'groovy/components/page/stories', resourceSuperType = 'circuit2015/groovy/components/page/global', disableTargeting = true, tabs = [@Tab(title = PROPERTIES_LABEL), @Tab(title = MAIN_IMAGE_LABEL)]) @AutoInstantiate(instanceName = "article") @Slf4j("LOG") class Article extends AbstractStoryComponent implements AbstractStoryRequiredImage, SeoReadyStory { @JsonView(JacksonViews.DetailView) List<AbstractCircuit2015Component> getBody() { Optional<ComponentNode> mainParNode = getComponentNode(MAIN_PAR) if (mainParNode.present) { List<AbstractCircuit2015Component> components = mainParNode.get().componentNodes.collect { it.resource.adaptTo(AbstractCircuit2015Component) } - null return components } [] } } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 18. AbstractStoryComponent abstract class AbstractStoryComponent extends AbstractCircuit2015Component implements Story { protected static final String MAIN_PAR = "mainpar" @Inject private PageManager pageManager @Inject @Named('jcr:title') @DialogField(fieldLabel = "Title", name = './jcr:title', required = true, tab = PROPERTIES_INDEX) @TextField String title @Inject @Named('jcr:description') @DialogField(fieldLabel = "Description", name = './jcr:description', tab = PROPERTIES_INDEX) @TextArea @JsonView(JacksonViews.DetailView) String description @Inject @DialogField(fieldLabel = "Published date", name = './publishedDate', required = true, tab = PROPERTIES_INDEX) @DateTime Date publishedDate @Inject @DialogField(fieldLabel = "Author Bio Path", name = "./authorBioPath", tab = PROPERTIES_INDEX) @PathField @JsonIgnore String authorBioPath @JsonView(JacksonViews.DetailView) Link getStoryLink() { pageManager.getContainingPage(resource).adaptTo(ComponentNode).link } @JsonView(JacksonViews.ListView) String getStoryHref() { pageManager.getContainingPage(resource).adaptTo(ComponentNode).href } @JsonView(JacksonViews.DetailView) Bio getAuthorBio() { pageManager.getPage(authorBioPath)?.getContentResource()?.adaptTo(Bio) } } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 19. AbstractStoryComponent abstract class AbstractStoryComponent extends AbstractCircuit2015Component implements Story { protected static final String MAIN_PAR = "mainpar" @Inject private PageManager pageManager @Inject @Named('jcr:title') @DialogField(fieldLabel = "Title", name = './jcr:title', required = true, tab = PROPERTIES_INDEX) @TextField String title @Inject @Named('jcr:description') @DialogField(fieldLabel = "Description", name = './jcr:description', tab = PROPERTIES_INDEX) @TextArea @JsonView(JacksonViews.DetailView) String description @Inject @DialogField(fieldLabel = "Published date", name = './publishedDate', required = true, tab = PROPERTIES_INDEX) @DateTime Date publishedDate ... Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 20. AbstractStoryComponent … @Inject @DialogField(fieldLabel = "Author Bio Path", name = "./authorBioPath", tab = PROPERTIES_INDEX) @PathField @JsonIgnore String authorBioPath @JsonView(JacksonViews.DetailView) Link getStoryLink() { pageManager.getContainingPage(resource).adaptTo(ComponentNode).link } @JsonView(JacksonViews.ListView) String getStoryHref() { pageManager.getContainingPage(resource).adaptTo(ComponentNode).href } @JsonView(JacksonViews.DetailView) Bio getAuthorBio() { pageManager.getPage(authorBioPath)?.getContentResource()?.adaptTo(Bio) } } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 21. AbstractStoryComponent abstract class AbstractStoryComponent extends AbstractCircuit2015Component implements Story { protected static final String MAIN_PAR = "mainpar" @Inject private PageManager pageManager @Inject @Named('jcr:title') @DialogField(fieldLabel = "Title", name = './jcr:title', required = true, tab = PROPERTIES_INDEX) @TextField String title @Inject @Named('jcr:description') @DialogField(fieldLabel = "Description", name = './jcr:description', tab = PROPERTIES_INDEX) @TextArea @JsonView(JacksonViews.DetailView) String description @Inject @DialogField(fieldLabel = "Published date", name = './publishedDate', required = true, tab = PROPERTIES_INDEX) @DateTime Date publishedDate @Inject @DialogField(fieldLabel = "Author Bio Path", name = "./authorBioPath", tab = PROPERTIES_INDEX) @PathField @JsonIgnore String authorBioPath @JsonView(JacksonViews.DetailView) Link getStoryLink() { pageManager.getContainingPage(resource).adaptTo(ComponentNode).link } @JsonView(JacksonViews.ListView) String getStoryHref() { pageManager.getContainingPage(resource).adaptTo(ComponentNode).href } @JsonView(JacksonViews.DetailView) Bio getAuthorBio() { pageManager.getPage(authorBioPath)?.getContentResource()?.adaptTo(Bio) } } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 23. AbstractStoryOptionalImage trait AbstractStoryRequiredImage implements ComponentNode { @Inject @Named('mainImageCaption') @DialogField(fieldLabel = 'Main Image Caption', name = 'mainImageCaption', fieldName = 'mainImageCaption', tab = MAIN_IMAGE_INDEX, ranking = 200D, additionalProperties = [@Property(name = 'name', value = ’.mainImageCaption')]) @TextField private String mainImageCaption @Inject @ImageInject(path = 'mainImage') @DialogField(fieldLabel = 'Main Image', name = 'mainImage', fieldName = 'mainImage', tab = MAIN_IMAGE_INDEX, required = true, ranking = 201D, additionalProperties = [@Property(name = "name", value = './mainImage')]) @Html5SmartImage(allowUpload = false, name = "mainImage", tab = false, height = 400) private Image mainImage public String getMainImageCaption() { mainImageCaption } public Circuit2015Image getMainImage() { mainImage ? new Circuit2015Image(src: mainImage.src) : null } } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 24. AbstractStoryRequiredImage trait AbstractStoryRequiredImage implements ComponentNode { @Inject @Named('mainImageCaption') @DialogField(fieldLabel = 'Main Image Caption', name = 'mainImageCaption', fieldName = 'mainImageCaption', tab = MAIN_IMAGE_INDEX, ranking = 200D, required = true, additionalProperties = [@Property(name = 'name', value = './mainImageCaption')]) @TextField private String mainImageCaption @Inject @ImageInject(path = 'mainImage') @DialogField(fieldLabel = 'Main Image', name = 'mainImage', fieldName = 'mainImage', tab = MAIN_IMAGE_INDEX, required = true, ranking = 201D, additionalProperties = [@Property(name = "name", value = './mainImage')]) @Html5SmartImage(allowUpload = false, name = "mainImage", tab = false, height = 400) private Image mainImage public String getMainImageCaption() { mainImageCaption } public Circuit2015Image getMainImage() { mainImage ? new Circuit2015Image(src: mainImage.src) : null } } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 26. Content API Servlet •  Accepts page paths only •  Accepts XML/JSON extension •  Accepts “story” and “stories” selector •  Constructs search parameters •  Passes current path and search parameters on to service
  • 27. ContentApiServlet @SlingServlet(resourceTypes = [ NameConstants.NT_PAGE ], selectors = [ "stories", "story" ], extensions = [ EXTENSION_JSON, "xml" ], methods = [ "GET" ]) @Slf4j("LOG") public class ContentApiServlet extends XmlOrJsonResponseServlet { @Reference ContentApiService contentApiService @Override protected final void doGet(final SlingHttpServletRequest slingRequest, final SlingHttpServletResponse slingResponse) { RequestPathInfo requestPathInfo = slingRequest.requestPathInfo slingResponse.setHeader("Cache-Control", "no-cache, no-store, must-revalidate") slingResponse.setHeader("Expires", "0"); StorySearchParameters storySearchParameters = buildStorySearchParameters(slingRequest) if (requestPathInfo.selectors.contains('stories')) { writeResponse(slingResponse, requestPathInfo.extension, JacksonViews.ListView, contentApiService.getStories(storySearchParameters)) } else { writeResponse(slingResponse, requestPathInfo.extension, JacksonViews.DetailView, contentApiService.getStory(slingRequest.resource)) } } static private StorySearchParameters buildStorySearchParameters(final SlingHttpServletRequest slingHttpServletRequest) { StorySearchParameters storySearchParameters = new StorySearchParameters() storySearchParameters.setBaseResource(slingHttpServletRequest.resource) slingHttpServletRequest.parameterMap.each { key, value -> if (key == 'type') { storySearchParameters.setType(resolveStoryTypeClass(slingHttpServletRequest.getParameter('type'))) } else { storySearchParameters[key as String] = (value as String[])[0] } } storySearchParameters } static private Class<? extends Story> resolveStoryTypeClass(String type) { Class<? extends Story> storyTypeClass storyTypeClass = type ? Class.forName("com.bryanw.conferences.circuit2015.groovy.components.page.stories.${type}.$ {type.capitalize()}").asSubclass(Story) : Story storyTypeClass } } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 28. ContentApiServlet.doGet() @Override protected final void doGet(final SlingHttpServletRequest slingRequest, final SlingHttpServletResponse slingResponse) { RequestPathInfo requestPathInfo = slingRequest.requestPathInfo slingResponse.setHeader("Cache-Control", "no-cache, no-store, must-revalidate") slingResponse.setHeader("Expires", "0"); StorySearchParameters storySearchParameters = buildStorySearchParameters(slingRequest) if (requestPathInfo.selectors.contains('stories')) { writeResponse(slingResponse, requestPathInfo.extension, JacksonViews.ListView, contentApiService.getStories(storySearchParameters)) } else { writeResponse(slingResponse, requestPathInfo.extension, JacksonViews.DetailView, contentApiService.getStory(slingRequest.resource)) } } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 29. ContentApiServlet.buildStorySearchParameters() static private StorySearchParameters buildStorySearchParameters( final SlingHttpServletRequest slingRequest) { StorySearchParameters searchParams = new StorySearchParameters() storySearchParameters.setBaseResource(slingRequest.resource) slingRequest.parameterMap.each { key, value -> if (key == 'type') { String typeParam = slingRequest.getParameter('type') searchParams.setType(resolveStoryTypeClass(typeParam)) } else { searchParams[key as String] = (value as String[])[0] } } searchParams } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 30. ContentApiServlet.resolveStoryTypeClass() private Class<? extends Story> resolveStoryTypeClass(String type) { Class<? extends Story> storyTypeClass String packagePrefix = 'com.bryanw.conferences.circuit2015.groovy.components.page.stories' storyTypeClass = type ? Class .forName("${packagePrefix}.${type}.${type.capitalize()}") .asSubclass(Story) : Story storyTypeClass } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 31. StorySearchParameters @EqualsAndHashCode class StorySearchParameters { Class<Story> type Resource baseResource String text String start String limit Map<String, String> searchables = [:] def propertyMissing(String name, value) { searchables[name] = value } } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 32. XmlOrJsonResponseServlet @Slf4j("LOG") class XmlOrJsonResponseServlet extends SlingAllMethodsServlet { public static final String DEFAULT_DATE_FORMAT = "MM/dd/yyyy hh:mm aaa z"; private static final DateFormat MAPPER_DATE_FORMAT = new SimpleDateFormat(DEFAULT_DATE_FORMAT, Locale.US) private static final XmlFactory XML_FACTORY = new XmlFactory() public void writeResponse(final SlingHttpServletResponse response, final String extension, final Class<JacksonViews.View> view, final Object object) { if ("xml" == extension) { XmlMapper xmlMapper = new XmlMapper().setDateFormat(MAPPER_DATE_FORMAT) as XmlMapper writeXmlResponse(response, xmlMapper, view, object) } else { ObjectMapper jsonMapper = new ObjectMapper().setDateFormat(MAPPER_DATE_FORMAT) writeJsonResponse(response, jsonMapper, view, object) } } protected static void writeXmlResponse(final SlingHttpServletResponse response, final XmlMapper xmlMapper, final Class<JacksonViews.View> view, final Object object) { MediaType mediaType = MediaType.XML_UTF_8 response.setContentType(mediaType.withoutParameters().toString()) response.setCharacterEncoding(mediaType.charset().get().name()) final ToXmlGenerator generator = XML_FACTORY.createGenerator(response.getWriter()) xmlMapper.writerWithView(view).writeValue(generator, object) } protected void writeJsonResponse(final SlingHttpServletResponse response, final ObjectMapper mapper, final Class<JacksonViews.View> view, final Object object) throws IOException { response.setContentType(MediaType.JSON_UTF_8.withoutParameters().toString()); response.setCharacterEncoding(MediaType.JSON_UTF_8.charset().get().name()); final JsonGenerator generator = new JsonFactory().disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET) .createGenerator(response.getWriter()); mapper.writerWithView(view).writeValue(generator, object); } } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 33. XmlOrJsonResponseServlet.writeResponse() public void writeResponse( final SlingHttpServletResponse response, final String extension, final Class<JacksonViews.View> view, final Object object) { if ("xml" == extension) { XmlMapper xmlMapper = new XmlMapper() .setDateFormat(MAPPER_DATE_FORMAT) as XmlMapper writeXmlResponse(response, xmlMapper, view, object) } else { ObjectMapper jsonMapper = new ObjectMapper() .setDateFormat(MAPPER_DATE_FORMAT) writeJsonResponse(response, jsonMapper, view, object) } } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 34. XmlOrJsonResponseServlet.writeXmlResponse() protected void writeXmlResponse( final SlingHttpServletResponse response, final XmlMapper xmlMapper, final Class<JacksonViews.View> view, final Object object) { MediaType mediaType = MediaType.XML_UTF_8 response.setContentType( mediaType.withoutParameters().toString()) response.setCharacterEncoding( mediaType.charset().get().name()) ToXmlGenerator generator = XML_FACTORY .createGenerator(response.getWriter()) xmlMapper.writerWithView(view) .writeValue(generator, object) } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 35. XmlOrJsonResponseServlet.writeJsonResponse() protected void writeJsonResponse( final SlingHttpServletResponse response, final ObjectMapper mapper, final Class<JacksonViews.View> view, final Object object){ MediaType mediaType = MediaType.JSON_UTF_8 response.setContentType( mediaType.withoutParameters().toString()); response.setCharacterEncoding( mediaType.charset().get().name()); JsonGenerator generator = new JsonFactory() .disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET) .createGenerator(response.getWriter()); mapper.writerWithView(view) .writeValue(generator, object); } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 36. Content API Service •  Calls repository layer •  (Guava) caching
  • 37. DefaultContentApiService @Component @Service(ContentApiService) @Slf4j("LOG") class DefaultContentApiService extends AbstractCacheService implements ContentApiService { @Reference private ContentApiRepository contentApiRepository @Override StorySearchResult getStories(StorySearchParameters storySearchParameters) { // Caching should go here contentApiRepository.search(storySearchParameters) } @Override Story getStory(Resource storyResource) { storyResource?.getChild(JcrConstants.JCR_CONTENT)?.adaptTo(Story) } @Override protected Logger getLogger() { LOG } } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 38. Content API Repository Layer •  Constructs proper QueryBuilder query •  Instantiates appropriate models from results
  • 39. DefaultContentApiRepository @Component @Service(ContentApiRepository) @Slf4j("LOG") class DefaultContentApiRepository implements ContentApiRepository { @Reference private QueryBuilder queryBuilder @Override public StorySearchResult search(StorySearchParameters storySearchParameters) { Session session = storySearchParameters.baseResource.resourceResolver.adaptTo(Session) String start = storySearchParameters.start ? storySearchParameters.start : '0' String limit = storySearchParameters.limit ? storySearchParameters.limit : '100' PredicateGroup mainGroup = new PredicateGroup(); mainGroup.add(new Predicate("path").set("path", storySearchParameters.baseResource.path)) mainGroup.add(new Predicate("type").set("type", "cq:PageContent")) mainGroup.add(new Predicate("property").set(Predicate.ORDER_BY, "publishedDate")) mainGroup.add(new Predicate("property").set("${Predicate.PARAM_SORT}.${Predicate.PARAM_SORT}", Predicate.SORT_DESCENDING)) mainGroup.add(new Predicate("property").set(Predicate.PARAM_OFFSET, start)) mainGroup.add(new Predicate("property").set(Predicate.PARAM_LIMIT, limit)) if (storySearchParameters.text) { mainGroup.add(new Predicate("fulltext").set("fulltext", storySearchParameters.text)) } mainGroup.add(createResourceTypePredicate(storySearchParameters)) storySearchParameters.searchables.each { key, value -> // Add whatever other search predicates you want to allow here } Query query = queryBuilder.createQuery(mainGroup, session); SearchResult searchResult = query.getResult(); StorySearchResult storyResult = new StorySearchResult(); storyResult.stories = searchResult.hits.collect { it.resource.adaptTo(Story) } - null storyResult.setTotalResults(searchResult.totalMatches); storyResult.setStart(start as Long); storyResult } private Predicate createResourceTypePredicate(StorySearchParameters storySearchParameters) { Predicate resourceTypePredicate = new Predicate("property") resourceTypePredicate.set("property", SLING_RESOURCE_TYPE_PROPERTY) if (!storySearchParameters.type || storySearchParameters.type.name == Story.name) { resourceTypePredicate.set("operation", "like") resourceTypePredicate.set("value", "circuit2015/groovy/components/page/stories/%") } else { //String typeName = storySearchParameters.baseResource.class.simpleName.toLowerCase() String typeName = storySearchParameters.type.simpleName resourceTypePredicate.set("value", "circuit2015/groovy/components/page/stories/${typeName.toLowerCase()}") } resourceTypePredicate } } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 40. DefaultContentApiRepository.search() @Override public StorySearchResult search(StorySearchParameters storySearchParameters) { Session session = storySearchParameters.baseResource.resourceResolver.adaptTo(Session) String start = storySearchParameters.start ? storySearchParameters.start : '0' String limit = storySearchParameters.limit ? storySearchParameters.limit : '100' PredicateGroup mainGroup = new PredicateGroup(); mainGroup.add(new Predicate("path").set("path", storySearchParameters.baseResource.path)) mainGroup.add(new Predicate("type").set("type", "cq:PageContent")) mainGroup.add(new Predicate("property").set(Predicate.ORDER_BY, "publishedDate")) mainGroup.add(new Predicate("property").set("${Predicate.PARAM_SORT}.${Predicate.PARAM_SORT}", Predicate.SORT_DESCENDING)) mainGroup.add(new Predicate("property").set(Predicate.PARAM_OFFSET, start)) mainGroup.add(new Predicate("property").set(Predicate.PARAM_LIMIT, limit)) if (storySearchParameters.text) { mainGroup.add(new Predicate("fulltext").set("fulltext", storySearchParameters.text)) } mainGroup.add(createResourceTypePredicate(storySearchParameters)) storySearchParameters.searchables.each { key, value -> // Add whatever other search predicates you want to allow here } Query query = queryBuilder.createQuery(mainGroup, session); SearchResult searchResult = query.getResult(); StorySearchResult storyResult = new StorySearchResult(); storyResult.stories = searchResult.hits.collect { it.resource.adaptTo(Story) } - null storyResult.setTotalResults(searchResult.totalMatches); storyResult.setStart(start as Long); storyResult } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 41. DefaultContentApiRepository.createResourceTypePredicate() private Predicate createResourceTypePredicate( StorySearchParameters storySearchParameters) { Predicate resourceTypePredicate = new Predicate("property") resourceTypePredicate.set("property”,SLING_RESOURCE_TYPE_PROPERTY) if (!storySearchParameters.type || storySearchParameters.type.name == Story.name) { resourceTypePredicate.set("operation", "like") resourceTypePredicate.set("value", "circuit2015/groovy/components/page/stories/%") } else { String typeName = storySearchParameters.type.simpleName resourceTypePredicate.set("value", "circuit2015/groovy/components/page/stories/$ {typeName.toLowerCase()}") } resourceTypePredicate } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 42. StorySearchResult @EqualsAndHashCode @XmlRootElement(name = "result") class StorySearchResult { List<Story> stories long totalResults long start } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 43. ResourceTypeImplementationPicker @Component @Service(ImplementationPicker) @Property(name = Constants.SERVICE_RANKING, intValue = 1000) @Slf4j("LOG") public class ResourceTypeImplementationPicker implements ImplementationPicker { public Class pick(Class adapterType, Class[] implTypes, Object adaptable) { Class pickedClass = null if (adaptable instanceof Resource) { Resource resource = adaptable as Resource String resourceType = resource.getResourceType() pickedClass = implTypes.find { if (it instanceof AbstractCircuit2015Component) { (it as AbstractCircuit2015Component).conventionalResourceType() == resourceType } } } pickedClass } } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 44. ClassNameImplementationPicker @Component @Service(ImplementationPicker) @Property(name = Constants.SERVICE_RANKING, intValue = 1001) @Slf4j("LOG") class ClassNameImplementationPicker implements ImplementationPicker { public Class pick(Class adapterType, Class[] implTypes, Object adaptable) { Class classNameClass = null if (adaptable instanceof Resource) { Resource resource = adaptable as Resource String className = resource.properties?.get("className") classNameClass = implTypes.find { it.name == className } } classNameClass } } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  • 45. http://mydomain.com/circuit-2015-demo.stories.json { "stories": [ { "title": "CIRCUIT Promo Video", "publishedDate": "08/12/2015 10:49 AM CDT", "seoTitle": "CIRCUIT Promo Video", "seoDescription": "Maximas vero virtutes iacere omnis necesse", "mainImage": { "src": "/content/dam/circuit-2015-demo/images/promo.png" }, "mainImageCaption": "CIRCUIT Promo Video Image", "index": 0, "storyHref": "/content/circuit-2015-demo/circuit-promo-video.html" ”videoHref": "https://www.youtube.com/watch?v=r2mFb1dIiug" }, { "title": "Article 1", "publishedDate": "08/06/2015 01:21 AM CDT", "seoTitle": "Article One", "seoDescription": "Maximas vero virtutes iacere omnis necesse", "mainImage": { "src": "/content/dam/circuit-2015-demo/images/article1banner.jpg" }, "mainImageCaption": "Article 1 Banner Image", "index": 0, "storyHref": "/content/circuit-2015-demo/article-1.html" } ], "totalResults": 2, "start": 0 }
  • 46. http://mydomain.com/circuit-2015-demo.stories.json?type=article { "stories": [ { "title": "Article 1", "publishedDate": "08/06/2015 01:21 AM CDT", "seoTitle": "Article One", "seoDescription": "Maximas vero virtutes iacere omnis necesse", "mainImage": { "src": "/content/dam/circuit-2015-demo/images/article1banner.jpg" }, "mainImageCaption": "Article 1 Banner Image", "index": 0, "storyHref": "/content/circuit-2015-demo/article-1.html" } ], "totalResults": 1, "start": 0 }
  • 47. http://mydomain.com/circuit-2015-demo/article-1.story.json { "title": "Article 1", "description": "Maximas vero virtutes iacere omnis necesse", "publishedDate": "08/06/2015 01:21 AM CDT", "seoTitle": "Article One", "seoDescription": "Maximas vero virtutes iacere omnis necesse", "body": [ { "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.n", "index": 0 } ], "mainImage": { "src": "/content/dam/circuit-2015-demo/images/article1banner.jpg" }, "mainImageCaption": "Article 1 Banner Image", "index": 0, "authorBio": { "firstName": "Bryan", "lastName": "Williams", "twitter": "@brywilliams", "description": "Maximas vero virtutes iacere omnis necesse", "mainImage": { "src": "/content/dam/circuit-2015-demo/images/bryanw-profile.png" }, "mainImageCaption": "Bryan Williams Bio Image", "index": 0 }, "storyLink": { "path": "/content/circuit-2015-demo/article-1", "extension": "html", "suffix": "", "href": "/content/circuit-2015-demo/article-1.html", "selectors": [ ], "queryString": "", "external": false, "target": "_self", "title": "", "properties": { }, "empty": false } }
  • 48. Versioning •  Like I said, not OOTB for Jackson •  Need something like @Since in GSON •  Jackson Filters
  • 50. Testing with Prosper •  Integration testing using Spock/Groovy •  Specifically for AEM testing •  Builders for Nodes/Pages •  Uses Sling Mocks
  • 51. Publish vs Author •  May want internal apps to access author •  replicatedDate is not what it seems
  • 52. Conclusions •  Bedrock –  https://github.com/Citytechinc/bedrock –  Provided us with useful classes, annotations and model injectors •  CQ Component (Maven) Plugin –  https://github.com/Citytechinc/cq-component-maven-plugin –  Allowed us to create dialogs without writing a single XML file •  Sling Models –  https://sling.apache.org/documentation/bundles/models.html –  Wired up our backing beans for us •  Jackson –  https://github.com/FasterXML/jackson –  Let us define the details of bean to JSON/XML conversion •  Prosper –  https://github.com/Citytechinc/prosper –  Simplified tests •  Groovy –  http://www.groovy-lang.org/ –  Less code