Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

CIRCUIT 2015 - Content API's For AEM Sites

789 views

Published on

Bryan Williams - ICF Interactive
Many sites need to expose their AEM repository content through a flexible remote API whether it be for consumption by mobile apps, third parties, etc. This presentation will walk through setting up a custom, extensible, secure and testable API utilizing various open source tools that are at your disposal.

Published in: Technology
  • Be the first to comment

CIRCUIT 2015 - Content API's For AEM Sites

  1. 1. CIRCUIT – An Adobe Developer Event Presented by ICF Interactive Content API’s for AEM Bryan Williams ICF Interactive
  2. 2. Bryan.Williams@icfi.com @brywilliams Bryan Williams ICF Interactive (10+ years) Working with CQ/AEM over 6 years AEM Developer Certified (Beta)
  3. 3. Prize Question #1 ?  
  4. 4. What do I mean by Content API? •  Read only •  Controlled •  Possibly public but not necessarily •  Usually an afterthought •  Disclaimer: Not production code
  5. 5. 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
  6. 6. 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/
  7. 7. 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)
  8. 8. Sling Models •  Automates mapping of Sling objects such as resources, request, etc. to POJOs •  Available out of the box in AEM 6
  9. 9. Why Jackson? •  Popular •  Able to produce JSON or XML •  Lots of features •  We were already using it
  10. 10. Groovy •  An object-oriented programming language for the Java platform •  Dynamic in nature •  Reduced syntax •  Traits
  11. 11. Prize Question #2 ?:
  12. 12. 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.)
  13. 13. 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
  14. 14. Sample Article Page
  15. 15. 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  
  16. 16. 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  
  17. 17. 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  
  18. 18. 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  
  19. 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 @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  
  20. 20. Paul Michelotti Blog h;p://citytechinc.com/us/en/blog/2015/03/groovy-­‐component-­‐composiHon-­‐with-­‐traits.html   Groovy  Component  ComposiHon  With  Traits    
  21. 21. 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  
  22. 22. 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  
  23. 23. 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  
  24. 24. 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
  25. 25. 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  
  26. 26. 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  
  27. 27. 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  
  28. 28. 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  
  29. 29. 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  
  30. 30. 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  
  31. 31. 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  
  32. 32. 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  
  33. 33. 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  
  34. 34. Content API Service •  Calls repository layer •  (Guava) caching
  35. 35. 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  
  36. 36. Content API Repository Layer •  Constructs proper QueryBuilder query •  Instantiates appropriate models from results
  37. 37. 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  
  38. 38. 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  
  39. 39. 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  
  40. 40. StorySearchResult @EqualsAndHashCode @XmlRootElement(name = "result") class StorySearchResult { List<Story> stories long totalResults long start } Bedrock    CQ  Component  Plugin   Sling  Models    Jackson  
  41. 41. 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  
  42. 42. 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  
  43. 43. 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 }
  44. 44. 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 }
  45. 45. 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 } }
  46. 46. Versioning •  Like I said, not OOTB for Jackson •  Need something like @Since in GSON •  Jackson Filters
  47. 47. Filters •  Encoding •  Externalizing URLs
  48. 48. Testing with Prosper •  Integration testing using Spock/Groovy •  Specifically for AEM testing •  Builders for Nodes/Pages •  Uses Sling Mocks
  49. 49. Publish vs Author •  May want internal apps to access author •  replicatedDate is not what it seems
  50. 50. 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
  51. 51. Bryan Williams ICF Interactive Bryan.Williams@icfi.com @brywilliams

×