CIRCUIT 2015 - Content API's For AEM Sites
-
Upload
circuit -
Category
Technology
-
view
55 -
download
3
Transcript of CIRCUIT 2015 - Content API's For AEM Sites
CIRCUIT – An Adobe Developer Event Presented by ICF Interactive
Content API’s for AEM
Bryan Williams
ICF Interactive
@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 [email protected] @brywilliams