[{"content":"I think that every developer from AEM/Sling world has already read many articles regarding switching to the R6 and R7 OSGi annotations. But new annotations do not only allow us to write less boilerplate code, but also bring new caveats and we better be aware of them.\nSeveral days ago there was suggested a PR for AC Tool which fixes missing referenced service in AEM 6.4, even though this service was shown as satisfied in Web Console. The fix contained next change:\nKonrad Windszus identified that this issue was the result of switching to the official OSGi annotations in AC Tool. This switch changed method injection to field injection and OSGi R7 Specification in 112.3.9.1 states next:\nFor a dynamic reference, the field must be declared with the volatile modifier so that field value changes made by SCR are visible to other threads. If the field is not declared with the volatile modifier, SCR must log an error message with the Log Service, if present, and the field must not be modified.\nThis perfectly describes why the field was not initialized. But this is not the only problem you may hit while switching to OSGi annotations - there are 2 more related excerpts worth mentioning:\nIf the field is declared with the final modifier, SCR must log an error message with the Log Service, if present, and the field must not be modified.\nFor a reference with multiple cardinality, the type of the field must be Collection or List. If the field has a different type, SCR must log an error message with the Log Service, if present, and the field must not be modified.\nHow to survive? Luckily for us, next release (4.2.0) of Bnd (including bnd-maven-plugin) will contain corresponding checks, so we will be notified about such problems at early stage - during the build.\nThis is another example which proves that reading specification is not a waste of time but a way to prevent such issues from happening.\nSo don\u0026rsquo;t forget to read specifications as well as update your dependencies as soon as 4.2.0 is released.\n","permalink":"https://taradevko.com/2019/01-16-migration-osgi-annotations-caveats-sling-aem/","summary":"\u003cp\u003eI think that every developer from AEM/Sling world has already read many articles regarding switching to the \u003ca href=\"http://www.nateyolles.com/blog/2017/05/osgi-declarative-services-annotations-in-aem\"\u003eR6\u003c/a\u003e and \u003ca href=\"http://blogs.adobe.com/experiencedelivers/experience-management/osgi/using-osgi-r7-annotations-aem/\"\u003eR7\u003c/a\u003e OSGi annotations. But new annotations do not only allow us to write less boilerplate code, but also bring new caveats and we better be aware of them.\u003c/p\u003e\n\u003cp\u003eSeveral days ago there was suggested a \u003ca href=\"https://github.com/Netcentric/accesscontroltool/pull/346\"\u003ePR\u003c/a\u003e for \u003ca href=\"https://github.com/Netcentric/accesscontroltool\"\u003eAC Tool\u003c/a\u003e which fixes missing referenced service in AEM 6.4, even though this service was shown as satisfied in Web Console. The fix contained next change:\u003c/p\u003e","title":"OSGi: migration to OSGi annotations caveats"},{"content":"Before I learned about AC Tool, I worked with a couple of projects several years ago - they were quite big and had many markets and locales. And, as you can imagine, these projects had quite a sophisticated hierarchical structure of users and groups. Time to time, we were rolling out content for new markets/locales, what required the creation of a bunch of groups and setting of correct permissions to them. This routine required one developer to be fully focused on this job for several hours until everything was created, assigned and activated. Then, we had to synchronize made changes among all environments, and later, sometimes, fix issues which we made because of human mistake. If only we had Access Control Tool that days.\nWhat is Access Control Tool? Access Control Tool (or simply AC Tool) developed by Netcentric allows us to specify complex Access Control Lists in separate files in yaml format. With it, we are able to create and configure groups and users. Furthermore, it can export all existing AC entries in the system, so migration to AC Tool is really easy!\nWhy do you need AC Tool AC Tool provides a great set of features which we can and should use. It allows not only persisting of ACLs inside the project but also it represents these ACLs in human readable yaml format. Having ACLs defined in the sources allows testing it as we test any other thing in the project according to a development/release circle. Some small set of what we can do with this tool:\ndefine ACLs; create users (including name, password, groups and profile content); define groups (like name, path, members); provide different ac configurations for different run modes; clean up repository by purging obsolete authorisables; use loops which are derived from a content structure. A good comparison with alternatives can be found here.\nHow may look the project setup with AC Tool First of all - we need to install the package with the tool into AEM instance. We can grab the package from maven repository https://repo1.maven.org/maven2/biz/netcentric/\u0026hellip; Unfortunately, there is no good wat so far to embed it into our configuration (or other) package and have is installed automatically.\nNow we need to select a way we want to apply our yaml configuration files to AEM instance: most probably you use content-package-maven-plugin in your project and it would be a good idea to go with. Put your yaml files into the package module and configure the plugin:\n\u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;com.day.jcr.vault\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;content-package-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;installhook.actool.class\u0026gt;biz.netcentric.cq.tools.actool.installhook.AcToolInstallHook\u0026lt;/installhook.actool.class\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; For other ways of configurations installing check this page.\nMigrating to Access Control Tool Now we need to setup configuration which represents our current state in AEM. You have tens/hundreds of groups and you think it will take days to do this? NOPE! You just need to use export feature of AC Tool and get yaml files generated for you.\nGo to _/system/console/_jmx and navigate to ACTool bean.\nTrigger export based on groups clicking on groupBasedDump(). You will get a dump of all groups and related to them ACEs.\nNow you can copy rules to the package. Make sure to clean up configuration - most probably, you don\u0026rsquo;t want to manage most of OOTB groups (e.g.: \u0026ldquo;administrators\u0026rdquo;) with AC Tool. More info on this topic is available on GitHub.\nOrganizing configuration in the project Storing groups definitions and their ACEs together in one file does not much help to increase maintainability of our AEM system. So we need to split them into files and folders. One of the approaches I will describe below.\nGlobal fragments We define two fragments - one denies access to all the content and features within AEM and another gives permissions to the resources which allow basic usage of the platform (e.g. /var, /libs). Later, each role will be a member of both these fragment groups.\nFor the demo purposes, we will only restrict access to /content section and to Projects Console. In a real project, you will want to have a complete ACEs list for all the futures in AEM.\n# File /apps/taradevko/acl-configuration/fragments.author/platform-access.yaml - group_config: - fragment-restrict-basic: - path: /home/groups/taradevko/fragments - fragment-allow-basic: - path: /home/groups/taradevko/fragments - ace_config: - fragment-restrict-basic: # Restrict access to the content - path: /content permission: deny actions: privileges: jcr:read,jcr:readAccessControl repGlob: - path: /content permission: allow actions: privileges: jcr:read,jcr:readAccessControl repGlob: \u0026#34;\u0026#34; - path: /content permission: allow actions: privileges: jcr:read,jcr:readAccessControl repGlob: /jcr:* # Here you may want to disable access to all the main editing consoles and content. # For the demo purposes we just disable it for Projects console - path: /libs/cq/core/content/nav/projects permission: deny actions: read - path: /libs/cq/core/content/projects permission: deny actions: read - path: /content/projects permission: deny actions: read # - fragment-allow-basic: # Here you may want to allow basic to the platform, e.g. /libs, /var and default design. Configuration attributes are more or less self-explaining but to get a good overview of available attributes and their possible values you can refer to the official documentation. Also, note that folder contains \u0026ldquo;.author\u0026rdquo; at the end, what tells the tool to apply this configuration only on author AEM instance. As long, as we do not deny access to the basic functionality we have nothing in ace_config section for fragment-allow-basic.\nFunctional fragments Now we want to define functional fragments which allow accessing features we denied above. In our case, we need to define only one - for the Projects Console.\n# File /apps/taradevko/acl-configuration/fragments.author/authoring-fragments.yaml - group_config: - fragment-projects: - path: /home/groups/taradevko/fragments - ace_config: - fragment-projects: - path: /libs/cq/core/content/nav/projects permission: allow actions: read - path: /libs/cq/core/content/projects permission: allow actions: read - path: /content/projects permission: allow actions: read Content fragments We also want to define fragments for accessing content (as we may have several projects and/or markets/languages in one AEM installation and different roles should have access to different parts of the content. In this example, we will use default We Retail site as a target.\n# File /apps/taradevko/acl-configuration/fragments.author/content-fragments.yaml - group_config: - fragment-weretail: - path: /home/groups/taradevko/fragments - ace_config: - fragment-weretail: - path: /content/we-retail permission: allow actions: read User Role definitions Finally, we define user roles for our project.\n# File /apps/taradevko/acl-configuration/taradevko.author/roles.yaml # All roles are defined here - group_config: - group-taradevko-project-viewer: - path: /home/groups/taradevko isMemberOf: fragment-restrict-basic,fragment-allow-basic,contributor,fragment-projects - group-taradevko-content-viewer: - path: /home/groups/taradevko isMemberOf: fragment-restrict-basic,fragment-allow-basic,contributor,fragment-weretail In the configuration above we defined 2 roles - one for viewing projects and another for viewing We.Retail\u0026rsquo;s content. It\u0026rsquo;s probably not the best real-world example but it\u0026rsquo;s a good demonstration of the approach.\nAs stated before, for the sake of simplicity we do not restrict access to everything and do not provide separate fragments to allow features like Sites console. So we will use predefined group contributor which already have all the basic permissions we need.\nTesting AC Tool configurations Ok, now we want to deploy and test these configurations. To do this we will need some test users. Why not create them with this tool as well?\n# File apps/taradevko/acl-configuration/test-users.local,dev/test-users.yaml - user_config: - test-content-viewer: - name: \u0026#34;Test Content Viewer\u0026#34; isMemberOf: group-taradevko-content-viewer password: test-content-viewer path: /home/users/taradevko/test preferencesContent: \u0026lt;jcr:root jcr:primaryType=\u0026#34;nt:unstructured\u0026#34; cq.authoring.editor.page.showOnboarding620=\u0026#34;false\u0026#34; cq.authoring.editor.page.showOnboarding62=\u0026#34;false\u0026#34; granite.shell.showonboarding620=\u0026#34;false\u0026#34;/\u0026gt; - test-project-viewer: - name: \u0026#34;Test Project Viewer\u0026#34; isMemberOf: group-taradevko-project-viewer password: test-project-viewer path: /home/users/taradevko/test preferencesContent: \u0026lt;jcr:root jcr:primaryType=\u0026#34;nt:unstructured\u0026#34; cq.authoring.editor.page.showOnboarding620=\u0026#34;false\u0026#34; cq.authoring.editor.page.showOnboarding62=\u0026#34;false\u0026#34; granite.shell.showonboarding620=\u0026#34;false\u0026#34;/\u0026gt; One test user for each role. Passwords are not encrypted but it\u0026rsquo;s fine for our test system - as you can see folder name contains runmodes local and dev so it will not be applied to rest environments (e.g. production). Here we also provide the content of the preferences node - we don\u0026rsquo;t want to get \u0026ldquo;Getting Started\u0026rdquo; popups in AEM. It\u0026rsquo;s time to deploy and check the results!\nmvn clean install -PautoInstallPackage Under /var/statistics/achistory we can check logs and status of previous installations. This is the first place to check if you package installation fails because of the AC Tool. In our case, status is \u0026ldquo;success\u0026rdquo;.\nLet\u0026rsquo;s now impersonate - first as a Test Project Viewer user.\nAs expected, Sites Console shows no content.\nIn Projects Console we see a project for We.Retail site. Now it\u0026rsquo;s time for \u0026ldquo;Test Content Viewer\u0026rdquo;.\nThe user sees only We.Retail site (admin also would see campaigns and screens folders there).\nThere is no Projects icon in Navigation. If the user will try to Projects Console directly by the link (/projects.html) - he will get 404 error.\nWhat\u0026rsquo;s more? Service User Using AC Tool we can define service users. E.g.:\n# File /apps/taradevko/acl-configuration/system-users.yaml - user_config: - service-a-user: - name: \u0026#34;System user for We-Retail content\u0026#34; isMemberOf: fragment-weretail path: /home/users/system/taradevko isSystemUser: \u0026#34;true\u0026#34; Encrypted Password Sometimes it\u0026rsquo;s quite useful to create a real user which will get even to production - one of such cases is Replication Receiver for the publish AEM instance. To be safe enough we should have password encrypted in our configuration file. This can be done with AC Tool:\n# File /apps/taradevko/acl-configuration/replication.publish/replication-user.yaml - user_config: - replicationReceiver: - name: \u0026#34;Section A replication receiver\u0026#34; password: \u0026#34;{c8e4c8f496979cced14e3334b90d9bf6ef2dbb5b064127ac8881f072656124691a55582b40ed83849dcc67a07f0a7ed8}\u0026#34; path: /home/users/taradevko/replication - ace_config: - replicationReceiver: - FOR replicationPath IN [ /content/section_a, /etc/tags/seaction_a ]: - path: ${replicationPath} permission: allow privileges: jcr:all Crypto Support Console (/system/console/crypto) can be used to get the encrypted password. But you have to make sure that you share the same crypto key among all instances this configuration should be applied to.\nGlobal Configuration Some things can be configured globally. E.g. we can configure minimal required version and in case version of installed AC Tool is less than specified - configurations will not be applied.\n- global_config: minRequiredVersion: 2.0.4 You can find complete documentation on GitHub.\nAll configurations from this article are in this repository.\nFeatured Image is by Ronald Cuyan\n","permalink":"https://taradevko.com/2018/05-29-aem-ac-tool-manage-groups-users/","summary":"\u003cp\u003eBefore I learned about AC Tool, I worked with a couple of projects several years ago - they were quite big and had many markets and locales. And, as you can imagine, these projects had quite a sophisticated hierarchical structure of users and groups. Time to time, we were rolling out content for new markets/locales, what required the creation of a bunch of groups and setting of correct permissions to them. This routine required one developer to be fully focused on this job for several hours until everything was created, assigned and activated. Then, we had to synchronize made changes among all environments, and later, sometimes, fix issues which we made because of human mistake. If only we had Access Control Tool that days.\u003c/p\u003e","title":"AEM: AC Tool or How To Manage Groups \u0026 Users"},{"content":"In this article we will look into the steps we need to go through to create a custom Project Tile. In case you are not familiar with AEM Projects, there is a good intro from Adobe.\nNote: steps here were written for Adobe AEM 6.3.\nAdd And Render our AEM Project Tile As a content for our custom Project Tile, we will display a number of not activated pages per market/locale in We-Retail website. We will start with creating our tile component.\n\u0026lt;!--/* /apps/taradevko/components/activationStatistics/tile/tile.html */--\u0026gt; \u0026lt;sly data-sly-use.init=\u0026#34;init.js\u0026#34;/\u0026gt; \u0026lt;sly data-sly-resource=\u0026#34;${ @ path=\u0026#39;/etc/projects/dashboard/gadgets/activationStatistics/jcr:content/default\u0026#39;}\u0026#34;\u0026gt;\u0026lt;/sly\u0026gt; // /apps/taradevko/components/activationStatistics/tile/init.js use(function () { request.setAttribute(\u0026#34;projectLinkResource\u0026#34;, resource); }); As you can see, we are not doing here a lot. In init.js we set current resource as a projectLinkResource attribute. We will need this info later. Then, in tile.html we execute our js and include some resource. We want to reuse OOTB component cq/gui/components/projects/admin/dashboard/tile which will render basic structure of our project tile, but to use it, we need to set projectLinkResource first. That\u0026rsquo;s why we kinda wrap it with our own component. This should make more sense after looking at our tile\u0026rsquo;s resource definition:\n\u0026lt;!-- /etc/projects/dashboard/gadgets/activationStatistics/.content.xml --\u0026gt; \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;jcr:root xmlns:sling=\u0026#34;http://sling.apache.org/jcr/sling/1.0\u0026#34; xmlns:jcr=\u0026#34;http://www.jcp.org/jcr/1.0\u0026#34; jcr:primaryType=\u0026#34;nt:unstructured\u0026#34; allow=\u0026#34;1\u0026#34;\u0026gt; \u0026lt;jcr:content jcr:primaryType=\u0026#34;nt:unstructured\u0026#34; jcr:description=\u0026#34;Activation statistics per market/locale\u0026#34; jcr:title=\u0026#34;Not Activated Pages Info\u0026#34; sling:resourceType=\u0026#34;taradevko/components/activationStatistics/tile\u0026#34;\u0026gt; \u0026lt;image jcr:primaryType=\u0026#34;nt:unstructured\u0026#34;\u0026gt; \u0026lt;file/\u0026gt; \u0026lt;/image\u0026gt; \u0026lt;default jcr:primaryType=\u0026#34;nt:unstructured\u0026#34; sling:resourceType=\u0026#34;cq/gui/components/projects/admin/dashboard/tile\u0026#34;\u0026gt; \u0026lt;layout jcr:primaryType=\u0026#34;nt:unstructured\u0026#34; sling:resourceType=\u0026#34;cq/gui/components/projects/admin/dashboard/layouts/tile\u0026#34;/\u0026gt; \u0026lt;header jcr:primaryType=\u0026#34;nt:unstructured\u0026#34;\u0026gt; \u0026lt;title jcr:primaryType=\u0026#34;nt:unstructured\u0026#34; jcr:title=\u0026#34;Not Activated Pages Info\u0026#34;/\u0026gt; \u0026lt;/header\u0026gt; \u0026lt;body jcr:primaryType=\u0026#34;nt:unstructured\u0026#34; sling:resourceType=\u0026#34;taradevko/components/activationStatistics/body\u0026#34;/\u0026gt; \u0026lt;/default\u0026gt; \u0026lt;/jcr:content\u0026gt; \u0026lt;/jcr:root\u0026gt; Attribute allow stands for a number of tiles with given sling:resourceType to be allowed on the dashboard. Our resource contains additionally \u0026ldquo;default\u0026rdquo; resource, which has a type of the OOTB project tile component. Bellow it, we have layout definition, header, and body. In addition, under /etc/projects/dashboard/gadgets/activationStatistics/_jcr_content/image we can add an image which will be shown on \u0026ldquo;Add Tile\u0026rdquo; screen as an icon of our own Tile.\nProject Tile\u0026rsquo;s content All the content specific to this tile type will reside in the body, so we need to create a component for rendering it.\n\u0026lt;!--/* /apps/taradevko/components/activationStatistics/body/body.html */--\u0026gt; \u0026lt;div class=\u0026#34;cq-projects-AppDetails cq-projects-Pod-content\u0026#34; data-sly-use.info=\u0026#34;com.taradevko.aem.granite.ActivationStatistics\u0026#34;\u0026gt; \u0026lt;section class=\u0026#34;coral-light\u0026#34; data-sly-repeat.market=\u0026#34;${info.activationStatistics}\u0026#34;\u0026gt; \u0026lt;h2 class=\u0026#34;coral-Heading coral-Heading--2\u0026#34;\u0026gt;${market}\u0026lt;/h2\u0026gt; \u0026lt;coral-list\u0026gt; \u0026lt;coral-list-item data-sly-repeat.locale=\u0026#34;${info.activationStatistics[market]}\u0026#34;\u0026gt;${locale} -\u0026gt; ${info.activationStatistics[market][locale]}\u0026lt;/coral-list-item\u0026gt; \u0026lt;/coral-list\u0026gt; \u0026lt;/section\u0026gt; \u0026lt;/div\u0026gt; Nothing fancy, just rendering our activation statistics which we get from Sling Model:\n@Model(adaptables = SlingHttpServletRequest.class) public class ActivationStatistics { @Self private SlingHttpServletRequest request; private final IsPage isPage = new IsPage(); private final NotActivated notActivated = new NotActivated(); private final IsMarket isMarket = new IsMarket(); private Map\u0026lt;String, Map\u0026lt;String, Integer\u0026gt;\u0026gt; activationStatistics; @PostConstruct public void init() { //skipped permissions check ResourceResolver resourceResolver = request.getResourceResolver(); String projectPath = request.getRequestPathInfo().getSuffix(); Resource siteRootResource = resourceResolver.getResource(getContentPath(projectPath)); activationStatistics = getActivationStatistics(siteRootResource); } public Map\u0026lt;String, Map\u0026lt;String, Integer\u0026gt;\u0026gt; getActivationStatistics() { return activationStatistics; } private Map\u0026lt;String, Map\u0026lt;String, Integer\u0026gt;\u0026gt; getActivationStatistics(final Resource siteRootResource) { final Map\u0026lt;String, Map\u0026lt;String, Integer\u0026gt;\u0026gt; results = new TreeMap\u0026lt;\u0026gt;(); for (Resource market : siteRootResource.getChildren()) { if (isMarket.and(isPage).test(market)) { Map\u0026lt;String, Integer\u0026gt; marketStats = processMarket(market); results.put(market.getName(), marketStats); } } return results; } private Map\u0026lt;String, Integer\u0026gt; processMarket(final Resource market) { final Map\u0026lt;String, Integer\u0026gt; results = new TreeMap\u0026lt;\u0026gt;(); for (Resource locale : market.getChildren()) { if (isPage.test(locale)) { Integer localeStats = processPageTree(locale); results.put(locale.getName(), localeStats); } } return results; } private Integer processPageTree(final Resource page) { Integer stats = 0; for (Resource child : page.getChildren()) { if (isPage.test(child)) { stats += processPageTree(child); } } if (notActivated.test(page)) { stats += 1; } return stats; } //ideally this should come from project settings. private String getContentPath(final String projectPath) { return projectPath.replace(\u0026#34;/projects\u0026#34;, \u0026#34;\u0026#34;); } class IsPage implements Predicate\u0026lt;Resource\u0026gt; { @Override public boolean test(Resource resource) { return \u0026#34;cq:Page\u0026#34;.equals(resource.adaptTo(ValueMap.class).get(\u0026#34;jcr:primaryType\u0026#34;)); } } class NotActivated implements Predicate\u0026lt;Resource\u0026gt; { @Override public boolean test(Resource resource) { return !\u0026#34;Activate\u0026#34;.equals(resource.getChild(\u0026#34;jcr:content\u0026#34;) .adaptTo(ValueMap.class).get(\u0026#34;cq:lastReplicationAction\u0026#34;)); } } class IsMarket implements Predicate\u0026lt;Resource\u0026gt; { @Override public boolean test(Resource resource) { return resource.getName().length() == 2; } } } Here we collect a number of not activated pages per market/locale of We-Retail website. Note, while OOTB tiles have all logic in JSP files and it\u0026rsquo;s somewhat complex, we in our example removed many checks for the sake of simplicity.\nNow we can deploy our example:\nmvn clean install -PautoInstallPackage,autoInstallBundle Finally, navigate to http://localhost:4502/projects/details.html/content/projects/we-retail and click Add button. You will see a screen like this:\nThere at the bottom in the middle, you see our custom Project Tile. Select it and submit. Now, after AEM redirect, you see Projects Dashboard and our tile on it:\nThat\u0026rsquo;s it for our example.\nWhat\u0026rsquo;s next? If you need more examples - you can check next components/resources in the repository:\n/etc/projects/dashboard/gadgets - contains a definition of the tiles which are available on the Projects page. /libs/cq/gui/components/projects/admin/pod - components, which render gadgets from above. Mostly contains initialisation logic (as we have in taradevko/components/activationStatistics/tile/tile.html) and include of the default resource. /libs/cq/core/content/projects/dashboard/default - contains definition of the resources of OOTB tiles. Contains layout, header actions, footer link and some others. /libs/cq/gui/components/projects/admin/dashboard/tile - OOTB tile renderer (renders tile\u0026rsquo;s layout, header with actions, footer etc). /libs/cq/gui/components/projects/admin/dashboard/tiles - contains components for rendering specific content to OOTB tiles (like Project Info) You can find the complete example in this repository.\n","permalink":"https://taradevko.com/2018/04-17-aem-how-create-custom-project-tile/","summary":"\u003cp\u003eIn this article we will look into the steps we need to go through to create a custom Project Tile. In case you are not familiar with AEM Projects, there is a good intro from \u003ca href=\"https://helpx.adobe.com/experience-manager/6-3/sites/authoring/using/projects.html\"\u003eAdobe\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eNote: steps here were written for Adobe AEM 6.3.\u003c/p\u003e\n\u003ch2 id=\"add-and-render-our-aem-project-tile\"\u003eAdd And Render our AEM Project Tile\u003c/h2\u003e\n\u003cp\u003eAs a content for our custom Project Tile, we will display a number of not activated pages per market/locale in We-Retail website. We will start with creating our tile component.\u003c/p\u003e","title":"AEM: How to create custom Project Tile"},{"content":"Most of the articles which focus on Sling Model Exporter (like this one) tell us how to configure exporter for our models. But none of them really tells how to develop custom Sling Model Exporter. I will try to fill this gap.\nSo let’s imagine, that instead of json, we want to receive Sling Models in a human-readable yaml format. But OOTB we have the ability to export only as JSON. To have it as a yaml, we need to provide our custom implementation of ModelExporter interface. We are not going to invent the wheel, so let\u0026rsquo;s first add a dependency to Jackson, as it\u0026rsquo;s already used by Sling for json export:\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.fasterxml.jackson.core\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jackson-databind\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.8.4\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.fasterxml.jackson.dataformat\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jackson-dataformat-yaml\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.8.4\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; jackson-databind is already provided by Sling/AEM and we just need to add yaml data format.\nTime for Sling ModelExporter Now we are ready to implement ModelExporter.\npackage com.taradevko.aem.customexporter; import java.io.IOException; import java.io.StringWriter; import java.util.Map; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Service; import org.apache.sling.models.export.spi.ModelExporter; import org.apache.sling.models.factory.ExportException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; @Component @Service public class YamlModelExporter implements ModelExporter { @Override public boolean isSupported(Class\u0026lt;?\u0026gt; clazz) { return clazz.equals(String.class); } @Override public \u0026lt;T\u0026gt; T export(Object model, Class\u0026lt;T\u0026gt; clazz, Map\u0026lt;String, String\u0026gt; options) throws ExportException { ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); StringWriter stringWriter = new StringWriter(); try { mapper.writeValue(stringWriter, model); } catch (IOException e) { throw new ExportException(e); } return (T) stringWriter.toString(); } @Override public String getName() { return \u0026#34;yaml-exporter\u0026#34;; } } Let\u0026rsquo;s now go through this code.\nMethod isSupported From what I see in sources of org.apache.sling.models.impl.ExportServlet String is the only target format Sling make Model export to. So we return true whenever String is expected. This will tell ModelAdapterFactory (which is used by_ExportServlet)_, that our exporter can be used to get model as a String.\nMethod getName Every exporter should have a name - \u0026ldquo;yaml-exporter\u0026rdquo; is good enough to use.\nMethod export This is the place where all the fun happens (not much fun in this example, but you can always take a look at JacksonExporter for some inspiration). In our case, it\u0026rsquo;s a simple creation of ObjectMapper for yaml, and writing our model to the String. That is it!\nSling Model Next, we need a model to export. Let\u0026rsquo;s export Hero Image component from We-Retail demo site.\npackage com.taradevko.aem.customexporter; import javax.inject.Inject; import org.apache.sling.api.resource.Resource; import org.apache.sling.models.annotations.Exporter; import org.apache.sling.models.annotations.Model; import org.apache.sling.models.annotations.injectorspecific.Self; @Model(adaptables = Resource.class, resourceType = { \u0026#34;weretail/components/content/heroimage\u0026#34; }) @Exporter(name = \u0026#34;yaml-exporter\u0026#34;, extensions = \u0026#34;yaml\u0026#34;) public class HeroImage { @Inject private String title; @Inject private String fileReference; @Self private Metadata metadata; public String getTitle() { return title; } public Metadata getMetadata() { return metadata; } public String getFileReference() { return fileReference; } } package com.taradevko.aem.customexporter; import javax.inject.Inject; import javax.inject.Named; import org.apache.sling.api.resource.Resource; import org.apache.sling.models.annotations.Model; import org.apache.sling.models.annotations.Optional; @Model(adaptables = Resource.class) public class Metadata { @Inject @Named(\u0026#34;jcr:primaryType\u0026#34;) private String primaryType; @Inject @Optional @Named(\u0026#34;jcr:mixinTypes\u0026#34;) private String[] mixingTypes; public String getPrimaryType() { return primaryType; } public String[] getMixingTypes() { return mixingTypes; } } And configure yaml mime type in Apache Sling MIME Type Service, e.g. add \u0026ldquo;text/yaml yaml\u0026rdquo; entry.\nFinally, we are ready to test!\nWhat\u0026rsquo;s next? Next, you may want to add annotations support to be able to configure export of the model in the same way you can do with OOTB json export.\nYou can find the complete example in this repository.\n","permalink":"https://taradevko.com/2018/01-29-sling-model-exporter-now-really-custom/","summary":"\u003cp\u003eMost of the articles which focus on Sling Model Exporter (like \u003ca href=\"https://helpx.adobe.com/experience-manager/kt/platform-repository/using/sling-model-exporter-tutorial-develop.html\"\u003ethis one\u003c/a\u003e) tell us how to configure exporter for our models. But none of them really tells how to develop custom Sling Model Exporter. I will try to fill this gap.\u003c/p\u003e\n\u003cp\u003eSo let’s imagine, that instead of json, we want to receive Sling Models in a human-readable yaml format. But OOTB we have the ability to export only as JSON. To have it as a yaml, we need to provide our custom implementation of \u003ca href=\"https://sling.apache.org/apidocs/sling9/org/apache/sling/models/export/spi/ModelExporter.html\"\u003eModelExporter\u003c/a\u003e interface. We are not going to invent the wheel, so let\u0026rsquo;s first add a dependency to Jackson, as it\u0026rsquo;s already used by Sling for json export:\u003c/p\u003e","title":"Sling Model Exporter. Now really custom."},{"content":"For testing (you do this, right?) your AEM project, most probably, you use AEM Mocks from wcm.io. This is a powerful library, which makes your life easier when it comes to testing Sling Models, Servlets, other classes which work with Page and other entities from AEM.\nHow to use AemContextCallback Interface AemContextCallback allows us to define some default operations which are common for all our test classes, e.g. to register our models.\nimport io.wcm.testing.mock.aem.junit.AemContext; import io.wcm.testing.mock.aem.junit.AemContextCallback; public class AppContextSetup implements AemContextCallback { @Override public void execute(final AemContext aemContext) throws Exception { aemContext.addModelsForPackage(\u0026#34;com.taradevko.aem\u0026#34;); } } Now, when we define JUnit rule for AemContext, we should create and pass our callback into AemContext\u0026rsquo;s constructor.\npublic final AppContextSetup appContextSetup = new AppContextSetup(); @Rule public AemContext aemContext = new AemContext(appContextSetup, ResourceResolverType.RESOURCERESOLVER_MOCK); After these changes, all our models in the defined package will be initialized. There is no need to do this explicitly anymore.\nMixing AemContextCallback and TestRule So far, our callback does not make our life much easier, but we definitely can take more out of AemContextCallback! When you need to create some resource for a class under the test, you either build it directly in your test class (or some helper) or you load it from the file (e.g. json). We can easily move this logic into or callback implementing additionally JUnit\u0026rsquo;s TestRule interface. Let\u0026rsquo;s take a look at this:\nimport io.wcm.testing.mock.aem.junit.AemContext; import io.wcm.testing.mock.aem.junit.AemContextCallback; import org.apache.sling.api.resource.Resource; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; public class AppContextSetup implements AemContextCallback, TestRule { private Class testClass; @Override public void execute(final AemContext aemContext) throws Exception { aemContext.addModelsForPackage(\u0026#34;com.taradevko.aem\u0026#34;); final Resource content = aemContext.load().json( String.format(\u0026#34;/%s/%s.json\u0026#34;, testClass.getPackage().getName(), testClass.getSimpleName()), \u0026#34;/content\u0026#34;); aemContext.currentResource(content); } @Override public Statement apply(final Statement base, final Description description) { testClass = description.getTestClass(); return base; } } So what is going on here? Method apply is called for each test in our test class. Here we just store this class to reuse later.\nThen, when AemContext executes our callback, we build a path \u0026ldquo;/class_package/class_name.json\u0026rdquo;, load it with ContentLoader and set as a current resource.\nTime for testing Now it\u0026rsquo;s time to use it. Imagine, we have a simple model we want to test:\nimport org.apache.sling.api.resource.Resource; import org.apache.sling.models.annotations.Model; import javax.inject.Inject; @Model(adaptables = Resource.class) public class SomeModel { @Inject private String property1; public String doSmthWithProperty() { return property1.toUpperCase(); } } And also we prepared a json file with a resource to adapt to this model for test purposes. It\u0026rsquo;s located under \u0026ldquo;com.taradevko.aem.contcallback/SomeModelTest.json\u0026rdquo; in test resources section.\n{ \u0026#34;jcr:primaryType\u0026#34;: \u0026#34;nt:unstructured\u0026#34;, \u0026#34;property1\u0026#34;: \u0026#34;jsonProp1Value\u0026#34; } And now, finally - testing:\npackage com.taradevko.aem.contcallback; import com.taradevko.aem.AppContextSetup; import io.wcm.testing.mock.aem.junit.AemContext; import org.apache.sling.testing.mock.sling.ResourceResolverType; import org.junit.Rule; import org.junit.Test; import static org.junit.Assert.*; public class SomeModelTest { @Rule public final AppContextSetup appContextSetup = new AppContextSetup(); @Rule public AemContext aemContext = new AemContext(appContextSetup, ResourceResolverType.RESOURCERESOLVER_MOCK); @Test public void demoTest() { final SomeModel someModel = aemContext.currentResource().adaptTo(SomeModel.class); assertEquals(\u0026#34;Should contain value from json\u0026#34;, someModel.doSmthWithProperty(), \u0026#34;JSONPROP1VALUE\u0026#34;); } } As before, we create AppContextSetup and pass it to the constructor of AemContext. But now it also got annotation @Rule.\nNow, if we run the example, we can see that test passed successfully!\nNow, whenever we need to load resources from json, we just should put it on the correct path, and use our AppContextSetup. The rest will be done automatically!\nWhat is next You can do much more with AemContextCallback, like registering you stub implementations of OSGi services or executing additional callbacks on AemContext.\nYou can find the complete example in this repository.\n","permalink":"https://taradevko.com/2017/12-20-aem-simple-unit-testing-with-aemcontextcallback/","summary":"\u003cp\u003eFor testing (you do this, right?) your AEM project, most probably, you use AEM Mocks from \u003ca href=\"http://wcm.io/testing/aem-mock/\"\u003ewcm.io\u003c/a\u003e. This is a powerful library, which makes your life easier when it comes to testing Sling Models, Servlets, other classes which work with Page and other entities from AEM.\u003c/p\u003e\n\u003ch2 id=\"how-to-useaemcontextcallback\"\u003eHow to use \u003cem\u003eAemContextCallback\u003c/em\u003e\u003c/h2\u003e\n\u003cp\u003eInterface \u003cem\u003eAemContextCallback\u003c/em\u003e allows us to define some default operations which are common for all our test classes, e.g. to register our models.\u003c/p\u003e","title":"AEM: Simplify unit testing with AemContextCallback"},{"content":"It’s written quite a lot about Sling selectors, best practices and how we should use them. However, misuse is as common as many articles about selectors. So I would like to add my 5 cents to this topic.\nHow to (not) use Sling Selectors Let’s imagine a situation when you have a component which generates some reports. And it\u0026rsquo;s requested from some other part of the site via Ajax. There can be 2 representation of them - full and short. We know that it’s a good approach to map our servlet, which will generate a report, to a resource type. Also, you know about selectors, so you decide to map it to “/apps/mysite/mycomponent” as a resource type and selectors “fullreport” and “shortreport”. It may look like this:\n@Service @Component(metatype = false) @Properties(value = { @Property(name = \u0026#34;sling.servlet.resourceTypes\u0026#34;, value = \u0026#34;apps/mysite/mycomponent\u0026#34;), @Property(name = \u0026#34;sling.servlet.selectors\u0026#34;, value = { \u0026#34;fullreport\u0026#34;, \u0026#34;shortreport\u0026#34; }), @Property(name = \u0026#34;sling.servlet.extensions\u0026#34;, value = { \u0026#34;html\u0026#34; }), @Property(name = \u0026#34;sling.servlet.methods\u0026#34;, value = { \u0026#34;GET\u0026#34; }) }) public class ReportServlet extends SlingSafeMethodsServlet { public void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException { //do smth } } Now, whenever you call /content/mysite/mypage/jcr:content/mycomponent.(fullreport|shortreport).html our new servlet’s method doGet is called. We still need to print different format of the report on each of selectors. We may decide to do this with simple switch-case in our doGet method:\nfinal String selectorString = request.getRequestPathInfo().getSelectorString(); switch (selectorString) { case \u0026#34;fullreport\u0026#34;: printFullReport(); break; case \u0026#34;shortreport\u0026#34;: printShortReport(); break; } This code will perfectly work. At least for some time.\nOne day, in our imagined world, requirement changes and now we need to add one more report representation - “printablereport”. What should we do? It\u0026rsquo;s easy - we add one more case parameter, like this (+ adding selector to the servlet\u0026rsquo;s annotations):\nfinal String selectorString = request.getRequestPathInfo().getSelectorString(); switch (selectorString) { case \u0026#34;fullreport\u0026#34;: printFullReport(); break; case \u0026#34;shortreport\u0026#34;: printShortReport(); break; case \u0026#34;printablereport\u0026#34;: printPrintableReport(); break; } Now we are asked to provide the ability to generate the report with either vertical, either horizontal histograms. And we would like to have this as a selector as well (why not? we can cache our reports on a dispatcher and it complies with a rule to use selectors for defining a format and not a content). But now it\u0026rsquo;s gonna be tricky, because if we use switch-case, then we would have 6 cases now. Or we can go with if statement and String.contains. But all these approaches look messy - if/switch-cases constructions getting bigger, readability decreases, chances to fail somewhere - increase.\nOur code violates the open-closed principle. To extend functionality with new report type, with our approach, we need to make quite a couple of modifications in our servlet. Also, such approach does not tend to be robust.\nWhen reality strikes Here is an example from my experience.\nWe had a form, which was used to generate such reports. We had just 2 selectors + switch-case in servlet. Ajax communication between page and backend was quite sophisticated and included different pre- and postprocessors in Javascript + JQuery code. One of such preprocessors was checking URLs before making Ajax requests to the backend. It was a globally used one. One day, one team needed to add extra selector (e.g. “selector1”) in all Ajax requests in popups. However, they missed to add a check if this preprocessor is used in a popup, so it was adding the selector to all the URLs.\nAs a result, our form was not able to provide reports anymore. Yes, we got one more extra selector, but we still had the one, which servlet was mapped to and our component still had to work properly. BUT, we had that switch-case. So breaking several best practices led to us having a headache while trying to understand why form does not work anymore. So a human mistake in another team and fragile code in our servlet resulted in lost time.\nSo, how could we make our code more robust? It\u0026rsquo;s simple:\nNever ever extract selectors in your servlets. You just not need it, remember this! Sling provides enough functionality to map code to specific selectors without a need to check them directly. As a result, in our case we just needed 2 servlets each of them mapped to same resource type but to different selectors: @Service @Component(metatype = false) @Properties(value = { @Property(name = \u0026#34;sling.servlet.resourceTypes\u0026#34;, value = \u0026#34;apps/mysite/mycomponent\u0026#34;), @Property(name = \u0026#34;sling.servlet.selectors\u0026#34;, value = \u0026#34;fullreport\u0026#34;), @Property(name = \u0026#34;sling.servlet.extensions\u0026#34;, value = { \u0026#34;html\u0026#34; }), @Property(name = \u0026#34;sling.servlet.methods\u0026#34;, value = { \u0026#34;GET\u0026#34; }) }) public class FullReportServlet extends SlingSafeMethodsServlet { public void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException { //do full report } } @Service @Component(metatype = false) @Properties(value = { @Property(name = \u0026#34;sling.servlet.resourceTypes\u0026#34;, value = \u0026#34;apps/mysite/mycomponent\u0026#34;), @Property(name = \u0026#34;sling.servlet.selectors\u0026#34;, value = \u0026#34;shortreport\u0026#34;), @Property(name = \u0026#34;sling.servlet.extensions\u0026#34;, value = { \u0026#34;html\u0026#34; }), @Property(name = \u0026#34;sling.servlet.methods\u0026#34;, value = { \u0026#34;GET\u0026#34; }) }) public class ShortReportServlet extends SlingSafeMethodsServlet { public void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException { //do short report } } With the code above, our component would still work (taking into account that new selector added after our\u0026rsquo;s).\nConclusion Don’t be afraid of too many servlets - it’s not the excuse for violating Sling best practices and bringing problems into your code.\n","permalink":"https://taradevko.com/2017/12-11-sling-selector-best-practice/","summary":"\u003cp\u003eIt’s written quite a lot about Sling selectors, best practices and how we should use them. However, misuse is as common as many articles about selectors. So I would like to add my 5 cents to this topic.\u003c/p\u003e\n\u003ch3 id=\"how-to-not-use-sling-selectors\"\u003eHow to (not) use Sling Selectors\u003c/h3\u003e\n\u003cp\u003eLet’s imagine a situation when you have a component which generates some reports. And it\u0026rsquo;s requested from some other part of the site via Ajax. There can be 2 representation of them - full and short. We know that it’s a good approach to map our servlet, which will generate a report, to a resource type. Also, you know about selectors, so you decide to map it to “\u003cem\u003e/apps/mysite/mycomponent\u003c/em\u003e” as a resource type and selectors “\u003cem\u003efullreport\u003c/em\u003e” and “\u003cem\u003eshortreport\u003c/em\u003e”. It may look like this:\u003c/p\u003e","title":"Sling: More About Selector Best Practices"},{"content":"On publish instances for SEO purposes you may want to remove /content/[sitename] from the URLs. At the same time, you want these short URLs to be resolved to resources when a user requests them.To achieve this, you need to configure Mapping for Resource Resolution. Usually, it\u0026rsquo;s a trivial task, but it\u0026rsquo;s getting complicated when you need to host several projects on the same AEM instance.\nSo, let\u0026rsquo;s imagine, you have several projects hosted on the same AEM instance. One of them lives under /content/site1 and should be accessible via https://site1.com. And another one – under /content/site2 and accessible via https://site2.com. There are several ways to configure resource resolution in Sling and configuration via configuration tree (by default it’s /etc/map) is the most flexible way and we will use it in this article.\nSling Resource Mapping Configuration To not mess with existing mappings, let’s create a separate folder for our example - /etc/map.muldomains. Path to the configuration we will change with property resource.resolver.map.location of configuration with PID org.apache.sling.jcr.resource.internal.JcrResourceResolverFactoryImpl.\nNow we should create Root Level Mappings for our sites. Such mappings include scheme, host, port and URI path.\nSince HTTPS is widely used, we will support it as well. As a result, we have to create folder /etc/map.muldomains/https – this will be the start of our mapping tree, which tells that all mapping rules beneath it should be applied only for https scheme.\nNow it’s time to specify the rule for our domain /etc/map.muldomains/https/site1.com:\n{ \u0026#34;jcr:primaryType\u0026#34;: \u0026#34;sling:Mapping\u0026#34;, \u0026#34;sling:redirect\u0026#34;: \u0026#34;/home.html\u0026#34; } Here we create a rule for the host so that user will be redirected to the home page when access site by host only. The node name is used to match the host from the request.\nNow we need to specify how to resolve any other requested URI. For this let’s create a child node of the host rule - /etc/map.muldomains/https/site1.com/all.\n{ \u0026#34;jcr:primaryType\u0026#34;: \u0026#34;sling:Mapping\u0026#34;, \u0026#34;sling:match\u0026#34;: \u0026#34;(.+)$\u0026#34;, \u0026#34;sling:internalRedirect\u0026#34;: [ \u0026#34;/content/site1/$1\u0026#34;, \u0026#34;/$1\u0026#34; ] } Here we use the sling:match property to specify the regexp to match the path from URI, instead of the node name. We do it in this way, because we can give a more human readable name to the node and, also, pretty often such regexp do contain special chars which can not be used in the node’s name. Hence, it’s safer to put it into the property\u0026rsquo;s value. In the rule above we prepend /content/site1 to the path in all requested URIs and try to resolve the resource, in case it’s missing – Sling will try to resolve origin path (note the second entry of the sling:internalRedirect value). The same steps should be repeated for “site2” by copying created rules and replacing “site1” with “site2”. To test mapping I’ve created dummy content structure:\nBut to test it, we will use Resource Resolver Web Console - /system/console/jcrresolver. It\u0026rsquo;s handy to see effective Resolver Map Entries and to test configuration. So let’s check our sites (Resolve button):\nWe got 302 redirects to the home.html page. That is fine. Now it\u0026rsquo;s time to test specific pages:\nIn both cases, Sling Resource Resolver resolves URL to the correct resource.\nReverse Resource Resolving Configuration But before we add /content/[sitename] to the path, we should remove it. Let’s see if resource mapping works (button Map):\nWow, what just happened? Why content is removed and “site2” - not? Let’s check section “Mapping Map Entries” on the same page:\nSo there is a rule for replacing /content/ with / and nothing about site1/site2. It appears, that when we use regexp in the rule, it can not be used for the mapping (when we transform path to the resource to the URI for the end user). To overcome this issue, we should create separate rules (not root level mappings) where we specify regexp in sling:internalRedirect property and transformation instructions in sling:match (so in opposite to configs we previously specified).\nHere is a rule for the reverse mapping for the site2 - /etc/map.muldomains/site2-rev:\n{ \u0026#34;jcr:primaryType\u0026#34;: \u0026#34;sling:Mapping\u0026#34;, \u0026#34;sling:match\u0026#34;: \u0026#34;/$1\u0026#34;, \u0026#34;sling:internalRedirect\u0026#34;: \u0026#34;^/content/site2/(.*)\u0026#34; } Here for all paths which start with /content/site2/ we remove it. Let’s test:\nRepeat the same steps for site1.\nResource Resolving Configuration For Direct Access In some cases (debugging usually) you may need to access publish instance directly. But we have configured resolving mapping only for HTTPS scheme so far. Let’s fix this - /etc/map.muldomains/http/site1_noDIsp:\n{ \u0026#34;jcr:primaryType\u0026#34;: \u0026#34;sling:Mapping\u0026#34;, \u0026#34;sling:redirect\u0026#34;: \u0026#34;/home.html\u0026#34;, \u0026#34;sling:match\u0026#34;: \u0026#34;site1.com.\\\\d{4}\u0026#34; } Here we create folder _/etc/map.muldomains/_http for HTTP scheme and mapping node. We match our domain and additionally, we specify any 4-digit port. So common 4503/4504 will be matched here. And for all other pages - /etc/map.muldomains/http/site1_noDisp/all:\n{ \u0026#34;jcr:primaryType\u0026#34;: \u0026#34;sling:Mapping\u0026#34;, \u0026#34;sling:match\u0026#34;: \u0026#34;(.+)$\u0026#34;, \u0026#34;sling:internalRedirect\u0026#34;: [ \u0026#34;/content/site1/$1\u0026#34;, \u0026#34;/$1\u0026#34; ] } The same as we have for HTTPS scheme. Before testing – repeat above steps for site2.\nIn addition, if you cache your HTML pages you would want to prepend /content/[sitename] to all requested pages on Apache side to cache them under the correct (full) path as this prevents from cache invalidation problems.\nThis example can be found in the github repository.\n","permalink":"https://taradevko.com/2017/07-18-sling-resource-resolve-multi-domain-sys/","summary":"\u003cp\u003eOn publish instances for SEO purposes you may want to remove \u003cem\u003e/content/[sitename]\u003c/em\u003e from the URLs. At the same time, you want these short URLs to be resolved to resources when a user requests them.To achieve this, you need to configure Mapping for Resource Resolution. Usually, it\u0026rsquo;s a trivial task, but it\u0026rsquo;s getting complicated when you need to host several projects on the same AEM instance.\u003c/p\u003e\n\u003cp\u003eSo, let\u0026rsquo;s imagine, you have several projects hosted on the same AEM instance. One of them lives under /content/site1 and should be accessible via \u003cem\u003e\u003ca href=\"https://site1.com\"\u003ehttps://site1.com\u003c/a\u003e\u003c/em\u003e. And another one – under \u003cem\u003e/content/site2\u003c/em\u003e and accessible via \u003cem\u003e\u003ca href=\"https://site2.com\"\u003ehttps://site2.com\u003c/a\u003e\u003c/em\u003e. There are several ways to configure resource resolution in Sling and configuration via configuration tree (by default it’s \u003cem\u003e/etc/map\u003c/em\u003e) is the most flexible way and we will use it in this article.\u003c/p\u003e","title":"Sling: Resource Resolving for multi-domain system"},{"content":"In this article, we will take a look at how to control resolving process for bundles, in particular, how to restrict access to the bundle only for a specific set of OSGi bundles. This may be useful when you want to give access to the bundle only to authorized ones and prevent others from using it for satisfying imported packages. In such cases, you should take a look at interfaces ResolverHookFactory and ResolverHook.\nResolverHookFactory It has one method:\nResolverHook begin(Collection\u0026lt;BundleRevision\u0026gt; triggers); It’s called by Felix each time resolve process begins. As a parameter, it accepts a collection of bundles which triggered this resolving process. This method returns hook, which has 4 methods but we are interested only in one of them.\nHere is how our ResolverHookFactory can look like:\npackage com.taradevko.aem.hook; import org.osgi.framework.hooks.resolver.ResolverHook; import org.osgi.framework.hooks.resolver.ResolverHookFactory; import org.osgi.framework.wiring.BundleRevision; import org.apache.felix.scr.annotations.*; import java.util.Collection; @Component(immediate = true) @Service(value = ResolverHookFactory.class) public class TestHooksFactory implements ResolverHookFactory { public ResolverHook begin(Collection\u0026lt;BundleRevision\u0026gt; triggers) { return new TestResolverHook(); } } In our case, we are not interested in bundles which trigger resolving process so we just create and return our resolver hook.\nResolverHook In our example we will use next method:\nvoid filterMatches(BundleRequirement requirement, Collection\u0026lt;BundleCapability\u0026gt; candidates); There we can filter out candidates (other bundles) to be wired to the bundle specified in requirement object. In other words:\nrequirement - we will use it to extract the name of the bundle, which needs some requirements to be resolved; candidates - this object contains candidates for resolving given requirement. We need to remove from this collection any candidates which are not allowed to be used by the bundle from above. package com.taradevko.aem.hook; import org.osgi.framework.hooks.resolver.ResolverHook; import org.osgi.framework.wiring.BundleCapability; import org.osgi.framework.wiring.BundleRequirement; import org.osgi.framework.wiring.BundleRevision; import java.util.*; public class TestResolverHook implements ResolverHook { private static final Map\u0026lt;String, List\u0026lt;String\u0026gt;\u0026gt; restictions = new HashMap\u0026lt;\u0026gt;(); static { restictions.put(\u0026#34;org.apache.abdera.parser\u0026#34;, Arrays.asList(\u0026#34;org.apache.abdera.server\u0026#34;, \u0026#34;org.apache.abdera.client\u0026#34;)); } @Override public void filterResolvable(Collection\u0026lt;BundleRevision\u0026gt; candidates) { } @Override public void filterSingletonCollisions(BundleCapability singleton, Collection\u0026lt;BundleCapability\u0026gt; collisionCandidates) { } @Override public void filterMatches(BundleRequirement requirement, Collection\u0026lt;BundleCapability\u0026gt; candidates) { candidates.removeIf(candidate -\u0026gt; { final String candidateName = candidate.getRevision().getSymbolicName(); final String requirementName = requirement.getRevision().getSymbolicName(); final List\u0026lt;String\u0026gt; allowedList = restictions.get(candidateName); return allowedList != null \u0026amp;\u0026amp; !allowedList.contains(requirementName); }); } @Overrideh public void end() { } } In filterMatches, we iterate over the candidates and check if we have any restrictions for this bundle. If found – we check if requirement (bundle) is allowed to use this candidate. If it’s not allowed – we remove the candidate from the collection (and as a result, removed bundle will not be used by Felix to resolve requirement).\nAs an example I took org.apache.abdera.parser which should be accessible only by org.apache.abdera.server and org.apache.abdera.client bundles.\nFor testing purposes I’ve created a test bundle with one class – Activator, which imports package from the \u0026ldquo;protected\u0026rdquo; bundle:\n\u0026lt;Import-Package\u0026gt;org.apache.abdera.parser,*\u0026lt;/Import-Package\u0026gt; Result Let’s deploy our test bundle and see if all requirements of it are resolved:\n#From test module dir mvn clean install -PautoInstallBundle So bundle is in active status and all requirements are resolved. Let’s deploy the bundle with resolver hook.\n#From hook module dir mvn clean install -PautoInstallBundle Now we need to trigger resolving process so our resolver hook will prevent test bundle from resolving restricted dependency. Let’s trigger package refresh for test bundle and see what happen next - now test bundle is in installed state and in the console (error.log) we can see the error message like:\nThat’s it. Now only restricted set of bundles can use org.apache.abdera.parser.\nNote: in the real world, you would want to control start level of the hook bundle to make sure that it’s started at the beginning (probably start level 1) so no bundle will bypass your resolver hook.\nAll code from this axample can be found in the github repository.\n","permalink":"https://taradevko.com/2017/06-09-osgi-control-resolving-process-bundle/","summary":"\u003cp\u003eIn this article, we will take a look at how to control resolving process for bundles, in particular, how to restrict access to the bundle only for a specific set of OSGi bundles. This may be useful when you want to give access to the bundle only to authorized ones and prevent others from using it for satisfying imported packages. In such cases, you should take a look at interfaces \u003cem\u003eResolverHookFactory\u003c/em\u003e and \u003cem\u003eResolverHook\u003c/em\u003e.\u003c/p\u003e","title":"OSGi: Control resolving process of the bundle"},{"content":"Starting from AEM 6 developers are encouraged to create special service users for their bundles/services instead of using administrative sessions. Adobe has documentation on this topic here. However, there are some features which are either not widely used, either - not well documented. So, let\u0026rsquo;s uncover them!\nReport on configured Service Users in AEM Since Service User Mapper 1.2.2\nThere is a page in Web Console where you can check all configured system users - /system/console/status-slingserviceusers\nFor each service user you can see service and sub service names it\u0026rsquo;s configured for. This UI is much better than using Configuration Manager Console to check this, right?\nAllow non-system users to be used in mappings Since Service User Mapper 1.2.2\nYes, the heading is totally correct - you can use regular users in the mapping. To achieve this, you should create configuration for Apache Sling JCR System User Validator (org.apache.sling.jcr.resource.internal.JcrSystemUserValidator) where you should disable validation (allow.only.system.user).\nI am not sure what was the reason for implementing this feature, but you would not want to use this on production.\nDelay OSGi component activation until service user configuration is available Since Service User Mapper 1.2.0\nThere can be cases when you do not want your component to be active if service user\u0026rsquo;s mapping is not available. In such situations, you can use ServiceUserMapped. You reference this service in your component and it will be resolved only when the corresponding mapping is available. Check example bellow:\npackage com.taradevko.aem.usm; import org.apache.felix.scr.annotations.Activate; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Reference; import org.apache.sling.serviceusermapping.ServiceUserMapped; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Component(immediate = true) public class SomeComponent { public static final Logger LOG = LoggerFactory.getLogger(SomeComponent.class); @Reference private ServiceUserMapped serviceUserMapped; @Activate private void activate() { System.out.println(\u0026#34;I am active\u0026#34;); } } Now, after deploy, it is in the state \u0026ldquo;unsatisfied\u0026rdquo; (click image for full size):\nThen, after adding mapping \u0026ldquo;com.taradevko.aem.usm.core=testaem\u0026rdquo; it\u0026rsquo;s getting active. Nice, isn\u0026rsquo;t it?\nAlso, you can specify subservice in the target of the reference if you use it: @Reference(target = \u0026ldquo;(subServiceName=mySubService)\u0026quot;) .\nWrite custom Service User Validator Above we talked about the case when you want any user to be used with ResourceResolverFactory.getServiceResourceResolver(). But what if you want to use some different constraints instead (e.g. allow usage of the regular user which have \u0026ldquo;service-\u0026rdquo; in its name)? To achieve this, you can provide service which implements interface ServiceUserValidator.\nNote: this feature has been introduced in Service User Mapper 1.1.0 but in that version, the user is allowed to use only if ALL validators returned true. Since 1.2.0 user is allowed if ANY of validators returns true.\nMapping amendments for service user mapper If you are still not using service user mapping amendments - you are doing a big mistake! This feature allows you to define mappings in a modular way. To use it, create config for factory org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended. Check example bellow:\n\u0026lt;!--file name is org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-someuniqueid.xml--\u0026gt; \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;jcr:root xmlns:sling=\u0026#34;http://sling.apache.org/jcr/sling/1.0\u0026#34; xmlns:jcr=\u0026#34;http://www.jcp.org/jcr/1.0\u0026#34; jcr:primaryType=\u0026#34;sling:OsgiConfig\u0026#34; user.mapping=\u0026#34;[com.taradevko.aem.usm=userid]\u0026#34; /\u0026gt; Conclusion I hope this article will help you to fully leverage Sling Service User Mapper functionality.\n","permalink":"https://taradevko.com/2017/06-01-aem-service-user-mapper-do-you-know-it/","summary":"\u003cp\u003eStarting from AEM 6 developers are encouraged to create special service users for their bundles/services instead of using administrative sessions. Adobe has documentation on this topic \u003ca href=\"https://docs.adobe.com/docs/en/aem/6-2/administer/security/service-users.html\"\u003ehere\u003c/a\u003e. However, there are some features which are either not widely used, either - not well documented. So, let\u0026rsquo;s uncover them!\u003c/p\u003e\n\u003ch2 id=\"report-on-configured-service-users-in-aem\"\u003eReport on configured Service Users in AEM\u003c/h2\u003e\n\u003cp\u003eSince Service User Mapper 1.2.2\u003c/p\u003e\n\u003cp\u003eThere is a page in Web Console where you can check all configured system users - /system/console/status-slingserviceusers\u003c/p\u003e","title":"AEM: Service User (Mapper) - do you really know it?"},{"content":"Canonical Tagging Support for AEM SEO Why does somebody need this library? Hi there. Once I had to add a canonical tag to the pages due to duplicated content on the site. Having such duplication can greatly affect the ranking of your site so it\u0026rsquo;s important for AEM SEO as well. At that moment I had a lack of time to write something nice and flexible, so I ended up with nasty and not tested solution. It worked, but it was not something I can feel good with. Later on, I\u0026rsquo;ve decided to write the library for adding canonical tags. The main reason for creating this library was that I felt really badly about that code I could not refactor (because I was not working in that company anymore).\nDetailed documentation is in git repo - https://github.com/otarsko/aem-seo-canonical-url. Bellow, I will just outline main points.\nFeatures Let\u0026rsquo;s check out features of the library:\nSpecify canonical page for another page ( /path1/page1\u0026gt;/path2/page1)); Specify the pattern for the path, so all child pages will be searched by their relative path in the lookup path (/path[1-5]=\u0026gt;/corporate - for each page under /path[1-5] search canonical page under the /corporate). How to use What are the steps to start using this library:\nadd a dependency to the library (latest version is in Maven Central Repository) or build it yourself; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.taradevko\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;com.taradevko.aem.seo\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.1.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; include library\u0026rsquo;s bundle into the package with other bundles to be installed in AEM, build and install it; create and configure system user for the bundle; configure rules for the mapping; configure Externalizer, so publish domain in specified (it will be used in canonical links); insert \u0026lt;link rel=\u0026ldquo;canonical\u0026rdquo; /\u0026gt; into the page component, head section The library has been tested with AEM 6.1 SP1.\nAnd again, Canonical Tagging Support for AEM SEO repository is there.\n","permalink":"https://taradevko.com/2017/03-27-canonical-tagging-support-aem-seo/","summary":"\u003ch2 id=\"canonical-tagging-support-for-aem-seo\"\u003eCanonical Tagging Support for AEM SEO\u003c/h2\u003e\n\u003ch3 id=\"why-does-somebody-needthis-library\"\u003eWhy does somebody need this library?\u003c/h3\u003e\n\u003cp\u003eHi there. Once I had to add a \u003ca href=\"https://en.wikipedia.org/wiki/Canonical_link_element\"\u003ecanonical tag\u003c/a\u003e to the pages due to duplicated content on the site. Having such duplication can greatly affect the ranking of your site so it\u0026rsquo;s important for AEM SEO as well. At that moment I had a lack of time to write something nice and flexible, so I ended up with nasty and not tested solution. It worked, but it was not something I can feel good with. Later on, I\u0026rsquo;ve decided to write the library for adding canonical tags. The main reason for creating this library was that I felt really badly about that code I could not refactor (because I was not working in that company anymore).\u003c/p\u003e","title":"AEM SEO: Canonical Tagging Support"},{"content":"In previous articles (part 1, part2) we created chatbot for Telegram. As I promised, here we will configure some settings of our bot:\ncommands list profile photo about text bot description Let\u0026rsquo;s begin by configuring our commands list.\nCommands list Command list represents the commands supported by the bot. Telegram suggests them when user types / in the chat with our bot. Each command in this list has a name, which starts with ‘/’, and a text description. Command name length is limited to 32 characters.\nLet’s check our help command to refresh the memory:\nSo we have 2 commands we need to add to our list. Now we go to @BotFather and type _/_setc. You should see next:\nIt appears that BotFather also has this command suggestion and that is how it looks like. Let’s click on suggested command.\nHere you should select which bot you want to configure – in my case it’s @repaitMyStuffBot. Click on it and bot asks you to specify commands list in given format. I’ve entered next text:\nfix - submit request. E.g.: /fix desc myrequests - to get submitted request\nAfter sending a message back to the bot it tells us that operation has been successfully finished.\nWe are done with the configuration and we can test it. To do this, go to chat with your bot and start to type command. You will get a suggestion from your bot (if there is anything to suggest):\nIt works perfectly!\nChatbot profile picture (userpic) Currently, we have “RS” in a cycle as a profile image, and I do not like this. Let’s put there something more related to our business.\nSteps to change profile picture are pretty similar to setting commands list:\nOpen chat with @BotFather type /setuserpic select your bot use clip icon, which is next to the input field, to upload the image NOTE: I’ve not found any documentation regarding image size limitation for bot profile picture, but 64x64px and 128x128px will not work:\nBut 256x256px works perfectly – chatbot sends us success message:\nNow, to check results you should refresh the page.\nAlso, profile picture has been updated in Bot profile dialog:\nAbout text At this point, we have configured command list and profile picture. But profile dialog looks pretty empty. We should do something with it. About text will be shown in the profile of your bot, so users can see, what is the purpose of this bot.\nOpen chat with @BotFather type /setabouttext choose bot Write about text Now we will check profile page one more time.\nDescription Another important thing is chatbot description. Do you remember what have you seen when started the chat with your bot for the first time? It looked smth like this:\nNot really user-friendly, right? Let’s fix this. Description will be shown for new users before they start a chat. It should be attractive so they will not close chat but start chatting (that is why we are writing bots, right?). As usually:\nOpen chat with @BotFather type /setdescription select bot type description text Finally, if you clean conversation history with this chatbot, refresh the page and open chat you will see our description:\nMuch better, right?\nConclusion In this article, we configured profile picture, about and description texts, commands list. Some of them are needed to improve user experience with our bot, while others – to attract users and force them to use it.\nP.S.: will be happy to get questions from you and see you soon.\n","permalink":"https://taradevko.com/2017/02-06-configure-telegram-chatbot/","summary":"\u003cp\u003eIn previous articles (\u003ca href=\"/2017/01-02-write-stateful-bot-telegram-part-1/\"\u003epart 1\u003c/a\u003e, \u003ca href=\"/2017/01-09-write-stateful-bot-telegram-part-2/\"\u003epart2\u003c/a\u003e) we created chatbot for Telegram. As I promised, here we will configure some settings of our bot:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ecommands list\u003c/li\u003e\n\u003cli\u003eprofile photo\u003c/li\u003e\n\u003cli\u003eabout text\u003c/li\u003e\n\u003cli\u003ebot description\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eLet\u0026rsquo;s begin by configuring our commands list.\u003c/p\u003e\n\u003ch2 id=\"commands-list\"\u003eCommands list\u003c/h2\u003e\n\u003cp\u003eCommand list represents the commands supported by the bot. Telegram suggests them when user types / in the chat with our bot. Each command in this list has a name, which starts with ‘/’, and a text description. Command name length is limited to 32 characters.\u003c/p\u003e","title":"Bot Dev: How to configure chatbot in Telegram"},{"content":"Once I needed to write several debugging servlets for some functionality. Those servlets had to accept a big pile of parameters and I was tired of having tens of rows like String paramValue = request.getParameter(\u0026ldquo;paramName\u0026rdquo;). Luckily, I\u0026rsquo;ve already known Sling Models. So I\u0026rsquo;ve decided to find the way to inject these parameters into the model, so I could write just request.adaptTo(MyModel.class) . Unfortunately, I found nothing, so decided to write it myself. So in this article, I will show you how to write custom Sling Model injector on the example of Sling Model Request Parameter Injector.\nCustom Sling Model Injector To write it, we need to implement an OSGi service, which inherits from Injector interface. It will force us to implement 2 methods:\ngetName - returns string, which will be used as logical name for the injector. getValue - returns a value for an injection point. The last one has several parameters and we will review some of them a bit later.\nSling Model Request Parameter Injector Let\u0026rsquo;s imagine, that we have model RequestParamsInjected where we have several fields, which should be populated with request parameters.\npackage com.taradevko.aem.model; import com.taradevko.aem.model.injector.annotation.RequestParameter; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.models.annotations.Model; import java.util.HashMap; import java.util.Map; @Model(adaptables = SlingHttpServletRequest.class) public class RequestParamsInjected { private static final Map\u0026lt;Object, String\u0026gt; CONTENT = new HashMap\u0026lt;\u0026gt;(); static { CONTENT.put(\u0026#34;param1\u0026#34;, \u0026#34;Content 1\u0026#34;); CONTENT.put(963, \u0026#34;Content 2\u0026#34;); } private String stringParam; private Integer integerParam; public String getStringContent() { return CONTENT.get(stringParam); } public String getIntegerContent() { return CONTENT.get(integerParam); } } For injecting values into the model fields, we need to annotate them. Actually, we can use existing annotations @Inject and @Source but it\u0026rsquo;s not flexible at all. We will write our own annotation!\npackage com.taradevko.aem.model.injector.annotation; import org.apache.sling.models.annotations.Source; import org.apache.sling.models.spi.injectorspecific.InjectAnnotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @InjectAnnotation @Source(\u0026#34;request-parameter\u0026#34;) public @interface RequestParameter { } We mark an annotation to be used with fields only, however you can have it for methods and parameters as well if you specify them in @Target. Then we declare our annotation as custom injection annotation. And finally, we specify which logical name to be used with for annotation.\nNow we can mark fields with new annotations:\npackage com.taradevko.aem.model; ... @Model(adaptables = SlingHttpServletRequest.class) public class RequestParamsInjected { .... @RequestParameter private String stringParam; @RequestParameter private Integer integerParam; ... } Without injector our annotation is useless. Let\u0026rsquo;s fix it:\npackage com.taradevko.aem.model.injector; import com.taradevko.aem.model.injector.annotation.RequestParameter; import org.apache.commons.lang3.StringUtils; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Property; import org.apache.felix.scr.annotations.Service; import org.apache.sling.models.spi.DisposalCallbackRegistry; import org.apache.sling.models.spi.Injector; import org.osgi.framework.Constants; import javax.servlet.ServletRequest; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Type; @Component @Service @Property(name = Constants.SERVICE_RANKING, intValue = Integer.MIN_VALUE) public class RequestParameterInjector implements Injector { @Override public String getName() { return \u0026#34;request-parameter\u0026#34;; } @Override public Object getValue(final Object adaptable, final String fieldName, final Type type, final AnnotatedElement annotatedElement, final DisposalCallbackRegistry disposalCallbackRegistry) { if (adaptable instanceof ServletRequest) { final ServletRequest request = (ServletRequest) adaptable; if (type instanceof Class\u0026lt;?\u0026gt;) { Class\u0026lt;?\u0026gt; fieldClass = (Class\u0026lt;?\u0026gt;) type; return getValue(request, fieldClass, fieldName); } } return null; } private Object getValue(final ServletRequest request, final Class\u0026lt;?\u0026gt; fieldClass, final String fieldName) { String parameterValue = request.getParameter(fieldName); if (StringUtils.isBlank(parameterValue)) { return null; } if (fieldClass.equals(Integer.class)) { try { return Integer.parseInt(parameterValue); } catch (NumberFormatException ex) { //got exception, so not an integer, return null; return null; } } return parameterValue; } } Earlier we discussed briefly methods we override above. Now it\u0026rsquo;s time to go through the method getValue:\nfor now we will use first 3 arguments: adaptable - object which Sling tries to adapt from. We expect it to be the request; fieldName - name of the field. which has been annotated; type - the declared type of the injection point; first, we check if adaptable is request and type is Class; then we call method getValue where we get parameter by field name and cast it to the integer if injection field is of type Integer; finally, we return integer, string or null value. For testing, let\u0026rsquo;s create simple component:\n\u0026lt;pre data-sly-use.model=\u0026#34;com.taradevko.aem.model.RequestParamsInjected\u0026#34;\u0026gt; Your parameter brought next content: ${model.stringContent} ${model.integerContent} \u0026lt;/pre\u0026gt; We are ready to test it:\nAs you can see, both parameters were injected into the fields successfully. But what if one of the parameters is not specified? Our model will not be adapted because both fields are required. Other annotations, like OSGiService, has nice attribute optional which can be used in such cases. Let\u0026rsquo;s add such feature for our annotation:\npackage com.taradevko.aem.model.injector.annotation; ... public @interface RequestParameter { boolean optional() default false; } And now adapt model as well:\npackage com.taradevko.aem.model; ... @Model(adaptables = SlingHttpServletRequest.class) public class RequestParamsInjected { ... @RequestParameter(optional = true) private Integer integerParam; ... } Why not test it now?\nOops, does not work. But why?\nCustom Inject Annotation Processor It appears, that to make custom annotation fully working, we need to implement one more interface - InjectAnnotationProcessorFactory (note that in latest versions this interface is marked as deprecated and StaticInjectAnnotationProcessorFactory should be used instead).\nLet\u0026rsquo;s implement it.\npackage com.taradevko.aem.model.injector; ... @Component @Service @Property(name = Constants.SERVICE_RANKING, intValue = Integer.MIN_VALUE) public class RequestParameterInjector implements Injector, InjectAnnotationProcessorFactory { ... @Override public InjectAnnotationProcessor createAnnotationProcessor(final Object adaptable, final AnnotatedElement element) { // check if the element has the expected annotation RequestParameter annotation = element.getAnnotation(RequestParameter.class); if (annotation != null) { return new RequestParameterAnnotationProcessor(annotation); } return null; } private static class RequestParameterAnnotationProcessor extends AbstractInjectAnnotationProcessor { private final RequestParameter annotation; RequestParameterAnnotationProcessor(RequestParameter annotation) { this.annotation = annotation; } @Override public Boolean isOptional() { return annotation.optional(); } } } There, we create annotation processor, which overrides isOptional and returns corresponding value from the annotation. After this changes, it works:\nCustom Annotation Attribute Let\u0026rsquo;s imagine that now we have new requirement - we need to get parameter from the request, but we do not know its name. However, we have a regular expression which we can use to find it.\nFirst, we need to update our annotation - add regexp attribute to it:\npackage com.taradevko.aem.model.injector.annotation; ... public @interface RequestParameter { boolean optional() default false; String regexp() default \u0026#34;\u0026#34;; } Then we need to update injector:\npackage com.taradevko.aem.model.injector; ... public class RequestParameterInjector implements Injector, InjectAnnotationProcessorFactory { ... @Override public Object getValue(final Object adaptable, final String fieldName, final Type type, final AnnotatedElement annotatedElement, final DisposalCallbackRegistry disposalCallbackRegistry) { if (adaptable instanceof ServletRequest) { final ServletRequest request = (ServletRequest) adaptable; String parameterName = null; final RequestParameter annotation = annotatedElement.getAnnotation(RequestParameter.class); if (annotation != null \u0026amp;\u0026amp; StringUtils.isNotBlank(annotation.regexp())) { parameterName = findParameterName(request, annotation.regexp()); } if (type instanceof Class\u0026lt;?\u0026gt;) { Class\u0026lt;?\u0026gt; fieldClass = (Class\u0026lt;?\u0026gt;) type; return getValue(request, fieldClass, StringUtils.defaultString(parameterName, fieldName)); } } return null; } private String findParameterName(final ServletRequest request, final String paramNameRegexp) { final Enumeration parameterNames = request.getParameterNames(); while (parameterNames.hasMoreElements()) { final String parameterName = (String) parameterNames.nextElement(); if (parameterName.matches(paramNameRegexp)) { return parameterName; } } return null; } ... } There, before we can get value, we need to find parameter\u0026rsquo;s name if regular expression is provided. To do this, we iterate over all parameters and match them with regexp from our annotation.\nAnd add a new field with specified regular expression:\npackage com.taradevko.aem.model; ... @Model(adaptables = SlingHttpServletRequest.class) public class RequestParamsInjected { private static final Map\u0026lt;Object, String\u0026gt; CONTENT = new HashMap\u0026lt;\u0026gt;(); static { CONTENT.put(\u0026#34;param1\u0026#34;, \u0026#34;Content 1\u0026#34;); CONTENT.put(963, \u0026#34;Content 2\u0026#34;); CONTENT.put(\u0026#34;regexpparam\u0026#34;, \u0026#34;Content for regexp\u0026#34;); } ... @RequestParameter(regexp = \u0026#34;\\\\d\\\\w{2}\\\\d\u0026#34;, optional = true) private String regexpParam; ... public String getRegexpContent() { return CONTENT.get(regexpParam); } } Finally, let\u0026rsquo;s adapt our test component:\n\u0026lt;pre data-sly-use.model=\u0026#34;com.taradevko.aem.model.RequestParamsInjected\u0026#34;\u0026gt; Your parameter brought next content: string: ${model.stringContent} integer: ${model.integerContent} regexp: ${model.regexpContent} \u0026lt;/pre\u0026gt; Test it? Test it!\nGreat, it works as expected.\nConclusion: Even though we\u0026rsquo;ve implemented pretty basic Request Parameter injector, it can give you the insight of how to write more advanced version. Being able to write custom injectors gives nice tool for making your logic cleaner and more maintainable.\nP.S.: will be happy to get feedback and questions from you and see you soon.\nYou can find the complete code in this GitHub repo.\n","permalink":"https://taradevko.com/2017/01-25-sling-model-request-parameter-injector/","summary":"\u003cp\u003eOnce I needed to write several debugging servlets for some functionality. Those servlets had to accept a big pile of parameters and I was tired of having tens of rows like  String paramValue = request.getParameter(\u0026ldquo;paramName\u0026rdquo;). Luckily, I\u0026rsquo;ve already known Sling Models. So I\u0026rsquo;ve decided to find the way to inject these parameters into the model, so I could write just request.adaptTo(MyModel.class) . Unfortunately, I found nothing, so decided to write it myself. So in this article, I will show you how to write custom Sling Model injector on the example of Sling Model Request Parameter Injector.\u003c/p\u003e","title":"Sling Models: How to write Sling Model Injector"},{"content":"In the first article, we implemented pretty simple Telegram bot, which gathers user requests to fix something. It’s already can be used in our business while we want to post-process requests with a separate tool. But, maybe, it would be too easy for us, so we will go ahead.\nApplication Requirements In this article we will add next features to our bot:\ninitial authentication of our repairers requests broadcasting to our repairers request assignment to the repairer Telegram Bot Deeplinking Let’s imagine that we have some separate administrative tool for our bot. And there we defined several unique IDs and assigned them to our employees/friends ( friends-employees? Why not). And now we need to authenticate them in our bot, so we can send them fix requests from our customers. We can use deep linking from Telegram to solve this task.\nIn simple words: we will provide our repairers with a link to the bot which already contains some unique ID so we can identify each separate user. Let’s try this with simple start command which will return us our unique id.\n//src/command/start.js \u0026#39;use strict\u0026#39;; import log from \u0026#39;npmlog\u0026#39;; export default class Start { handle(message, bot) { return bot.sendMessage(message.from, `I got start parameter from you ${message.option}`); } } And updated command router bellow:\n//src/command/index.js \u0026#39;use strict\u0026#39;; import log from \u0026#39;npmlog\u0026#39;; import FixRequest from \u0026#39;./fixRequest\u0026#39;; import Help from \u0026#39;./help\u0026#39;; import MyRequests from \u0026#39;./myRequests\u0026#39;; import Start from \u0026#39;./start\u0026#39;; const DEFAULT_HANDLER_KEY = \u0026#39;help\u0026#39;; export default class HandlerRouter { constructor() { this.handlers = { \u0026#39;help\u0026#39;: new Help(), \u0026#39;fix\u0026#39;: new FixRequest(), \u0026#39;myrequests\u0026#39;: new MyRequests(), \u0026#39;start\u0026#39;: new Start() } } getCommandHandler(message) { let command = message.command; log.verbose(\u0026#39;Handler\u0026#39;, `Looking for the handler for command ${command}`); if (command.indexOf(\u0026#39;/\u0026#39;) == 0) { command = command.substr(1); } return this.handlers[command] || this.handlers[DEFAULT_HANDLER_KEY]; } } To be able to communicate to our bot, in the root of our project directory, we start our nodejs application with the command:\nNODE_ENV=development TELEGRAM_TOKEN=YOUR_TOKEN npm start If now we open link http://telegram.me/repairMyStuffBot?start=someUniqueKey (do not forget to replace bot name) we will see chat with our bot and button start:\nAfter clicking on start:\nSeems to work as expected. We do not have that admin tool and we still need some content for testing. Let’s add database seeding for our MongoDB. Database seeding is the initial seeding of a database with data – surprised, huh?\nFirst, we will adjust config.\n//src/config/index.js \u0026#39;use strict\u0026#39;; import _ from \u0026#39;lodash\u0026#39;; const all = { telegram: { token: process.env.TELEGRAM_TOKEN || \u0026#39;\u0026#39;, externalUrl: process.env.EXTERNAL_URL || \u0026#39;\u0026#39;, port: process.env.PORT || 443, host: \u0026#39;0.0.0.0\u0026#39; }, logLevel: \u0026#39;verbose\u0026#39; }; const envs = { \u0026#39;production\u0026#39;: { logLevel: \u0026#39;info\u0026#39;, mongo: { uri: process.env.MONGODB_URI || process.env.MONGOHQ_URL || process.env.OPENSHIFT_MONGODB_DB_URL + process.env.OPENSHIFT_APP_NAME, seedDB: false } }, \u0026#39;development\u0026#39;: { logLevel: \u0026#39;verbose\u0026#39;, mongo: { uri: \u0026#39;mongodb://localhost/repairmystuff\u0026#39;, seedDB: true } } }; export default _.merge(all, envs[process.env.NODE_ENV]); In this code snippet, we added extra field seedDB - we will seed DB only in development mode (we do not wanna to overwrite this collection on Production instance, right?).\nNext, we want to work on the new model for our users.\n//src/services/user/user.model.js \u0026#39;use strict\u0026#39;; import mongoose from \u0026#39;mongoose\u0026#39;; const UserSchema = new mongoose.Schema({ startId: String, type: { type: String, enum : [\u0026#39;REGULAR\u0026#39;,\u0026#39;REPAIRER\u0026#39;], default : \u0026#39;REGULAR\u0026#39; }, userId: String }); export default mongoose.model(\u0026#39;User\u0026#39;, UserSchema); The Model contains only 3 fields (ok, actually it’s 4, but we do not define _id field explicitly):\nstartId – unique id for user authentication userID – associated telegram userId with id above type – can have one of 2 values: REGULAR and REPAIRER. Each value will be used for a separate group of the users. Users which got link with correct start ID will get REPAIRER value, all others - REGULAR And now, finally, seeding code:\n//src/config/seeddb.js \u0026#39;use strict\u0026#39;; import User from \u0026#39;../services/user/user.model\u0026#39; export default class SeedDb { static seed() { return User.find({}).remove() .then(()=\u0026gt; { User.create({ startId: \u0026#39;repairer1\u0026#39;, type: \u0026#39;REPAIRER\u0026#39; }, { startId: \u0026#39;repairer2\u0026#39;, type: \u0026#39;REPAIRER\u0026#39; }, { startId: \u0026#39;repairer3\u0026#39;, type: \u0026#39;REPAIRER\u0026#39; }) }) .then(() =\u0026gt; { console.log(\u0026#39;Finished populating users\u0026#39;); }); } } Here we remove all items in users collection and insert several predefined repairers. So seeding is here, the model is here, but how about execute it? Let’s navigate to our index.js:\n//src/index.js \u0026#39;use strict\u0026#39;; import log from \u0026#39;npmlog\u0026#39;; import mongoose from \u0026#39;mongoose\u0026#39;; import Promise from \u0026#39;bluebird\u0026#39;; import config from \u0026#39;./config\u0026#39; import Messenger from \u0026#39;./lib/messenger\u0026#39;; import SeedDB from \u0026#39;./config/seeddb\u0026#39;; mongoose.connect(config.mongo.uri); mongoose.connection.on(\u0026#39;error\u0026#39;, function(err) { log.error(\u0026#39;Main\u0026#39;, \u0026#39;MongoDB connection error: \u0026#39; + err); process.exit(-1); }); mongoose.Promise = Promise; if (config.mongo.seedDB) { SeedDB.seed() } log.level = config.logLevel; const telegram = new Messenger(); telegram.listen().then(() =\u0026gt; log.info(\u0026#39;Main\u0026#39;, \u0026#39;Bot started\u0026#39;)); Nothing special here – we just execute seed method if seeding is enabled. Let’s run our application and check results.\nTELEGRAM_TOKEN=YOUR_TOKEN NODE_ENV=development npm start Console output:\nAnd now check MongoDB:\nWe are done with database seeding.\nUser DAO At this point, you have your mongoose scheme for User implemented and we can go ahead just with it. But the title of this section says User DAO! Why do we need dao? If it is implemented in a good way, we can suffer much less in case of persistence storage changes (and trust me, we will have this change =) )\n//src/command/start.js:5 \u0026#39;use strict\u0026#39;; import log from \u0026#39;npmlog\u0026#39;; import User from \u0026#39;./user.model\u0026#39;; export default class UserDao { findRepairerByStartId(startId) { log.verbose(\u0026#39;UserRepository\u0026#39;, `Searching for repairer with start id ${startId}`); return User.findOne({\u0026#39;startId\u0026#39; : startId, \u0026#39;type\u0026#39; : \u0026#39;REPAIRER\u0026#39;}); } save(user) { log.verbose(\u0026#39;UserRepository\u0026#39;, `Saving user ${JSON.stringify(user)} `); return user.save(); } } For now, it will have 2 methods only. First method is for getting repairer by ID. Second – just for saving user.\nSo we are ready to use the power of deep linking. It’s time to rewrite start command.\n//src/command/start.js \u0026#39;use strict\u0026#39;; import log from \u0026#39;npmlog\u0026#39;; import UserRepository from \u0026#39;../services/user/user.dao\u0026#39;; function sendSimpleUserResponse(userId, bot) { return bot.sendMessage(userId, \u0026#39;Hello. I can help you to fix your broken stuff. Write /help to know more.\u0026#39;); } function sendRepairerUserResponse(userId, bot) { return bot.sendMessage(userId, \u0026#39;You have been registered as repairer. You will get requests as soon as new are submitted.\u0026#39;); } function sendErrorResponse(userId, bot) { return bot.sendMessage(userId, \u0026#39;Startup parameters were not recognized.\u0026#39;); } export default class Start { constructor() { this.userDao = new UserRepository(); } handle(message, bot) { const userId = message.from; const startId = message.option; log.verbose(\u0026#39;Start\u0026#39;, `Got start command with startId \u0026#39;${startId}\u0026#39;`); if (!startId) { return sendSimpleUserResponse(userId, bot); } return this.userDao.findRepairerByStartId(startId) .then(user =\u0026gt; { if (user) { user.userId = userId; log.verbose(\u0026#39;Start\u0026#39;, `Found repairer user, will set userId for him`); return this.userDao.save(user); } else { log.verbose(\u0026#39;Start\u0026#39;, `User not found by given id`); throw new Error(\u0026#39;User not found\u0026#39;); } }) .then(() =\u0026gt; { return sendRepairerUserResponse(userId, bot); }) .catch((error) =\u0026gt; { return sendErrorResponse(userId, bot); }) } } Now if we open link with id from our database we will be registered as a repairer. You can access it with a link telegram.me/YourBotName?start=repairer1\nAnd in MongoDB, we can see that userId has been assigned to the user entry.\nBroadcasting messages Now, with recognized repairers, we will need to broadcast user requests in order to notify them.\nLet’s first rearrange some files:\nnew folder src/command/request fixRequest.js → new folder myRequests.js → new folder adapt src/command/index.js We will need new method in our UserDao – method which will return all repairers.\n//src/services/user/user.dao.js:9 ... findAllRegisteredRepairers() { log.verbose(\u0026#39;UserRepository\u0026#39;, `Searching for all repairers`); return User.find({\u0026#39;type\u0026#39; : \u0026#39;REPAIRER\u0026#39;, userId : {$ne : null}}); } ... Here we search for our menders who has not null userId field (so user already got a link and started a chat with our bot). And we are ready to implement broadcasting itself.\n//src/command/request/fixRequest.broadcast.js \u0026#39;use strict\u0026#39;; import log from \u0026#39;npmlog\u0026#39;; import UserRepository from \u0026#39;../../services/user/user.dao\u0026#39;; export default class FixRequestBroadcastHandler { constructor() { this.userDao = new UserRepository(); } handle(fixRequest, bot) { return this.userDao.findAllRegisteredRepairers() .then(repairers =\u0026gt; { repairers.forEach(repairer =\u0026gt; { log.verbose(\u0026#39;FixRequestBroadcastHandler\u0026#39;, `Send request to user ${JSON.stringify(fixRequest)}`); return bot.sendMessage(repairer.userId, `New request: ${fixRequest.request}`) }) }) .catch(err =\u0026gt; { log.error(\u0026#39;FixRequestBroadcastHandler\u0026#39;, `Was not able to broadcast fix request ${JSON.stringify(fixRequest)} because of ${JSON.stringify(err)}`); }); } } In this code snippet, we\u0026rsquo;re doing next: first, we find all registered menders and then we send given request to each of them.This command is not a standalone command, but will be triggered from the FixRequest. So we need update it as well.\n//src/command/request/fixRequest.js \u0026#39;use strict\u0026#39;; import log from \u0026#39;npmlog\u0026#39;; import FixRequest from \u0026#39;../../services/fixRequest/fixRequest.model\u0026#39;; import FixRequestBroadcast from \u0026#39;./fixRequest.broadcast\u0026#39;; export default class FixRequestHandler { constructor() { this.broadcaster = new FixRequestBroadcast(); } handle(message, bot) { const fixRequest = new FixRequest({ \u0026#39;userId\u0026#39;: message.from, \u0026#39;request\u0026#39;: message.option }); return fixRequest.save() .then(() =\u0026gt; { log.verbose(\u0026#39;FixRequest\u0026#39;, `Saved fix request ${JSON.stringify(fixRequest)}`); this.broadcaster.handle(fixRequest, bot); return bot.sendMessage(message.from, \u0026#39;Your request has been submitted. We will contact you soon.\u0026#39;); }) .catch(err =\u0026gt; { log.error(\u0026#39;FixRequest\u0026#39;, `Was not able to save request ${JSON.stringify(fixRequest)}, got error: ${JSON.stringify(err)}}`); return bot.sendMessage(message.from, \u0026#39;Sorry we can not accept your request. Try later.\u0026#39;); }); } } Now, after request is saved, we broadcast it. Let’s test it.\nTo do this we will perform next steps:\nregister as repairer send fix request (yes, not really real life situation, it\u0026rsquo;s just for testing purposes). check message from the bot If we will stop at this point, is there anything that can go wrong? Definitely yes - 10 repairers can be interested in one request. And they do not know if somebody will arrive at the place faster. You need a mechanism of requests assignment so that our menders do not have such problems.\nThere are several options how to do this in telegram, but we will go with callback buttons.\nLet’s check code bellow.\n//src/command/request/fixRequest.broadcast.js \u0026#39;use strict\u0026#39;; import log from \u0026#39;npmlog\u0026#39;; import UserRepository from \u0026#39;../../services/user/user.dao\u0026#39;; function getAssignRequestKeyboard(requestId) { return { reply_markup: JSON.stringify({ inline_keyboard: [ [ { text: \u0026#39;Assign to me\u0026#39;, callback_data: `assignRequest_${requestId}` } ] ] }) }; } export default class FixRequestBroadcastHandler { constructor() { this.userDao = new UserRepository(); } handle(fixRequest, bot) { return this.userDao.findAllRegisteredRepairers() .then(repairers =\u0026gt; { repairers.forEach(repairer =\u0026gt; { log.verbose(\u0026#39;FixRequestBroadcastHandler\u0026#39;, `Send request to user ${JSON.stringify(fixRequest)}`); bot.sendMessage(repairer.userId, `New request: ${fixRequest.request}`, getAssignRequestKeyboard(fixRequest._id)) }) }) .catch(err =\u0026gt; { log.error(\u0026#39;FixRequestBroadcastHandler\u0026#39;, `Was not able to broadcast fix request ${JSON.stringify(fixRequest)} because of ${JSON.stringify(err)}`); }); } } Method sendMessage can accept the third parameter as reply options. And one of the options may be inline_keyboard – 2-dimensional array. First dimension is rows and 2nd – columns.\nIn our case it’s only one button, so we do not care about this. What we care about is callback_data. It consists of 2 parts:\nprefix ‘assignRequest_’ which will be used, to route user response to correct handler, which we will implement next requestId – to determine which request should be assigned to the repairer, who will press the button. So, when method handle is called we send this request to all repairers and also show button with text “Assign to me”. When repairer clicks on it – request id with prefix assignRequest_ will be sent back to the bot.\nFor testing, we will use the same (repairer) account for submitting a request and applying on it.\nNow we have button, but it does nothing, because we do not handle button click. For handling callback query messages (messages, our bot gets when user clicks on such buttons) we need to wrap it into our dto:\n//src/lib/callbackQueryMessage.js export default class CallbackQueryMessage { static from(telegramMessage) { return { from: telegramMessage.from.id, data: telegramMessage.data } } } Nothing special here, so we will implement handler for such messages now.\n//src/command/request/assignRequest.js \u0026#39;use strict\u0026#39;; import FixRequest from \u0026#39;../../services/fixRequest/fixRequest.model\u0026#39;; export default class AssignRequestHandler { handleCallbackQuery(message, bot) { const requestId = message.data.split(\u0026#39;_\u0026#39;)[1]; return FixRequest.findById(requestId) .then(result =\u0026gt; { if (!result) { return bot.sendMessage(message.from, \u0026#39;Sorry but we found no such request.\u0026#39;); } if (result.repairer) { return bot.sendMessage(message.from, \u0026#39;Request already assigned to another repairer.\u0026#39;); } result.repairer = message.from; result.status = \u0026#39;ASSIGNED\u0026#39;; return result.save() .then(() =\u0026gt; { return bot.sendMessage(message.from, \u0026#39;Request has been assigned to you. Hurry up, somebody is waiting for the hero.\u0026#39;); }); }); } } Callback query handler has a different contract - instead of implementing handle method, it implements handleCallbackQuery.\nLet’s go through the code:\nwe get request id from the message. As long as message data contains current handler’s key – we need to remove it Then check if such request exists and is not assigned to another repairer. Then assign it. After changes are saved – notify repairer about this In real life application, we would need to add more checks here (like if current user is a real repairer), but for the sake of simplicity we skipped it.\nWe are almost done here. Let\u0026rsquo;s check src/lib/messenger.js . Here we handle only “text” events. Now we need to handle callback queries as well. To do this, we need to add one row right after the text handling:\nthis.bot.on(\u0026#39;callback_query\u0026#39;, this.handleCallbackQuery.bind(this)); And new method:\nhandleCallbackQuery(msg) { const message = CallbackQueryMessage.from(msg); handlerRouter.getCallbackQueryHandler(message) .handleCallbackQuery(message, this.botWrapper) .catch(err =\u0026gt; { log.error(\u0026#39;Messenger\u0026#39;, `Got error from callback query handler: ${err}`); }); } Also, do not forget to add import for CallbackQueryMessage. And, finally, let\u0026rsquo;s add mapping to the router of our handlers:\n//src/command/index.js \u0026#39;use strict\u0026#39;; import log from \u0026#39;npmlog\u0026#39;; import FixRequest from \u0026#39;./request/fixRequest\u0026#39;; import Help from \u0026#39;./help\u0026#39;; import MyRequests from \u0026#39;./request/myRequests\u0026#39;; import Start from \u0026#39;./start\u0026#39;; import AssignRequest from \u0026#39;./request/assignRequest\u0026#39;; const DEFAULT_HANDLER_KEY = \u0026#39;help\u0026#39;; export default class HandlerRouter { constructor() { this.handlers = { \u0026#39;help\u0026#39;: new Help(), \u0026#39;fix\u0026#39;: new FixRequest(), \u0026#39;myrequests\u0026#39;: new MyRequests(), \u0026#39;start\u0026#39;: new Start() }; this.callbackQueryHandlers = { \u0026#39;assignRequest\u0026#39;: new AssignRequest() } } getCommandHandler(message) { let command = message.command; log.verbose(\u0026#39;HandlerRouter\u0026#39;, `Looking for the handler for command ${command}`); if (command.indexOf(\u0026#39;/\u0026#39;) == 0) { command = command.substr(1); } return this.handlers[command] || this.handlers[DEFAULT_HANDLER_KEY]; } getCallbackQueryHandler(message) { let key = message.data; log.verbose(\u0026#39;HandlerRouter\u0026#39;, `Looking for the callback query handler with key ${key}`); if (key.indexOf(\u0026#39;_\u0026#39;) \u0026gt; -1) { key = key.substr(0, key.indexOf(\u0026#39;_\u0026#39;)); } return this.callbackQueryHandlers[key]; } } We created another dictionary here for callback query handlers and separate method to get such handlers. And again, message data contains handler key as a prefix, so we need to get it from the data.\nLet’s do final checks of what we have done – authenticate yourself as a repairer, and submit request. After you clicked on the button “Assign to me” you should see message:\nAnd let’s check MongoDB:\nWe can see that request is assigned to the repairer.\nConclusion So now have bot with pretty handy functionality. In the article, we learned some new features which are provided by Telegram API, like deep linking and callback buttons. In the next article we will add some humanity to it.\nP.S.: will be happy to get questions from you and see you soon.\nYou can find the complete code in this GitHub repo.\n","permalink":"https://taradevko.com/2017/01-09-write-stateful-bot-telegram-part-2/","summary":"\u003cp\u003eIn the \u003ca href=\"/2017/01-02-write-stateful-bot-telegram-part-1/\"\u003efirst article\u003c/a\u003e, we implemented pretty simple Telegram bot, which gathers user requests to fix something. It’s already can be used in our business while we want to post-process requests with a separate tool. But, maybe, it would be too easy for us, so we will go ahead.\u003c/p\u003e\n\u003ch3 id=\"application-requirements\"\u003eApplication Requirements\u003c/h3\u003e\n\u003cp\u003eIn this article we will add next features to our bot:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003einitial authentication of our repairers\u003c/li\u003e\n\u003cli\u003erequests broadcasting to our repairers\u003c/li\u003e\n\u003cli\u003erequest assignment to the repairer\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"telegram-bot-deeplinking\"\u003eTelegram Bot Deeplinking\u003c/h2\u003e\n\u003cp\u003eLet’s imagine that we have some separate administrative tool for our bot. And there we defined several unique IDs and assigned them to our employees/friends ( friends-employees? Why not). And now we need to authenticate them in our bot, so we can send them fix requests from our customers. We can use \u003ca href=\"https://core.telegram.org/bots#deep-linking\"\u003edeep linking\u003c/a\u003e from Telegram to solve this task.\u003c/p\u003e","title":"Bot Dev: Write a stateful Telegram bot – Part 2"},{"content":"At some point, Hexagonal architecture has been introduced to me. It espouses a one-way flow of dependencies - from the outside, in. And this is a great way to increase maintainability. However, we still need to access request and other external resources in our Sling Models. That is where @Self annotation can be used.\n@Self Annotation According to the documentation, this annotation injects the adaptable object itself. If the @Self annotation is present it is tried to adapt the adaptable to the field type.\nSo anytime, when you need to extract some data from the request or resource in your model (like remote user id or parent resource path), you can create a model, which extract this information and then inject it with @Self into the model, where you need this information. The first model (which works with the external resource) can be treated as adapter. So you keep your logic in the model away from the external dependencies, because it\u0026rsquo;s adapter\u0026rsquo;s business, how to get some information from request/resource/etc.\nProject setup As a boilerplate, I\u0026rsquo;ve used this archetype, and then removed useless things:\nmvn archetype:generate -DarchetypeGroupId=com.adobe.granite.archetypes -DarchetypeArtifactId=aem-project-archetype -DarchetypeVersion=10 -DarchetypeRepository=https://repo.adobe.com/nexus/content/groups/public/ @Self example Let\u0026rsquo;s imagine, that we need to get suffix from the request in our Sling Model. Then, depending on it, we will return different content. So we will create our first model - the one, which extracts suffix from the request:\npackage com.taradevko.aem.model; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.models.annotations.Model; import javax.inject.Inject; @Model(adaptables = SlingHttpServletRequest.class) public class RequestAdapter { @Inject private SlingHttpServletRequest request; public String getSuffix() { return request.getRequestPathInfo().getSuffix(); } } There we inject the request we adapted this model from and one method for returning suffix. Now we are ready to use @Self annotation:\npackage com.taradevko.aem.model; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.models.annotations.Model; import org.apache.sling.models.annotations.injectorspecific.Self; import java.util.HashMap; import java.util.Map; @Model(adaptables = SlingHttpServletRequest.class) public class SelfModel { private static final Map\u0026lt;String, String\u0026gt; CONTENT = new HashMap\u0026lt;\u0026gt;(); static { CONTENT.put(\u0026#34;/suffix1\u0026#34;, \u0026#34;Content 1\u0026#34;); CONTENT.put(\u0026#34;/suffix2\u0026#34;, \u0026#34;Content 2\u0026#34;); } @Self private RequestAdapter requestAdapter; public String getContent() { return CONTENT.get(requestAdapter.getSuffix()); } } Let\u0026rsquo;s go through the code snippet above:\nwe have a static map with demo content where the key is the suffix we expect to get from the user; we inject RequestAdapter using @Self annotation; method getContent, where we use our request adapter. Finally, we will add next HTL markup to our component:\n\u0026lt;pre data-sly-use.model=\u0026#34;com.taradevko.aem.model.SelfModel\u0026#34;\u0026gt; Your suffix brought next content: ${model.content} \u0026lt;/pre\u0026gt; There we just initialize our model, call getter method on it and display the result. Let\u0026rsquo;s test it:\nAs you can see, we got the string which has been mapped for the suffix /suffix1, so it works!\nP.S.: will be happy to get questions from you and see you soon.\nYou can find the complete code in this GitHub repo.\n","permalink":"https://taradevko.com/2017/01-05-sling-models-self-annotation/","summary":"\u003cp\u003eAt some point, \u003ca href=\"http://alistair.cockburn.us/Hexagonal+architecture\"\u003eHexagonal architecture\u003c/a\u003e has been introduced to me. It espouses a one-way flow of dependencies - from the outside, in.  And this is a great way to increase maintainability. However, we still need to access request and other external resources in our Sling Models. That is where @Self annotation can be used.\u003c/p\u003e\n\u003ch2 id=\"self-annotation\"\u003e@Self Annotation\u003c/h2\u003e\n\u003cp\u003eAccording to the documentation, this annotation injects the adaptable object itself. If the @Self annotation is present it is tried to adapt the adaptable to the field type.\u003c/p\u003e","title":"Sling Models: Why do I like @Self annotation?"},{"content":"Why do we need all these bots? Why people are talking about them so much? What the bot is good for? Look around, it\u0026rsquo;s 2017 outside and nobody uses phones for calls only. Even more: today a lot of people prefer chatting to calling somebody.\nBut, you are still forced to make calls to get a taxi or to understand why your LAN connection is down (oh man, you again forgot to pay for it!). How often are you listening to the music, when you are on hold, instead of doing real business? That\u0026rsquo;s fine when it\u0026rsquo;s Bach, but what if it\u0026rsquo;s Justin Bieber instead? They are just wasting your time!\nChat is a better medium for a lot of people. But, yeah, you still will wait until there is a free person to respond you. Ok, finally somebody responded to you, but what if that person is in bad mood? So it still looks too unreliable and complicated. That is where bots come into play.\nWhat We’ll Be Building In this series of articles, we will be exploring the world of chatbots. In particular, we will build an app which allows end users to submit requests for fixing their stuff. We will call this application FixMyStuff and we are starting with implementing bot. For development, we will use npm, Node.js, Babel (as ES6 transpiler), MongoDB as data storage and some libs. This tutorial assumes at least some prior knowledge of Node.js and MongoDB. If you are new to them, you better go through some tutorial for beginner.\nApplication Requirements As first steps we are going to have next:\nTelegram bot\nBot\u0026rsquo;s backend part written in Node.js with MongoDB\nEnd users are able to submit their fix requests\nUsers are able to check their requests and their status\nTelegram Bot Even though Telegram is not that popular as facebook messenger (especially in the USA and Western Europe) I\u0026rsquo;ve decided to start with it. Why? Just because I am using it.\nWhat should we start with? Obviously, creating the bot and getting it\u0026rsquo;s token so we can rule it. This step is pretty easy:\ngo to https://telegram.me/BotFather – telegram\u0026rsquo;s bot for creating bots\ntype /newbot\nBotFather will guide you through the steps. Generally, it will look like this:\nBellow last message you will find your token. Let\u0026rsquo;s create our project dir.\nmkdir repair-my-stuff \u0026amp;\u0026amp; cd repair-my-stuff The next step is to initialize our Node.js project. npm init and go through steps (usually default values are ok for us). Now let’s add some dependencies we need. That is how you package.json should look like (ok, you can change \u0026lsquo;author\u0026rsquo; field to your name).\n{ \u0026#34;name\u0026#34;: \u0026#34;fix-my-stuff\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;1.0.0\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Simple chatbot for requesting repair of some stuff\u0026#34;, \u0026#34;main\u0026#34;: \u0026#34;src/index.js\u0026#34;, \u0026#34;scripts\u0026#34;: { \u0026#34;start\u0026#34;: \u0026#34;NODE_ENV=development nodemon src/index.js --exec babel-node\u0026#34; }, \u0026#34;dependencies\u0026#34;: { \u0026#34;lodash\u0026#34;: \u0026#34;^4.16.4\u0026#34;, \u0026#34;mongoose\u0026#34;: \u0026#34;^4.5.9\u0026#34;, \u0026#34;node-telegram-bot-api\u0026#34;: \u0026#34;^0.23.3\u0026#34;, \u0026#34;npmlog\u0026#34;: \u0026#34;^4.0.0\u0026#34; }, \u0026#34;devDependencies\u0026#34;: { \u0026#34;babel-cli\u0026#34;: \u0026#34;^6.10.1\u0026#34;, \u0026#34;babel-core\u0026#34;: \u0026#34;6.10.4\u0026#34;, \u0026#34;babel-polyfill\u0026#34;: \u0026#34;^6.0.16\u0026#34;, \u0026#34;babel-preset-es2015\u0026#34;: \u0026#34;6.9.0\u0026#34;, \u0026#34;babel-preset-stage-2\u0026#34;: \u0026#34;^6.5.0\u0026#34;, \u0026#34;babel-register\u0026#34;: \u0026#34;^6.9.0\u0026#34;, \u0026#34;nodemon\u0026#34;: \u0026#34;^1.9.2\u0026#34; }, \u0026#34;author\u0026#34;: \u0026#34;Oleksandr Tarasenko\u0026#34;, \u0026#34;license\u0026#34;: \u0026#34;ISC\u0026#34; } Currently, it\u0026rsquo;s enough to have there only one script - start. There we set environment mode and run nodemon with preprocessing our code with babel.\nLet\u0026rsquo;s quickly go through dependencies:\nlodash – contains some useful methods, like object merge. mongoose – ORM for MongoDB node-telegram-bot-api – library which wraps Telegram API into a nice set of methods npmlog – lib for logging our messages. This will make our life easier babel-* - we are going to write our project in ES6 and for this we will use transpiler (es6 is supported on 99% by latest nodejs versions, but we can be not aware of nodejs version we will run our app on in production, so we will keep it safe) nodemon – lib for restarting our project on code change Now we should install dependencies:\nnpm install For babel we need to enable presets. That is why we create .babelrc file next to package json. This is its content:\n{ \u0026#34;presets\u0026#34;: [\u0026#34;es2015\u0026#34;] } Starting from babel 6, if you have no presets enabled, your code will be just parsed and generated again (unchanged). But we are writing in ECMAScript 2015 and want to transpile it to ES5. And yes, for using babel in our start script (where we use command line interface), we should install babel cli globally\nnpm install --global babel-cli App Setup Here\u0026rsquo;s our file structure:\nrepair-my-stuff/ ├── src │ ├── command \u0026lt;!--there we have our commands --\u0026gt; │ │ ├── fixRequest.js │ │ ├── help.js │ │ ├── index.js │ │ └── myRequests.js │ ├── config \u0026lt;!--there we store configuration of our app --\u0026gt; │ │ └── index.js │ ├── index.js \u0026lt;!--main script--\u0026gt; │ ├── lib \u0026lt;!--there we will put what is related to Telegram → │ │ ├── fixMyStuffBot.js │ │ ├── message.js │ │ └── messenger.js │ └── services \u0026lt;!--our services or telegram independent logic--\u0026gt; │ └── fixRequest │ └── fixRequest.model.js ├── package.json └── .babelrc As you can see, we put Telegram specific logic into the separate folder. Thought out this article, you will see that I am willing to reach a higher level of abstraction. In case you wanna to reuse existing logic with another bot (and I will cover this in one of next articles), you should keep bot-related logic away from your business logic.\nNow let\u0026rsquo;s get closer to our code. First, we will look into our main script:\n//src/index.js \u0026#39;use strict\u0026#39;; import log from \u0026#39;npmlog\u0026#39;; import mongoose from \u0026#39;mongoose\u0026#39;; import Promise from \u0026#39;bluebird\u0026#39;; import config from \u0026#39;./config\u0026#39; import Messenger from \u0026#39;./lib/messenger\u0026#39;; mongoose.connect(config.mongo.uri); mongoose.connection.on(\u0026#39;error\u0026#39;, function(err) { log.error(\u0026#39;Main\u0026#39;, \u0026#39;MongoDB connection error: \u0026#39; + err); process.exit(-1); }); mongoose.Promise = Promise; log.level = config.logLevel; const telegram = new Messenger(); telegram.listen().then(() =\u0026gt; log.info(\u0026#39;Main\u0026#39;, \u0026#39;Bot started\u0026#39;)); There we establish a connection to MongoDB with mongoose, force mongoose to use bluebird promises and set log level for npmlog. Then we create an instance of our Bot and start it.\nBefore going to the logic of our bot, let’s check our configuration:\n//src/config/index.js \u0026#39;use strict\u0026#39;; import _ from \u0026#39;lodash\u0026#39;; const all = { telegram: { token: process.env.TELEGRAM_TOKEN || \u0026#39;\u0026#39;, externalUrl: process.env.EXTERNAL_URL || \u0026#39;\u0026#39;, port: process.env.PORT || 443, host: \u0026#39;0.0.0.0\u0026#39; }, logLevel: \u0026#39;verbose\u0026#39; }; const envs = { \u0026#39;production\u0026#39;: { logLevel: \u0026#39;info\u0026#39;, mongo: { uri: process.env.MONGODB_URI || process.env.MONGOHQ_URL || process.env.OPENSHIFT_MONGODB_DB_URL + process.env.OPENSHIFT_APP_NAME } }, \u0026#39;development\u0026#39;: { logLevel: \u0026#39;verbose\u0026#39;, mongo: { uri: \u0026#39;mongodb://localhost/repairmystuff\u0026#39; } } }; export default _.merge(all, envs[process.env.NODE_ENV]); So there we have the configuration, which is common for all environments and several specific – development and production. At the end we merge common and current environment configurations. Telegram specific properties we will discuss later on. For now, we are interested in envs.development.mongo.uri. You should change it to match your local MongoDB instance (usually it\u0026rsquo;s the same).\nBot brains We are ready to look into our bot\u0026rsquo;s brains:\n//src/lib/messenger.js \u0026#39;use strict\u0026#39;; import TelegramBot from \u0026#34;node-telegram-bot-api\u0026#34;; import config from \u0026#39;../config\u0026#39;; export default class Messenger { constructor() { if (process.env.NODE_ENV === \u0026#39;production\u0026#39;) { this.bot = new TelegramBot(config.telegram.token, { webHook: { port: config.telegram.port, host: config.telegram.host } }); this.bot.setWebHook(config.telegram.externalUrl + \u0026#39;:443/bot\u0026#39; + config.telegram.token); } else { this.bot = new TelegramBot(config.telegram.token, { polling: {timeout: 10, interval: 100} }); } } listen() { this.bot.on(\u0026#39;text\u0026#39;, this.handleText.bind(this)); return Promise.resolve(); } handleText(msg) { } } As you can see, this file is not final. In method listen, we bind method handleText to this and set it as a callback on event \u0026rsquo;text\u0026rsquo;. And we return resolved Promise. Nothing interesting there so far.\nThe constructor looks more complicated. Let\u0026rsquo;s check what we have there.There are 2 available ways of getting data (in our case – messages) from Telegram server:\nwebhook – you give your url to the Telegram server, and when a new message arrives – message will be sent to you polling – every n seconds you make a request to the server and it responds to you with available data. With webhook your bot will respond to users much faster but it requires SSL certificate. Polling, on the other hand – is slower (because of intervals between requests). So in our case, we use webhook for the production environment and polling – for development, because in last case we do not need it to respond fast and we are not forced to mess with SSL certificate.\nIt\u0026rsquo;s time to write our first command:\n//src/command/help.js \u0026#39;use strict\u0026#39;; export default class Help { handle(message, bot) { return bot.sendMessage(message.from, \u0026#39;Sorry, we do not have any commands yet.\u0026#39;); } } There we just send a message to the user, we got this command from. We do not have any commands yet (yes, except help) so we tell user about this. sendMessage is one of the base methods in the library.\nTake a look at handle method\u0026rsquo;s signature. It will be our contract for all command handlers – they should implement method handle(message, bot), which returns Promise.\nRoute me Next, we need somehow to transport message from the messenger to our help command.\n//src/command/index.js \u0026#39;use strict\u0026#39;; import log from \u0026#39;npmlog\u0026#39;; import Help from \u0026#39;./help\u0026#39;; export default class HandlerRouter { constructor() { this.handlers = { \u0026#39;help\u0026#39;: new Help() } } getCommandHandler(message) { let command = message.command; log.verbose(\u0026#39;Handler\u0026#39;, `Looking for the handler for command ${command}`); if (command.indexOf(\u0026#39;/\u0026#39;) == 0) { command = command.substr(1); } return this.handlers[command]; } } Here we create a dictionary with available commands. And method, where we remove \u0026lsquo;/\u0026rsquo; from the command, getting handler from the dictionary and return it.\nNow it\u0026rsquo;s a time to connect our messenger and Handler Router. First, we should wrap message object from telegram with our own object to keep external dependencies away from our logic.\n//src/lib/message.js \u0026#39;use strict\u0026#39;; export default class Message { static from(telegramMessage) { let command, option; if (telegramMessage.text.indexOf(\u0026#39; \u0026#39;) \u0026gt; -1) { const parts = telegramMessage.text.split(\u0026#39; \u0026#39;); command = parts.shift(); option = parts.join(\u0026#39; \u0026#39;); } else { command = telegramMessage.text; } return { from: telegramMessage.from.id, command: command, option: option } } } Here we have one static method, where we split message on command itself and any other text which can go after it.\nLet’s get back to our messenger. We should add there 2 more imports and implement method handleText.\n//src/lib/messenger.js \u0026#39;use strict\u0026#39;; import TelegramBot from \u0026#34;node-telegram-bot-api\u0026#34;; import config from \u0026#39;../config\u0026#39;; import HandlerRouter from \u0026#34;../command\u0026#34;; import Message from \u0026#34;./message\u0026#34;; const handlerRouter = new HandlerRouter(); export default class Messenger { constructor() { ... } listen() { ... } handleText(msg) { const message = Message.from(msg); handlerRouter.getCommandHandler(message) .handle(message, this.bot); } } In method handleText we wrap message, find requested command and handle the message with it.\nTest it Finally, we are ready to test our bot. Next command is used to run the project (make sure, that your MongoDB instance is up and running).\nTELEGRAM_TOKEN=YOUR_TOKEN NODE_ENV=development npm start In you logs, you should see something similar to output bellow:\nHow about going to telegram to check if our bot works? Go to https://telegram.me/yourBotName. Type help and check output. I\u0026rsquo;ve got next:\nIf you got no answer – check your logs.\n“Fix” Command Now it\u0026rsquo;s time to create more useful command - /fix. With it the user will be able to submit his requests for fixing his broken things. First, we will try to send this command before we, actually, implement it. You will get nothing from the bot, but in logs you will see:\nverb Handler Looking handler for command /fix Unhandled rejection TypeError: Cannot read property \u0026#39;handle\u0026#39; of undefined Oops. Let’s check what has happened. In our HandlerRouter we return value from the dictionary. Usually, it\u0026rsquo;s handler instance, but what If there is no such handler? In such case we get undefined and, as a result, error. We need to fix it.\n//src/lib/messenger.js \u0026#39;use strict\u0026#39;; import log from \u0026#39;npmlog\u0026#39;; import Help from \u0026#39;./help\u0026#39;; const DEFAULT_HANDLER_KEY = \u0026#39;help\u0026#39;; export default class HandlerRouter { constructor() { this.handlers = { \u0026#39;help\u0026#39;: new Help() } } getCommandHandler(message) { let command = message.command; log.verbose(\u0026#39;Handler\u0026#39;, `Looking handler for command ${command}`); if (command.indexOf(\u0026#39;/\u0026#39;) == 0) { command = command.substr(1); } return this.handlers[command] || this.handlers[DEFAULT_HANDLER_KEY]; } } We changed method getCommandHandler to return default handler if there is no such command. We are using Help command handler for this.\nBot abstraction In one of the previous sections, I was talking about abstractions. And we have applied this principle in several places. But we forgot to apply this principle for Bot itself. Let’s fix it. We start with our wrapper for bot object. Here\u0026rsquo;s what the code looks like.\n//src/lib/fixMyStuffBot.js export default class TelegramBot { constructor (telegramBot) { this.bot = telegramBot; } sendMessage(userId, message, messageOptions) { return this.bot.sendMessage(userId, message, messageOptions); } } Currently, we just call method from API of the library we used. But in future, when you will want to adapt this code for another library(which works with, e.g. Facebook Messenger Bot API), your commands will not be affected by this change (ok, probably will be, but not that much). The biggest thing you will need to do – is to create an object with another implementation of sendMessage method.\nAlso here are some changes in messenger:\n//src/lib/messenger.js ... listen() { this.bot.on(\u0026#39;text\u0026#39;, this.handleText.bind(this)); this.botWrapper = new FixMyStuffBot(this.bot); return Promise.resolve(); } handleText(msg) { const message = Message.from(msg); handlerRouter.getCommandHandler(message) .handle(message, this.botWrapper) .catch(err =\u0026gt; { log.error(\u0026#39;Messenger\u0026#39;, `Got error from command handler: ${err}`); }); } ... Here we added import for our fixMyStuffBot.js, created wrapper and passed it to handleText method of our command handler.\n“Fix” Command (attempt #2) Finally, it\u0026rsquo;s time to get to really useful command – Fix Request. First, we should create model, which we will store in our MongoDB:\n//src/services/fixRequest.model.js \u0026#39;use strict\u0026#39;; import mongoose from \u0026#39;mongoose\u0026#39;; const FixRequestSchema = new mongoose.Schema({ userId: String, request: String }); export default mongoose.model(\u0026#39;FixRequest\u0026#39;, FixRequestSchema); It has 2 fields – userId and request, both are strings. Code of the command handler:\n//src/command/fixRequest.js \u0026#39;use strict\u0026#39;; import log from \u0026#39;npmlog\u0026#39;; import FixRequest from \u0026#39;../services/fixRequest/fixRequest.model\u0026#39; export default class FixRequestHandler { handle(message, bot) { const fixRequest = new FixRequest({ \u0026#39;userId\u0026#39;: message.from, \u0026#39;request\u0026#39;: message.option }); return fixRequest.save() .then(() =\u0026gt; { log.verbose(\u0026#39;FixRequest\u0026#39;, `Saved fix request ${fixRequest}`); return bot.sendMessage(message.from, \u0026#39;Your request has been submitted. We will contact you soon.\u0026#39;); }) .catch(err =\u0026gt; { log.error(\u0026#39;FixRequest\u0026#39;, `Was not able to save request ${fixRequest}, got error: ${err}`); return bot.sendMessage(message.from, \u0026#39;Sorry we do not have any commands yet.\u0026#39;); }); } } Here we create model, with userId from the message and with request description. Then we save this model, and if promise from mongoose resolves (so it\u0026rsquo;s saved) – we send message back to the user. If error occurs – we log this, and send error message to the user. And yes, we return promise from handle method, in case we need to do some extra steps outside of the command. After implementing command, we need to add it to our handler router src/command/index.js:\nnew import import FixRequest from \u0026lsquo;./fixRequest\u0026rsquo;;\nconstructor\nconstructor() { this.handlers = { \u0026#39;help\u0026#39;: new Help(), \u0026#39;fix\u0026#39;: new FixRequest() } } Also, we should update our help command:\n//src/command/help.js \u0026#39;use strict\u0026#39;; export default class Help { handle(message, bot) { return bot.sendMessage(message.from, \u0026#39;Type /fix your_address_and_problem_description to submit request (e.g. /fix NY. TV got broken)\u0026#39;); } } Let’s run our application and test it:\nAnd submit new request:\nAdditionally, we can check if request has been saved in our DB:\nSo everything looks good so far.\n“My requests” command After user submitted request, he, probably, will wanna to check it\u0026rsquo;s status. Let’s give him this feature. First, we should adapt our model.\n//src/services/fixRequest/fixRequest.model.js \u0026#39;use strict\u0026#39;; import mongoose from \u0026#39;mongoose\u0026#39;; const FixRequestSchema = new mongoose.Schema({ userId: String, request: String, status: { type: String, enum : [\u0026#39;NEW\u0026#39;,\u0026#39;ASSIGNED\u0026#39;, \u0026#39;IN_PROGRESS\u0026#39;, \u0026#39;RESOLVED\u0026#39;, \u0026#39;DECLINED\u0026#39;], default : \u0026#39;NEW\u0026#39; } }); export default mongoose.model(\u0026#39;FixRequest\u0026#39;, FixRequestSchema); Here we added status field, with enum type. Actually, it\u0026rsquo;s String type, but we provide the array of possible values, so it behaves like enum. Default value would be NEW.\nNow go ahead to the command:\n//src/command/myRequests.js \u0026#39;use strict\u0026#39;; import log from \u0026#39;npmlog\u0026#39;; import FixRequest from \u0026#39;../services/fixRequest/fixRequest.model\u0026#39; const MESSAGE_OPTIONS = { parse_mode: \u0026#39;Markdown\u0026#39; }; function formatRequests(requests) { let message = \u0026#39;Your requests:\\n\u0026#39;; requests.forEach(userRequest =\u0026gt; { message += `*Request*: ${userRequest.request}\\n` + `*Status*: ${userRequest.status}\\n\\n`; }); return message; } export default class MyRequests { handle(message, bot) { const userId = message.from; return FixRequest.find({\u0026#39;userId\u0026#39;: userId}) .then((result) =\u0026gt; { log.verbose(\u0026#39;MyRequests\u0026#39;, `For user ${userId} found next requests: ${JSON.stringify(result)}`); let message = \u0026#39;\u0026#39;; if (result \u0026amp;\u0026amp; result.length \u0026gt; 0) { message = formatRequests(result); } else { message = \u0026#39;You have no requests yet.\u0026#39;; } return bot.sendMessage(userId, message, MESSAGE_OPTIONS); }) .catch(err =\u0026gt; { log.error(\u0026#39;MyRequests\u0026#39;, `Was not able to find requests for user ${userId}, got error: ${err}`); return bot.sendMessage(userId, \u0026#39;Sorry we were not able to find your requests. Try a bit later\u0026#39;); }); } } Let’s go through handle method:\nWe search for all requests from user Then if there are any requests, we format them. Otherwise – we tell user, that we found no requests. We send message back to the user. In case of error, as usually, we log it and tell user to try later. Method formatRequests is quite simple: we add each user request and it\u0026rsquo;s status to the message body with several line separators.\nFinally, we will test this handler.\nWe got our requests back (output, obviously, depends on submitted requests). That is what we expected. Command seems to be finished now. The last thing would be to update help command.\nConclusion At this point, we\u0026rsquo;ve learned some basics of chatbots. Why they are good and how to write a simple bot. For now, we have only 2 commands. Standalone they are not really useful, but we will fix this in our next topic.\nYou can find the complete code in this GitHub repo.\nP.S.: will be happy to get questions from you and see you soon.\nP.P.S.: after this article has been finished and next article was started I came across another library for creating Telegram Bot with Node.js. It has pretty nice OOB routing and some other features. So you should give it a try as well.\n","permalink":"https://taradevko.com/2017/01-02-write-stateful-bot-telegram-part-1/","summary":"\u003cp\u003eWhy do we need all these bots? Why people are talking about them so much? What the bot is good for? Look around, it\u0026rsquo;s 2017 outside and nobody uses phones for calls only. Even more: today a lot of people prefer chatting to calling somebody.\u003c/p\u003e\n\u003cp\u003eBut, you are still forced to make calls to get a taxi or to understand why your LAN connection is down (oh man, you again forgot to pay for it!). How often are you listening to the music, when you are on hold, instead of doing real business? That\u0026rsquo;s fine when it\u0026rsquo;s Bach, but what if it\u0026rsquo;s Justin Bieber instead? They are just wasting your time!\u003c/p\u003e\n\u003cp\u003eChat is a better medium for a lot of people. But, yeah, you still will wait until there is a free person to respond you. Ok, finally somebody responded to you, but what if that person is in bad mood? So it still looks too unreliable and complicated. That is where bots come into play.\u003c/p\u003e","title":"Bot Dev: Write a stateful Telegram bot - Part 1"}]