J2EE developer with over 7 years of experience in designing and implementing enterprise j2ee solutions based on open source technologies like Tapestry, Hibernate, Spring. Current interests include Tapestry, Plastic, Spock, Scala. Taha is a DZone MVB and is not an employee of DZone and has posted 40 posts at DZone. You can read more from them at their website. View Full User Profile

Tapestry JFreeChart integration

08.02.2011
| 3123 views |
  • submit to reddit

I just finished integrating JFreeChart with Tapestry. Each time you integrate a library with Tapestry you are full of praise for the framework. This is something you can seldom say about other web frameworks.

I had two usages in mind.

  1. As a return value from an event handler.
  2. As a component which can be used to display JFreeChart and a corresponding imagemap


To display a chart only an instance of JFreeChart is not enough, you need to know the width and height of the image too. Also depending upon the type of image format you are going to use, additional information is required e.g. in case of JPEG format quality.
So we create an abstract data model to hold the information at one place.

public abstract class ChartModel
{
   private JFreeChart chart;

   private int width;

   private int height;

   private ChartRenderingInfo info;

   public ChartModel(JFreeChart chart, int width, int height)
   {
      this.chart = chart;
      this.width = width;
      this.height = height;
      info = new ChartRenderingInfo(new StandardEntityCollection());
   }

   public JFreeChart getChart()
   {
      return chart;
   }

   public int getWidth()
   {
      return width;
   }

   public int getHeight()
   {
      return height;
   }

   public ChartRenderingInfo getInfo()
   {
      return info;
   }

   public abstract String getFormat();

   public abstract String getContentType();

}

Then we create a concrete class each for a specific image format.

public class JPEGChartModel extends ChartModel
{

   private float quality;

   public JPEGChartModel(JFreeChart chart, int width, int height, float quality)
   {
      super(chart, width, height);
      this.setQuality(quality);
   }

   @Override
   public String getFormat()
   {
      return "jpeg";
   }

   @Override
   public String getContentType()
   {
      return "image/jpeg";
   }

   public void setQuality(float quality)
   {
      this.quality = quality;
   }

   public float getQuality()
   {
      return quality;
   }

}

public class PNGChartModel extends ChartModel
{
   private boolean encodeAlpha;

   private int compression;

   public PNGChartModel(JFreeChart chart, int width, int height,
      boolean encodeAlpha, int compression)
   {
      super(chart, width, height);
      this.setEncodeAlpha(encodeAlpha);
      this.setCompression(compression);
   }

   @Override
   public String getFormat()
   {
      return "png";
   }

   @Override
   public String getContentType()
   {
      return "image/png";
   }

   public void setEncodeAlpha(boolean encodeAlpha)
   {
      this.encodeAlpha = encodeAlpha;
   }

   public boolean isEncodeAlpha()
   {
      return encodeAlpha;
   }

   public void setCompression(int compression)
   {
      this.compression = compression;
   }

   public int getCompression()
   {
      return compression;
   }

}

The image is written to output stream using a rendering service, ChartWriter.

public interface ChartWriter {

   public void writeChart(OutputStream out, ChartModel chartModel) throws IOException;

}

public class ChartWriterImpl implements ChartWriter
{

   private ChartRenderer renderer;

   public ChartWriterImpl(ChartRenderer renderer)
   {
      this.renderer = renderer;
   }

   public void writeChart(OutputStream out, ChartModel chartModel) throws IOException
   {
      renderer.render(chartModel, chartModel.getChart(), out);
   }

}

The implementation delegates the rendering to the ChartRenderer service which is a Strategy Service based on the class type

public interface ChartRenderer
{
   void render(ChartModel chartModel, JFreeChart jfreeChart, OutputStream out) throws IOException;
}

//JPEG Chart Renderer
public class JPEGChartRenderer implements ChartRenderer
{

   public void render(ChartModel chartModel, JFreeChart jfreeChart, OutputStream out) throws IOException
   {
      JPEGChartModel jpegChart = (JPEGChartModel) chartModel;

      ChartUtilities.writeChartAsJPEG(out,
            jpegChart.getQuality(),
            jfreeChart,
            chartModel.getWidth(),
            chartModel.getHeight(),
            jpegChart.getInfo());

   }

}

//PNG Chart Renderer
public class PNGChartRenderer implements ChartRenderer
{

