Posts tagged "integration"

May 22, 2017

AEM – Circuit Breaker Innovation via a Hystrix Integration

From time to time the Adobe Partner Experience (APx) team has the privilege to check out some truly innovative stuff. Yogesh is a great guy and offered to show us a cool integration pattern that he has been working on. We liked it so much that we decided to let him share it with the world via the Content Management blog. – Darin Kuntze @DarinKuntze

Yogesh Kulkarni is an experienced Adobe AEM/CQ developer/architect specializing in best practices for designing connected digital experience using AEM technology stack.

He is currently working for AKQA (a digital agency) as a Senior Software Engineer (Adobe AEM/CQ).

AEM 6.1 SP1 – Hystrix Integration

We had a requirement where the client wanted to add the Circuit Breaker pattern to an AEM component which calls RESTful endpoints, to support the following use cases:

  • If more than 10% of the calls fail within a minute then the circuit should trip.
  • After the circuit is tripped, the system should periodically check if the external service API is back up and working using a background thread.
  • The component prevents users from experiencing a sub-optimal response time.
  • The component should present a user-friendly message in case of any service failure.

Circuit Breaker Pattern

In our case, the component is responsible for making a call to RESTful endpoints to register/log in the user and provide an option to update related data/content after login.

The Circuit Breaker pattern can handle remote resources and service call failures more gracefully. It can prevent an application from repeatedly trying to execute an operation that’s likely to fail, allowing it to continue without waiting for the fault to be fixed or wasting CPU cycles while it determines that the fault is long lasting. The Circuit Breaker pattern also enables an application to detect whether the fault has been resolved. If the problem appears to have been fixed, the application can try to invoke the operation once again.

Hystrix  (a Netflix library) has a built-in ready-to-use Circuit Breaker. When we apply a Circuit Breaker to a method, Hystrix watches for failing calls to that method, and if failures build up to a pre-defined threshold, Hystrix opens the circuit so that subsequent calls automatically fail. While the circuit is open, Hystrix redirects calls to the method, and they’re passed on to a specified fallback method.

Reference: https://docs.pivotal.io/spring-cloud-services/1-3/common/circuit-breaker/

OSGi Dependencies

In order to get Hystrix running in AEM, you need to install the following dependency bundles in Felix.

Artifact ID(s) Version
1 org.apache.servicemix.bundles.hystrix 1.5.9_1
2 org.apache.servicemix.bundles.hystrix-event-stream 1.5.9_1
3 rxjava 1.2.9
4 org.apache.servicemix.bundles.commons-configuration 1.9_2
5 com.diffplug.osgi.extension.sun.misc 0.0.0
6 HdrHistogram 2.1.9

 

Apply the Circuit Breaker Pattern

Netflix Hystrix looks for any method annotated with the @HystrixCommand annotation and wraps that method in a proxy connected to a Circuit Breaker so that Hystrix can monitor it.

The code to be isolated is wrapped inside the run() method of a HystrixCommand similar to the following:

import com.netflix.hystrix.HystrixCommand;

 

public class HelloServiceGetCommand extends HystrixCommand<HelloResult> {
 


private final HttpGet httpGet;


    public HelloServiceGetCommand(final HttpGet httpGet) {


     super(HystrixCommandGroupKey.Factory.asKey("HelloGroup"));
 
     LOG.debug("Is CC breaker open " + isCircuitBreakerOpen());
      this.httpGet = httpGet;
    }
 
 
    @Override
    protected HelloResult run() throws IOException {

       //your logic goes here
        CloseableHttpClient httpClient = HttpClientBuilder.create().build();


        LOG.debug("Health count : TotalRequests " + metrics.getHealthCounts().getTotalRequests());        

               //call httpClient.execute

 

               //catch any error and populate HelloResult object

               return helloResult;

 

}

Fallback

To handle the failure of external services, Hystrix has built in the following defaults:

