Recently we added some new Calendar Event Form components.  The Author's required the ability to select a TimeZone for an event, and also and event "reminder".  Authoring these components only, don't give you the full implementation needed to have the additional content added to the ".ics" file that is generated when clicking on the "Download event as ICS file" link.

This required something to be done on the back end.  My good old buddy recent requests helped me out again to identify what is fielding this request. Well, I looked at the ical stuff, and it's an embedded dependency within the bundle implementinmg the ICalExportServlet.  Thus, the ical API is not exposed to the OSGi container, and I am no fan of adding duplicate dependencies to a container.

Besides, the Event Edit form already handles the TimeZone field for me, with a little help of some additional JavaScript in my component.  It adjusts the TimeZone information for me on the date fields.  The only thing it doesn't handle is the reminder.  The ical format supports event reminders, but the OOTB Servlet is not coded to recognize any reminder content.

I briefly entertained the idea of extending the OOTB servlet.  Took it out to dinner, walked along the beach with it, and then I had to run away screaming into the night.  I was looking at a bunch of work for what amounted to the following lines:

BEGIN:VALARM
TRIGGER: -P1D
ACTION:DISPLAY
END:VALARM

The ical format supports much much more than this, but the extra supported info was not being requested by the Author's, so.... creating a Servlet to somehow reuse the OOTB ICalExportServlet looked like a huge fat mess.  Rewriting the ICalExportServlet didn't look much appealing either.  Yeah, no way.  Not gonna happen.

In general I prefer to attempt reuse what already exists, and add layers of functionality on top of the OOTB stuff.  I do this for bunches of reasons.  First, upgrade path.  If I extend it, will it continue to work in a new version of CQ?  What if the fucntionality I have extended gets deprecated or removed?  What if dependencies change? I have to have a non-sperficial understanding of what it is doing, and why, and, and, and....

IF I "wrap" CQ's OOTB functionality with my own, the affords me many conveniences.  First, upgrade path is really no longer an issue.  For the kind of change I will talk about in a moment, if the OOTB functionality is deprecated or removed, I simply remove the OSGi metadata, and my service no longer exists.  No intrusive chages.

Second, I no longer have to care about how the OOTB stuff does it's work, all I care about is what it produces.  This is way easy to do.  Click on the ical link and viola, I have what it produces.

Now, I am NOT saying that a Servlet Filter is a hammer, and every problem is a nail.  I think there are good usages of this kind of pattern, and I believe this to be the right kind of problem for it.  When I started working on this problem, I had been using the Servlet approach, with a ServiceTracker and ServiceTrackerCustomizer to watch the OOTB ICalExportServlet, blah, blah, blah.  I got to the point where I quickly realized that this solution was WAY overcomplicated for that 4 lines of text.

So, I define a Servlet Filter with:
@SlingFilter(

generateComponent = true,
generateService = true,
metatype = true,
name = "com.kelleher.cq5.foundation.servlet.filter.ICalendarServletFilter",
label = "extend the ICalExportServlet produced markup",
description = "For requests with a .ics extension, inject additional Calendar matadata into the Response",
order = -2000,
scope = SlingFilterScope.REQUEST

)

public class ICalendarServletFilter implements Filter {
' code here ...
}

To narrow things a bit more for this FIlter, I check:

  1. request method (GET)
  2. request extension (ics)
  3. content path (/content/mypath/.*) for the current request.

If one of these don't match, the filter simply invokes 'doFilter' and it's work is done.  It steps out of the requests way.

However, if they ALL match, I wrap the SlingHttpServletResponse with a custom SlingHttpResponseWrapper.  It uses an internal ByteArrayOutputStream to capture the response.  When the OOTB ICalExportServlet is done producing it's response, my filter does it's work.

The filter retrieves the reminder content from the repository, and produces the ICAL fragment show above.  I then use a regex to find "END:VEVENT" and insert my String right before it.  I then write the new String out to the real SlingHttpServletResponse.  If you ignore the 28 lines of import statements, and the next 60 lines defining the class and the OSGi metadata so the FIlter can be configured on the fly, the 20 lines for the OSGi lifecycle methods, the implementation is less than 127 lines.  I didnt bother counting comments, but you get the idea.  Pretty super small.

One more comment about how this was implemented.  Given that I know other developers have created Event Form components, and that the Author's will eventually want to see this content within the ical file, I extracted the transformation functionality into an Interface.  This allowed me to standardize the transformation and injection process.

Basically: produce an ical fragment, use a regex to search for text, insert the ical fragment somewhere near the regex.  I cheated and used backreferences, and embedded regex flags in my regex, so I dont need to produce a java.util.regex.Pattern instance nor its cousin the Matcher.  This allowed me to use "replaceFirst" on the source String, which alleviated me from managing the search and replace functionality.

From here, if this gets too complex, and they extend it much further than 3 or so more fields, I would likely go back and handle it from a Servlet perspective.  I would embed the Sourceforge ICal Java library, and produce my own ical file, as a complete replacement for the ICalExportServlet.  However, for now at only 1 additional field, potentially 2 more fields, I consider rewriting the Servlet overkill, and a waste of my time.

Filter on!

Enjoy!