   public void render(ChartModel chartModel,
      JFreeChart jfreeChart, OutputStream out) throws IOException
   {
      PNGChartModel pngChart = (PNGChartModel) chartModel;

      ChartUtilities.writeChartAsPNG(out,
            jfreeChart,
            chartModel.getWidth(),
            chartModel.getHeight(),
            pngChart.getInfo(),
            pngChart.isEncodeAlpha(),
            pngChart.getCompression()
            );

   }

}

Now that the bases are covered, let us move on to the two usages we had planned.

As a return value from an event handler.

This can be done by contributing a ComponentEventResultProcessor to handle ChartModel as a return value.

public class ChartResultProcessor implements ComponentEventResultProcessor<ChartModel>
{
   public Response response;
   private ChartWriter chartWriter;

   public ChartResultProcessor(Response response, ChartWriter chartWriter)
   {
      this.response = response;
      this.chartWriter = chartWriter;
   }

   public void processResultValue(ChartModel chartModel) throws IOException
   {
      response.disableCompression();

      OutputStream out = response.getOutputStream(chartModel.getContentType());
      chartWriter.writeChart(out, chartModel);

      response.setHeader("Pragma", "no-cache");
      response.setHeader("Cache-Control", "no-cache");
      response.setDateHeader("Expires", 0);
   }

}

The ChartResultProcessor delegates the chart rendering to ChartWriter.

A component to display JFreeChart and a corresponding imagemap

As different parameters are required for displaying the chart in different image formats we create an abstract class. Any abstract component class muct be placed in ${application-package}.base. The AbstractChart is implemented as

@Import(library = "chart.js")
public abstract class AbstractChart implements ClientElement
{
   @Parameter(value = "prop:componentResources.id", defaultPrefix = BindingConstants.LITERAL, allowNull = false)
   private String clientId;

   @Parameter(required = true, allowNull = false)
   private JFreeChart chart;

   @Parameter(required = true, defaultPrefix = BindingConstants.LITERAL, allowNull = false)
   private int width;

   @Parameter(required = true, defaultPrefix = BindingConstants.LITERAL, allowNull = false)
   private int height;

   @Parameter
   private Object[] context;

   @Parameter(defaultPrefix = BindingConstants.LITERAL)
   private String zone;

   @Parameter
   private ToolTipTagFragmentGenerator toolTipTagGenerator;

   @Parameter(value = "false", defaultPrefix = BindingConstants.LITERAL)
   private boolean useMap;

   ToolTipTagFragmentGenerator defaultToolTipTagGenerator()
   {
      return new StandardToolTipTagFragmentGenerator();
   }

   @Inject
   private ComponentResources resources;

   @Inject
   private JavaScriptSupport javaScriptSupport;

   @Inject
   private ChartWriter chartWriter;

   private String assignedClientId;

   private ChartModel internalChart;

   void setupRender()
   {
      assignedClientId = javaScriptSupport.allocateClientId(clientId);
   }

   boolean beginRender(MarkupWriter writer)
   {
      // Outer Div
      writer.element("div", "id", getClientId());

      // Write image tag
      writer.element("img", "src", getImageURL());

      // Add map if required
      if(useMap)
      {
         writer.attributes("useMap", "#" + getMapName());
      }
      writer.end(); // Close img tag

      String selectMapURL = getSelectMapURL();

      if(useMap)
      {
         createChart();
         initializeChart();

         writer.writeRaw(ChartUtilities.getImageMap(getMapName(),
               internalChart.getInfo(),
               toolTipTagGenerator,
               getURLTagGenerator(selectMapURL)));
      }

      writer.end();// Close Outer Div

      if(zone != null)
      {
         addJavaScript(selectMapURL);
      }

      return false;
   }

   private String getSelectMapURL()
   {
      return resources.createEventLink(ChartConstants.SELECT_MAP, context).toAbsoluteURI();
   }

   private URLTagFragmentGenerator getURLTagGenerator(final String url)
   {

      return new URLTagFragmentGenerator()
      {

         public String generateURLFragment(String text)
         {
            String[] parts = text.split("\\?");
            return String.format("href='%s?%s'", url, parts[1]);
         }

      };
   }

   private String getImageURL()
   {
      return resources.createEventLink(ChartConstants.SHOW_CHART, context).toAbsoluteURI();
   }

   private String getMapName()
   {
      return getClientId() + "_map";
   }