  1. Timeout for every request to an external system (default: 1000 ms)
  2. Limit of concurrent requests for external system (default: 10)
  3. The Circuit Breaker to avoid further requests (default: when more than 50% of all requests fail)
  4. Retry of a single request after the Circuit Breaker has triggered (default: every 5 seconds)
  5. Interfaces to retrieve runtime information at the request and aggregate level (there’s even a ready-to-use realtime dashboard for it) * Yet to be defined in OSGi.
    How-To-Use#Fallback

Simple Fallback method using Fallback: Stubbed pattern:

@Override
protected HelloResult getFallback() {
 


         LOG.debug("FALLBACK : is CC breaker open {} isResponseTimedOut() {}             isResponseTimedOut() {}",  isCircuitBreakerOpen(), isResponseTimedOut(),isResponseThreadPoolRejected());

LOG.debug("Health count : TotalRequests {} ErrorPercentage {} ErrorCount {}", metrics.getHealthCounts().getTotalRequests()
            , metrics.getHealthCounts().getErrorPercentage()
,metrics.getHealthCounts().getErrorCount());

 

// returns error object to service to send it to FE


return getHelloResultError();
}

The fallback method returns the error code which is then consumed by a UI component.

How to Run Hystrix Command

There are many ways to run the command. Following simple call is triggered from HelloServiceImpl to invoke Hystrix command.

public class HelloServiceImpl implements HelloService {

 

  private callCommand(){

    new HelloServiceGetCommand(getRequest).execute();

   //other service logic goes here

 }

}

Hystrix Runtime Configuration

Configuring a Hystrix command details can be found here: Hystrix Configuration. It is simple to update the configuration.

For example, the default value for circuitBreaker.requestVolumeThreshold is set to 20. We override the property using HystrixCommandProperties.Setter, as shown below.

public HelloServiceGetCommand(final HttpGet httpGet) {
    super(Setter
            .withGroupKey(HystrixCommandGroupKey.Factory.asKey("HelloGroup "))
            .andCommandPropertiesDefaults(
                    HystrixCommandProperties.Setter()
                       .withCircuitBreakerRequestVolumeThreshold(MyAudiConstants.CB_REQUEST_VOLUME_THRESHOLD)
                            .withCircuitBreakerErrorThresholdPercentage(MyAudiConstants.
                                    CB_REQUEST_ERROR_THRESHOLD)).
                    andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter().
                            withCoreSize(MyAudiConstants.CB_REQUEST_THREAD_POOL_SIZE)).
                    andCommandKey(HystrixCommandKey.Factory.asKey("HelloGroup ")));

 

…

}

Monitoring

A dashboard for monitoring applications using Hystrix is available in the hystrix-dashboard module. However, hystrix-dashboard has not been deployed to our AEM instance at this time.

1) Circuit Breaker is close at the start

DEBUG [hystrix-HelloGroup2] com.akqa…services.commands.HelloServiceGetCommand CC breaker open false

All HelloCommand requests are going through.

2) Now FAILURE occurs

DEBUG [hystrix-HelloGroup-2] com.akqa…services.commands.HelloServiceGetCommand CC breaker open True Events[SHORT_CIRCUITED]

3) Lastly, CB is closed once host is back online.

DEBUG [hystrix-HelloGroup-2] com….services.commands.HelloServiceGetCommand CC breaker open false

The example above just scratches the surface of how to improve the Service Resilience in a Felix container using Hystrix. The following resources can provide more advanced tricks to help make your application more fault tolerant.

https://github.com/Netflix/Hystrix/wiki/How-To-Use#Fallback

https://github.com/Netflix/Hystrix/wiki/How-To-Use#Common-Patterns

https://github.com/Netflix/Hystrix/wiki/Configuration

https://github.com/Netflix/Hystrix/wiki/Metrics-and-Monitoring

Summary

