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
?	
  
What do I mean by Content API?
•  Read only
•  Controlled
•  Possibly public but not necessarily
•  Usually an afterthought
•  Disclaimer: Not production code
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

CIRCUIT 2015 - Content API's For AEM Sites

  • 1.
    CIRCUIT – AnAdobe 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)
  • 3.
  • 5.
    What do Imean by Content API? •  Read only •  Controlled •  Possibly public but not necessarily •  Usually an afterthought •  Disclaimer: Not production code
  • 7.
    Why not usesomething 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 •  Automatesmapping 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-orientedprogramming language for the Java platform •  Dynamic in nature •  Reduced syntax •  Traits
  • 13.
  • 14.
    Layers of aContent 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 •  Childrenof 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
  • 16.
  • 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 AbstractStoryComponentextends 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 AbstractStoryComponentextends 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 = "AuthorBio 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 AbstractStoryComponentextends 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  
  • 22.
  • 23.
    AbstractStoryOptionalImage trait AbstractStoryRequiredImage implementsComponentNode { @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 implementsComponentNode { @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  
  • 25.
  • 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 voiddoGet(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 StorySearchParametersbuildStorySearchParameters( 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<? extendsStory> 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 extendsSlingAllMethodsServlet { 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( finalSlingHttpServletResponse 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( finalSlingHttpServletResponse 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( finalSlingHttpServletResponse 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 extendsAbstractCacheService 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 RepositoryLayer •  Constructs proper QueryBuilder query •  Instantiates appropriate models from results
  • 39.
    DefaultContentApiRepository @Component @Service(ContentApiRepository) @Slf4j("LOG") class DefaultContentApiRepository implementsContentApiRepository { @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(StorySearchParametersstorySearchParameters) { 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( StorySearchParametersstorySearchParameters) { 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") classStorySearchResult { 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": "CIRCUITPromo 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": "Article1", "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 Isaid, not OOTB for Jackson •  Need something like @Since in GSON •  Jackson Filters
  • 49.
  • 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
  • 53.