Sans Pareil Technologies, Inc.

Key To Your Business

User Generated Content


This page describes a simple technique for pushing content generated from the Magnolia public instances back to the author instance without a clustered repository/workspace. At the University of Chicago Press, the MySQL server that was used for the central database is less reliable than the servers on which the web containers are run. Magnolia (and possibly JackRabbit) does not function at all if the central database goes down (the entire application is unusable, and not just the pages depending upon the clustered workspace). This unreliability lead us to develop a simple mechanism for synchronising user generated content from the public instances back to the author instance, from where the same content could be modified and then pushed back to the registered public instances using the regular Magnolia activation mechanism.

The basic process is to create (or edit) the node on the public instance local workspace, export the node data and post the data back to a synch service Servlet running on the author instance. This servlet imports the node data into the author instance local workspace. Content editors can then use the appropriate dialogs and admin central features to edit/moderate the user generated content and then push it back to the public instances using Magnolia activation mechanism.

Public Instance

The user generated content is processed by a POST handler Servlet. This servlet performs the business logic on the content and as an additional step exports the newly created node and POST's it back to the listening Servlet on the author instance. The method below is used by the POST handler to send the newly created node back to the author instance.

  private void export( final Node node )
  {
    try
    {
      def xml = textAdoptionDAO.getXml node

      def props = new Properties()
      props.setProperty 'path', node.parent.path
      props.setProperty 'name', node.name
      props.setProperty 'content', xml

      def url = "http://${serverConfiguration.config.urls.cms}/books/textadoption/taSyncService"
      def result = com.sptci.util.StringUtilities.fromUrl( url, props ).trim()

      if ( 'true' == result )
      {
        logger.logp INFO, getClass().name, 'export',
            "Submitted text adoption request with path ${node.path} to author instance at url: ${url}"

        node.setProperty EXPORTED, true
        node.session.save()
      }
      else
      {
        logger.logp WARNING, getClass().name, 'export',
            "Error submitting text adoption request with path ${node.path} to author instance at url: ${url}.  Service returned: ${result}"
      }
    }
    catch ( Throwable t )
    {
      logger.logp WARNING, getClass().name, 'export',
          "Error exporting text adoption node (${node}) to admin instance", t
    }
  }


We set an exported property in the node to indicate that the node data has been successfully exported back to the author instance. This helps another background script that is run via the Magnolia scheduler module identify any nodes that could not be synchronised (due to author instance being down at that time for instance), and resubmit them to the author instance.

Note:
We could also have used the JCR observation feature to decouple this data sync process from the POST handler, however this solution is simpler (we need to distinguish between new user generated data and activation data coming from the author instance).

Due to complexities with managing background sessions, we chose to perform the export step in the main handling thread and not in a background task.

Author Instance

The synchronisation request handler servlet imports the node data sent by the public instance into the appropriate node hierarchy. Once the node is saved, content editors may edit/moderate the user generated content using the normal Magnolia content management features and then activate the modified node back to the public instances. The relevant code from the servlet class that we use to receive the POST requests from the public instances is as shown.


  private void process( final HttpServletRequest req )
  {
    def path = req.getParameter 'path'
    def name = req.getParameter 'name'
    def xml = req.getParameter 'content'

    def session = sessionFactory.clusteredSession
    new NodeCreator().createNodes path, session

    def is = new ByteArrayInputStream( xml.getBytes( 'UTF-8' ) )
    session.importXML path, is, IMPORT_UUID_COLLISION_REMOVE_EXISTING

    def node = session.getItem "${path}/${name}"
    node.setProperty EXPORTED, true
    textAdoptionDAO.save node
    session.save()
    logger.logp INFO, getClass().name, 'process', "Imported text adoption node: ${node.path}"
  }


  private static class NodeCreator extends TextAdoptionDAO
  {
    void createNodes( final String path, final Session session )
    {
      def node = session.rootNode

      for ( name in path.split( '/' ) )
      {
        if ( ! isEmpty( name ) )
        {
          node = getNode node, name
        }
      }
    }
  }