As demonstrated, it is possible to use the state-of–the-art, industry standard fault-tolerance library Hystrix in AEM to protect your service against cascading failures and to provide fallback behavior for potentially failing calls.

 

All opinions expressed by Yogesh Kulkarni and are his own and not Adobe’s.

 

11:14 AM Permalink
August 19, 2012

How to integrate with other web services without creating OSGi packages

A lot of enterprise grade systems require some sort of integration with other services these days, to extend and bring in more features into the systems. In this blog post I would like to show a quick and easy way to integrate CQ5 with other web services. Examples below will be showing a RESTful webservice but other webservices like SOAP will take similar approach.

Webservices integration usually involves building a integration layer. A Java Servlet would definitely be a good candidate. But to avoid building / compiling / packaging / deploying such code into an OSGi container, one can easily write a JSP (on the fly) in CQ to handle the integration.

Create an integration layer page:

This integration page is basically to create an instance of a CQ Page so that the rest of the content pages can communicate to it (Things under /app are not exposed or cannot be called directly). And extra benefit about this CQ Page -> You can control who has access to it!

Note the above page is using “Demo – API Page Template”, and here is the mapping:

 

And apipage.jsp is nothing more than what you want to surface in a typical Servlet.  Here is snippet of the code:

if (request.getParameter("action") != null) {
    if (request.getParameter("action").equals("getSomething")) {
        Node demoNode = resourceResolver.getResource(INTEGRATION_DESIGN_NODEPATH).adaptTo(Node.class);
        response.setContentType("application/json");
        out.write(getSomething(demoNode));
 
    } else if (request.getParameter("action").equals("getCategories")) {
        Node demoNode = resourceResolver.getResource(INTEGRATION_DESIGN_NODEPATH).adaptTo(Node.class);
        response.setContentType("application/json");
        out.write(getCategories(demoNode));
 
    } else if (request.getParameter("action").equals("getFields") &amp;&amp;
            request.getParameter("param1") != null &amp;&amp;
            request.getParameter("param2") != null) {
        response.setContentType("application/json");
        out.write(getFields(
            request.getParameter("param1"),
            request.getParameter("param2"));
 
    } else {
        response.setContentType("application/json");
        out.write("{\"error\":\"error\"}");
    }
}

 

And derived from the above, we now have the following links to retrieve data:

http://_server_:_port_/content/integrationdemo/en/integration/integrationapi?action=getSomething

http://_server_:_port_/content/integrationdemo/en/integration/integrationapi?action=getCategories

http://_server_:_port_/content/integrationdemo/en/integration/integrationapi?action=getFields&param1=test&param2=data

 

The integration implementation will be per your requirements, but at a high level you can pretty much do everything-in-a-servlet inside a JSP. JSPs are Servlets after all. Here is a snippet of a http call (to a webservice) inside the same JSP:

 

static String getFields(String apiKey, String accessToken, String someJson) {
    HttpClient httpClient = new HttpClient();
    HttpClientParams httpClientParams = new HttpClientParams();
    DefaultHttpMethodRetryHandler defaultHttpMethodRetryHandler = new DefaultHttpMethodRetryHandler(0, false);
    httpClientParams.setParameter(HttpClientParams.RETRY_HANDLER, defaultHttpMethodRetryHandler);
    httpClient.setParams(httpClientParams);
 
    PostMethod post = new PostMethod(EXTERNAL_WEBSERVICE_API_URL+apiKey.trim()+"/URL_EXAMPLE/test?oauth_token="+accessToken.trim());
    post.addRequestHeader("Accept", "application/json");
    post.addRequestHeader("Content-Type", "application/json");
    post.setRequestBody(someJson);
 
    try {
        httpClient.executeMethod(post);
        String result = post.getResponseBodyAsString();
 
        if (result != null &amp;&amp; !result.equals("")) {
            JSONObject returnObj = new JSONObject(result);
            return returnObj.toString();
        } else {
            return "";
        }
    } catch (Exception ex) {
        return ex.getMessage();
    }   
}

 

And finally, as an option (if you want to integration the CQ authoring experience with an external webservice), you can expose the data retrieved inside a CQ Component dialog field, you will need to configure the widget to retrieve webservice data. Details are documented in:

http://dev.day.com/docs/en/cq/current/widgets-api/index.html?class=CQ.form.Selection

Here’s a quick example:

And the result:

 

Enjoy~

11:21 AM Permalink
February 25, 2012

How to integrate CQ5 with Defensio Profanity detection

I have recently worked on a project where Defensio integration is needed to detect profanity in CQ5’s commenting system and discussion forums. Adobe CQ5 comes with OOTB integration with Automattic Kismet (Akismet) which is for Spam filtering but Akismet does not offer profanity filtering. For more information on Adobe CQ5’s integration with Akismet, please look at the following links:

Social Collaboration – Setting an Akismet Key

AkismetService API doc

Defensio is a profanity and spam filtering service on user generated content. It offers a fully RESTful API to automatically detect, reject, and mask profanity from user generated content on a website. To learn more about Defensio, please visit:

Defensio Web Service – Powered by WebSense

Defensio API Specification

 

Now let’s talk about the high level requirements of the integration I have recently worked on, before I go into the technical details.

Requirements:

  • Profanity detection should be enabled for any comments or discussion forum replies (user generated content).
  • If obscene words are found, they should be masked and the comment / forum reply should be marked as spam. The spam message should not be displayed unless approved by a soco (social collaboration) admin user.
  • No profanity messages should be displayed publicly. If it should be displayed (approved by admin), the masked version is used.
  • The Defensio Web Service API URL and also the API key should be configurable.

Technical Details:

  • A workflow process (com.day.cq.workflow.exec.WorkflowProcess) should be created for the profanity check so that it can be enabled in any CQ workflows.
  • To enable profanity detection for comments or discussion forum replies, the workflow process can be inserted into the OOTB “Comment Moderation” workflow.
  • In order to allow system administrators to configure the Defensio Web Service URL and the API key, an OSGi bundle can be created with service component runtime properties like the following:

 

OSGi Bundle implementation:

This blog entry is not to discuss the very detail of how to package and deploy an OSGi Bundle into Apache Felix. The code snippets I’m showing below utilized the Apache Maven software project management tool, and the codes are greatly shorted (life is easy with Java Annotations) using the maven-bundle-plugin and maven-scr-plugin. To learn more about these plugins, here are some links:

Apache Felix Bundle Plugin for Maven

Apache Felix Maven SCR Plugin

The OSGi bundle for the Defensio integration consists of three classes:

com.customername.cq.profanity.DefensioService
com.customername.cq.profanity.DefensioServiceImpl
com.customername.cq.profanity.DefensioProcess

DefensioService is the interface for the Defensio Profanity integration,
DefensioServiceImpl is an implementation of the integration, and
DefensioProcess is a CQ workflow process implementation (makes it available as a process step in a CQ workflow).

So here you go, please note the use of annotations, and also note that some of logs and minor implementation details are skipped (marked by ellipses in comments):

com.customername.cq.profanity.DefensioService:
package com.customername.cq.profanity;
 
public interface DefensioService {
 
    /**
     * Check to see if the Defensio API key has been verified
     *
     * @return <code>true</code> if the API key has been verified, <code>false</code> otherwise
     */	
	public boolean isVerifiedKey();
 
    /**
     * Verify Defensio API key
     *
     * @return <code>true</code> if the API key has been verified, <code>false</code> otherwise
     */	
	public boolean verifyAPIKey();
 
	@SuppressWarnings("unchecked")
	public String profanityCheck(String commentContent);
}
com.customername.cq.profanity.DefensioServiceImpl:
package com.customername.cq.profanity;
 
import java.io.IOException;
import java.util.Dictionary;
 
import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.sling.commons.json.JSONException;
import org.apache.sling.commons.json.JSONObject;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.ComponentContext;
 
/**
 * Defensio Service implementation
 * <p></p>
 * <a href="http://http://www.defensio.com/api">Defensio API</a> documentation.
 *
 * @author Oliver Choy
 *
 * @scr.component label="Adobe CQ AntiProfanity"
 *                description="Defensio configuration"
 * @scr.property name="service.vendor" value="Customer Name"
 * @scr.property name="service.description" value="Defensio Profanity Check for blog comments"
 * @scr.service
 */
public class DefensioServiceImpl implements DefensioService {
 
    private Log logger = LogFactory.getLog(DefensioServiceImpl.class);
 
    // Constants
    private static final String USER_AGENT_HEADER = "User-Agent";
    private static final String USER_AGENT_VALUE = "Adobe CQ5";
    private static final String API_PARAMETER_COMMENT_CONTENT = "comment_content";
 
    private HttpClient httpClient;
    private String apiKey;
    private boolean verifiedKey = false;
    private String providerUrlFragment;
    
    /** @scr.property */
    public static final String PARAM_API_KEY = "defensio.service.api.key";
    /** @scr.property */
    public static final String PARAM_API_URL = "defensio.service.api.url";
 
    public DefensioServiceImpl() {
    }
 
    DefensioServiceImpl(BundleContext bundleContext, Dictionary<String, Object> configuration) {
        this.setup(bundleContext, configuration);
    }
 
    void setup(BundleContext bundeContext, Dictionary<String, Object> configuration) {
    	// get api key
        Object key = configuration.get(PARAM_API_KEY);
        if (key != null) {
            this.apiKey = key.toString();
        }
        // get api url
        Object apiUrl = configuration.get(PARAM_API_URL);
        if (apiUrl != null) {
            this.providerUrlFragment = apiUrl.toString();
        }
 
        // autoverify
        if(providerUrlFragment != null && apiKey != null) {
            if(providerUrlFragment.length() > 0 && apiKey.length() > 0) {
        	logger.info("Defensio API key verification result is " + verifyAPIKey());
            }
        }
    }
 
    void shutdown() {
    	this.apiKey = null;
    }
 
    @SuppressWarnings("unchecked")
    protected void activate(ComponentContext context) {
	httpClient = new HttpClient();
	HttpClientParams httpClientParams = new HttpClientParams();
	DefaultHttpMethodRetryHandler defaultHttpMethodRetryHandler = new DefaultHttpMethodRetryHandler(0, false);
	httpClientParams.setParameter(USER_AGENT_HEADER, USER_AGENT_VALUE);
	httpClientParams.setParameter(HttpClientParams.RETRY_HANDLER,
		defaultHttpMethodRetryHandler);
	httpClient.setParams(httpClientParams);
	setup(context.getBundleContext(), context.getProperties());
    }
 
    protected void deactivate(ComponentContext context) {
        this.shutdown();
    }
 
    public boolean verifyAPIKey() {
    	boolean callResult = false;
        String defensioUserURL = "http://" + providerUrlFragment + apiKey + ".json";
        GetMethod get = new GetMethod(defensioUserURL);
 
        try {
            httpClient.executeMethod(get);
            String result = get.getResponseBodyAsString();
            
            if (result != null && !result.equals("")) {
            	JSONObject respObj = new JSONObject(result);
            	JSONObject defensioResultObj = respObj.getJSONObject("defensio-result");
            	if (defensioResultObj != null) {
            	    String verifyStatus = defensioResultObj.getString("status");
                    if (verifyStatus.equals("success")) {
            		callResult = true;
            	    }
            	} 
            }
        } catch (IOException e) {
            logger.error("...");
        } catch (JSONException e) {
      	    logger.error("...");
	}
 
        verifiedKey = callResult;
        return callResult;
    }
 
    /**
     * Generic call to Defensio
     **/
    @SuppressWarnings("unchecked")
	private String defensioCall(String commentContent) {
    	
    	String filteredResult = "";
        String defensioProfanityURL = "http://" + providerUrlFragment + apiKey + "/profanity-filter.json";
 
        PostMethod post = new PostMethod(defensioProfanityURL);
        post.addRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
 
        if (commentContent != null) {
            post.addParameter(new NameValuePair(API_PARAMETER_COMMENT_CONTENT, commentContent));
        }
 
        try {
            httpClient.executeMethod(post);
            String result = post.getResponseBodyAsString();
 
            if (result != null && !result.equals("")) {
            	JSONObject respObj = new JSONObject(result);
            	JSONObject defensioResultObj = respObj.getJSONObject("defensio-result");
            	if (defensioResultObj != null) {
            	    String verifyStatus = defensioResultObj.getString("status");
            	    if (verifyStatus.equals("success")) {
            		JSONObject filteredObject = defensioResultObj.getJSONObject("filtered");
            		if (filteredObject != null) {
            		    filteredResult = filteredObject.getString(API_PARAMETER_COMMENT_CONTENT);
            		    logger.debug("Defensio filtered comment: " + filteredResult);
            		}
            	    }
            	} 
            }
        } catch (IOException e) {
            logger.error("...");
        } catch (JSONException e) {
       	    logger.error("...");
	}
        return filteredResult;
    }
 
    @SuppressWarnings("unchecked")
	public String profanityCheck(String commentContent) {
        return defensioCall(commentContent);
    }
    
    public boolean isVerifiedKey() {
        return verifiedKey;
    }
}
com.customername.cq.profanity.DefensioProcess:
package com.customername.cq.profanity;
 
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
 
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.jcr.resource.JcrResourceResolverFactory;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import com.day.cq.workflow.WorkflowException;
import com.day.cq.workflow.WorkflowSession;
import com.day.cq.workflow.exec.WorkItem;
import com.day.cq.workflow.exec.WorkflowData;
import com.day.cq.workflow.exec.WorkflowProcess;
import com.day.cq.workflow.metadata.MetaDataMap;
 
/**
 * This <code>JavaProcess</code> checks a comment or forum reply for profanity
 * 
 * @scr.component metatype="false"
 * @scr.service interface="com.day.cq.workflow.exec.WorkflowProcess"
 * @scr.property name="process.label" value="Defensio Profanity Check"
 */
public class DefensioProcess implements WorkflowProcess {
 
    private static final String COMMENT_RESOURCE_TYPE = "collab/commons/components/comments/comment";
    private static final String FORUM_POST_RESOURCE_TYPE = "collab/forum/components/post";
    private static final String FORUM_TOPIC_RESOURCE_TYPE = "collab/forum/components/topic"
    private static final String SLING_RESOURCE_TYPE = "sling:resourceType";
    private static final String TYPE_JCR_PATH = "JCR_PATH";
    private final Logger logger = LoggerFactory.getLogger(this.getClass().getName());
 
    /**
     * @scr.reference policy="static"
     */
    private DefensioService defensioService;
    /**
     * @scr.reference policy="static"
     */
    private JcrResourceResolverFactory jcrResourceResolverFactory;
 
    protected void activate(final ComponentContext context) {
        // check if the key is valid
        logger.info("Verifying key for Defensio check");
        boolean isValidKey = defensioService.isVerifiedKey();
        if (isValidKey) {
            logger.info("...");
        } else {
            logger.warn("...");
        }
    }
 
    /**
     * @see WorkflowProcess#execute(WorkItem, WorkflowSession, MetaDataMap)
     */
    public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap args) throws WorkflowException {
        final Session session = workflowSession.getSession();
        final WorkflowData data = workItem.getWorkflowData();
        String path = null;
        String type = data.getPayloadType();
        try {
            if (type.equals(TYPE_JCR_PATH) && data.getPayload() != null) {
                String payloadData = (String) data.getPayload();
                if (session.itemExists(payloadData)) {
                    path = payloadData;
                }
            }
 
            if (path != null) {
                final Node userGeneratedNode = (Node) session.getItem(path);
 
                if (userGeneratedNode.hasProperty(SLING_RESOURCE_TYPE) &&
                        (userGeneratedNode.getProperty(SLING_RESOURCE_TYPE).getString().equals(COMMENT_RESOURCE_TYPE) ||
                        	userGeneratedNode.getProperty(SLING_RESOURCE_TYPE).getString().equals(FORUM_POST_RESOURCE_TYPE) || 
                                userGeneratedNode.getProperty(SLING_RESOURCE_TYPE).getString().equals(FORUM_TOPIC_RESOURCE_TYPE))
                ) {
 
                    logger.debug("Profanity check for comment node at " + userGeneratedNode.getPath());
                    String ipAddress = null;
                    if (userGeneratedNode.hasProperty("ip")) {
                        ipAddress = userGeneratedNode.getProperty("ip").getString();
                    } else {
                        setIsSpam(userGeneratedNode, false, session);
                    }
                    // if the ip is localhost we set isspam to false as well
                    if (ipAddress.equalsIgnoreCase("127.0.0.1") || ipAddress.equalsIgnoreCase("localhost")
                            || ipAddress.equalsIgnoreCase("0:0:0:0:0:0:0:1")) {
                        setIsSpam(userGeneratedNode, false, session);
                    }
 
                    // UserAgent spam check ...
                    // referrer spam check ...
                    // Author spam check ...
                    // author email spam check ...
                    // author URL spam check ...
                    
                    String commentText = null;
                    if (userGeneratedNode.hasProperty("jcr:description")) {
                        commentText = userGeneratedNode.getProperty("jcr:description").getString();
                    }
 
                    String filteredText = defensioService.profanityCheck(commentText);
                    if (!filteredText.equals(commentText)) {
                    	setIsSpam(userGeneratedNode, true, session);
                    	setFilteredText(userGeneratedNode, filteredText, session);
                    }
                    logger.info("Filtered result " + filteredText + " for comment: " + workItem.toString());
 
                } else {
                    logger.warn("Cannot check for profanity because item is not a comment/topic/post. Workitem: "
                            + workItem.toString());
                }
            } else {
                logger.warn("Cannot check for spam because path is null for this workitem: " + workItem.toString());
            }
        } catch (RepositoryException e) {
            throw new WorkflowException(e);
        }
    }
 
    private void setFilteredText(Node commentNode, String value, Session session) throws WorkflowException {
        try {
            commentNode.setProperty("jcr:description", value);
            session.save();
        } catch (Exception e) {
            throw new WorkflowException(e);
        }
    }
    
    private void setIsSpam(Node commentNode, boolean value, Session session) throws WorkflowException {
        try {
            commentNode.setProperty("isSpam", value);
            session.save();
        } catch (Exception e) {
            throw new WorkflowException(e);
        }
    }
}

Workflow Configuration:

Once the OSGi bundle is implemented and installed, a Process Step can be added to the CQ comment moderation workflow to trigger the profanity check.

Note that the Process Step, once dragged into the workflow, will need to be configured to use the OSGi bundle we have installed. In the example above, the Process Step is named as “Check Profanity” and the process chosen is “Defensio Profanity Check”, like the following:

 

Profanity Detection Result:

Once all of the above are implemented, installed, and configured, you can test the comment entry and discussion forum reply in CQ. It should yield something similar to the following:

The above image depicts two commenting scenarios (in CQ author environment): The first entry without profanity was successfully inserted into the system; the second comment was masked due to discovery of profanity, and was marked as spam waiting for approval/denial of a social collaboration administrator.

1:20 PM Permalink