   public String getClientId()
   {
      return assignedClientId;
   }

   private void addJavaScript(String url)
   {

      JSONObject params = new JSONObject();
      params.put("zone", zone);
      params.put("id", getMapName());
      params.put("url", url);
      javaScriptSupport.addInitializerCall("mapToZone", params);
   }

   @OnEvent(ChartConstants.SHOW_CHART)
   Object showChart()
   {
      createChart();
      return internalChart;
   }

   private void createChart()
   {
      internalChart = createChart(chart, width, height);
   }

   private void initializeChart()
   {
      OutputStream out = new DummyOutputStream();
      try
      {
         chartWriter.writeChart(out, internalChart);
      }
      catch(IOException e)
      {
         throw new RuntimeException("Could not write chart : ", e);
      }
   }

   protected abstract ChartModel createChart(JFreeChart chart, int width, int height);

}

There is one hack. The imagemap is generated by ChartRenderingInfo which can only be obtained after the chart has been created. So we have to create the chart twice, once during the rendering phase to obtain the map, and other during the action phase for rendering the chart. As the output is not required during the rendering phase, we use a DummyOutputStream.

public class DummyOutputStream extends OutputStream
{
   @Override
   public void write(int b) throws IOException
   {
   }
}

Another thing to notice is that the areas in imagemap have to be linked to the zone(if supplied). This is done by the script

Tapestry.Initializer.mapToZone = function(spec)
{
   $A($(spec.id).childNodes).each(function(e)
   {
       if(e.tagName == "AREA")
       {
          Event.observe($(e), "click", function(event){
             event.preventDefault();
             var zoneManager = Tapestry.findZoneManagerForZone(zone);
             if(zoneManager != null)
             {
                zoneManager.updateFromURL(e.href);
             }
          });
       }
   });

};

The JPEGChart & PNGChart inherits most of the functionality from the AbstractChart.

public class JPEGChart extends AbstractChart
{
   @Parameter(value = "0.9", defaultPrefix = BindingConstants.LITERAL, allowNull = false)
   private float quality;

   @Override
   protected ChartModel createChart(JFreeChart chart, int width, int height)
   {
      return new JPEGChartModel(chart, width, height, quality);
   }

}

public class PNGChart extends AbstractChart
{
   @Parameter(value = "false", defaultPrefix = BindingConstants.LITERAL, allowNull = false)
   private boolean encodeAlpha;

   @Parameter(value = "0", defaultPrefix = BindingConstants.LITERAL, allowNull = false)
   private int compression;

   @Override
   protected ChartModel createChart(JFreeChart chart, int width, int height)
   {
      return new PNGChartModel(chart, width, height, encodeAlpha, compression);
   }
}

And finally the Module class

public class ChartModule
{
   public static void bind(ServiceBinder binder)
   {
      binder.bind(ChartWriter.class, ChartWriterImpl.class);
   }

   public ChartRenderer buildChartRender(StrategyBuilder builder,
         @SuppressWarnings("rawtypes") Map<Class, ChartRenderer> chartRenderers)
   {
      return builder.build(ChartRenderer.class, chartRenderers);
   }

   @Contribute(ComponentEventResultProcessor.class)
   public void provideResultProcessors(
         @SuppressWarnings("rawtypes")
         MappedConfiguration<Class,ComponentEventResultProcessor> configuration)
   {
      configuration.addInstance(ChartModel.class, ChartResultProcessor.class);
   }

   @Contribute(ChartRenderer.class)
   public void provideChartRenderers(
         @SuppressWarnings("rawtypes")
         MappedConfiguration<Class, ChartRenderer> configuration)
   {
      configuration.addInstance(JPEGChartModel.class, JPEGChartRenderer.class);
      configuration.addInstance(PNGChartModel.class, PNGChartRenderer.class);
   }
}

ChartConstants is implemented as

public class JPEGChart extends AbstractChart
{
   @Parameter(value = "0.9", defaultPrefix = BindingConstants.LITERAL, allowNull = false)
   private float quality;

   @Override
   protected ChartModel createChart(JFreeChart chart, int width, int height)
   {
      return new JPEGChartModel(chart, width, height, quality);
   }

}

The full source code is here

From http://tawus.wordpress.com/2011/07/30/tapestry-jfreechart-integration/

Published at DZone with permission of Taha Siddiqi, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)