001    //$HeadURL: http://svn.wald.intevation.org/svn/deegree/base/trunk/src/org/deegree/enterprise/servlet/ChartServlet.java $
002    /*----------------------------------------------------------------------------
003     This file is part of deegree, http://deegree.org/
004     Copyright (C) 2001-2009 by:
005     Department of Geography, University of Bonn
006     and
007     lat/lon GmbH
008    
009     This library is free software; you can redistribute it and/or modify it under
010     the terms of the GNU Lesser General Public License as published by the Free
011     Software Foundation; either version 2.1 of the License, or (at your option)
012     any later version.
013     This library is distributed in the hope that it will be useful, but WITHOUT
014     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
015     FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
016     details.
017     You should have received a copy of the GNU Lesser General Public License
018     along with this library; if not, write to the Free Software Foundation, Inc.,
019     59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
020    
021     Contact information:
022    
023     lat/lon GmbH
024     Aennchenstr. 19, 53177 Bonn
025     Germany
026     http://lat-lon.de/
027    
028     Department of Geography, University of Bonn
029     Prof. Dr. Klaus Greve
030     Postfach 1147, 53001 Bonn
031     Germany
032     http://www.geographie.uni-bonn.de/deegree/
033    
034     e-mail: info@deegree.org
035     ----------------------------------------------------------------------------*/
036    
037    package org.deegree.enterprise.servlet;
038    
039    import java.awt.image.BufferedImage;
040    import java.io.IOException;
041    import java.net.URL;
042    import java.util.Iterator;
043    import java.util.Map;
044    
045    import javax.servlet.ServletException;
046    import javax.servlet.http.HttpServlet;
047    import javax.servlet.http.HttpServletRequest;
048    import javax.servlet.http.HttpServletResponse;
049    
050    import org.deegree.framework.log.ILogger;
051    import org.deegree.framework.log.LoggerFactory;
052    import org.deegree.framework.util.ImageUtils;
053    import org.deegree.framework.util.KVP2Map;
054    import org.deegree.framework.util.MimeTypeMapper;
055    import org.deegree.graphics.charts.ChartConfig;
056    import org.deegree.graphics.charts.ChartsBuilder;
057    import org.deegree.graphics.charts.IncorrectFormatException;
058    import org.deegree.graphics.charts.QueuedMap;
059    import org.deegree.i18n.Messages;
060    
061    /**
062     * <code>
063     * A Servlet that gets an http GET request, with the de.latlon.charts parameters and it generates
064     * and returns a BufferedImage based on these parameters. The request should look as follows:
065     * http://www.someserver.com/servletname?
066     * chart=ChartType&                 ( Mandatory )
067     * title=string&
068     * legend=boolean&                  ( default = true )
069     * width=int&                       ( Mandatory )
070     * height=int&                      ( Mandatory )
071     * xAxis=string&
072     * yAxis=string&
073     * tooltips=boolean&                ( default = false )
074     * lblType                          Determines in PieChart what the legend and the tool tip to be. You can
075     *                                  either show the data key, value or both. Possible values for this parameter are:
076     *                                  <i>Key</i>, <i>Value</i> and <i>KeyValue</i> ( default = Key )
077     * orientation=direction&           ( default = vertical )
078     * format=image_format&             ( Mandatory )
079     * value=value                      ( Mandatory )
080     * 
081     * The keys are the following
082     * I) Chart Type: Available chart Types are:
083     * 1- Pie
084     * 2- Pie3D
085     * 3- Bar
086     * 4- Bar3D
087     * 5- Line
088     * 6- Line3D
089     * 7- XYBar
090     * 8- XYLine
091     * 
092     * II)  title: Title of the generated image
093     * II)  legend: true/false to show/hide the legend of the categories/rows
094     * III) width: Width of the generated image
095     * IV) height: Height of the generated image
096     * V) xAxis Title of the
097     * X Axis Title of xAxis
098     * VI) yAxis Title of the Y Axis
099     * VII) tooltips Show/hide tool tips
100     * VIII)orientation vertical/horizontal. the orientation of the chart
101     * IX) format format of the output image, ex: image/png and image/jpeg
102     * X) type There is actually no parameter called type.
103     *      It only indicates the more parameters that carry the actual values for the chart.
104     *      They can have different three different formats. but only one format type shall be used
105     *      per request. The values can be given as normal key value pairs as follows:
106     * 
107     *          Format 1: &CDU=34&SPD=36&Gruene=11 - In this format the key has only one value
108     *              - This type can only be used together with the chart type: Pie, Pie3D, Bar, Line and Line3D
109     * 
110     *           Format 2: &CDU=23,25,21,26&SPD=42,23,33,36
111     *              - In this format multiple values per keys can be given - Either comma or white spaces can be
112     *                  used as separators(white spaces are of course encoded in case of a Http Get request)
113     *              - This type can only be used together with the chart type: Bar, Line and ine3D
114     * 
115     *          Format 3: &CDU=1970,23;1974,44;1978,43&SPD=1970,23;1974,44;1978,43
116     *              - In this format the key has multiple value, in which each value is a tuple that contains in
117     *                  turn two tokens representing x and y of a cartesian point. This format is
118     *                  used to create cartesian de.latlon.charts
119     *              - comma and white spacescan be used as separators between tokens, i.e. between x and y,
120     *                  while semi colon shall be used as a separator between tupels.
121     *              - This type can only be used together with XYLine (and later with XYBar)
122     * </code>
123     * 
124     * @author <a href="mailto:elmasry@lat-lon.de">Moataz Elmasry</a>
125     * @author last edited by: $Author: apoth $
126     * 
127     * @version $Revision: 29949 $, $Date: 2011-03-09 13:08:56 +0100 (Wed, 09 Mar 2011) $
128     */
129    public class ChartServlet extends HttpServlet {
130    
131        private static final ILogger LOG = LoggerFactory.getLogger( ChartServlet.class );
132    
133        private String errorMsgPath = null;
134    
135        private ChartConfig configs = null;
136    
137        /*
138         * (non-Javadoc)
139         * 
140         * @see javax.servlet.GenericServlet#init()
141         */
142        @Override
143        public void init()
144                                throws ServletException {
145            errorMsgPath = this.getServletContext().getRealPath( this.getInitParameter( "errorMsg" ) );
146            try {
147                String filePath = this.getServletContext().getRealPath( this.getInitParameter( "ConfigFile" ) );
148                if ( !filePath.startsWith( "file:///" ) ) {
149                    filePath = "file:///" + filePath;
150                }
151                configs = new ChartConfig( new URL( filePath ) );
152            } catch ( Exception e ) {
153                LOG.logError( e.getLocalizedMessage() );
154                LOG.logError( Messages.getMessage( "GRA_CHART_SERVLET_FAIL", new Object[] {} ) );
155            }
156        }
157    
158        private static final long serialVersionUID = -374323170888642652L;
159    
160        /*
161         * (non-Javadoc)
162         * 
163         * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest,
164         * javax.servlet.http.HttpServletResponse)
165         */
166        @Override
167        protected void doGet( HttpServletRequest request, HttpServletResponse response )
168                                throws ServletException, IOException {
169            performAction( request, response );
170        }
171    
172        /**
173         * @param request
174         * @param response
175         */
176        protected void performAction( HttpServletRequest request, HttpServletResponse response ) {
177    
178            /*
179             * chart=ChartType& title=string& legend=boolean& width=int& height=int& xAxis=string& yAxis=string&
180             * tooltips=boolean& orientation=direction& format=image_format& value=value
181             */
182    
183            Map<String, String> keyedValues = null;
184            try {
185                keyedValues = initAndValidateParams( request );
186            } catch ( Exception e ) {
187                LOG.logError( e.getLocalizedMessage() );
188                goToErrorPage( response );
189                return;
190            }
191    
192            /**
193             * TODO introduce key "SOSURL" to point towards a SOS. In case a SOSURL is provided, the URL will be queried and
194             * it will be assumed that a GetObservation request is returned.
195             * 
196             */
197            // probably not required any more
198    
199            // for ( String key : keyedValues.keySet() ) {
200            // String value = keyedValues.get( key );
201            // try {
202            // String encoding = Charset.defaultCharset().name();
203            // if ( request.getCharacterEncoding() != null ) {
204            // encoding = request.getCharacterEncoding();
205            // }
206            // String dec = URLDecoder.decode( value, encoding );
207            // keyedValues.put( key, dec );
208            // } catch ( Exception e ) {
209            // LOG.logError( e.getLocalizedMessage() );
210            // }
211            // }
212    
213            String format = request.getParameter( "format" );
214            String style = null;
215            ChartConfig chartConfigs = null;
216            if ( ( style = request.getParameter( "style" ) ) != null ) {
217                try {
218                    chartConfigs = new ChartConfig( new URL( style ) );
219                } catch ( Exception e ) {
220                    // Its not a big problem that the chartConfig is null. In this case the default chart config will be
221                    // used
222                    LOG.logError( Messages.getMessage( "GRA_CHART_INVALID_URL", style ) );
223                }
224            }
225            try {
226                BufferedImage img = buildChart( request.getParameter( "chart" ), request.getParameter( "title" ),
227                                                request.getParameter( "legend" ), request.getParameter( "width" ),
228                                                request.getParameter( "height" ), request.getParameter( "xAxis" ),
229                                                request.getParameter( "yAxis" ), request.getParameter( "tooltips" ),
230                                                request.getParameter( "orientation" ), request.getParameter( "lblType" ),
231                                                request.getParameter( "format" ), keyedValues, chartConfigs );
232                String imgType = null;
233                if ( MimeTypeMapper.isKnownImageType( format ) ) {
234                    int index = format.indexOf( "/" );
235                    imgType = format.substring( index + 1, format.length() );
236                } else {
237                    throw new IncorrectFormatException( "The given image type is not a supported type" );
238                }
239                try {
240                    ImageUtils.saveImage( img, response.getOutputStream(), imgType, 1.0f );
241                } catch ( Exception e ) {
242                    // Nothing. An exception happened here, because the GET request is called multiple times.
243                    // This exception happens on the second and third time.
244                }
245            } catch ( Exception e ) {
246                LOG.logError( e.getLocalizedMessage() );
247                goToErrorPage( response );
248            }
249        }
250    
251        /**
252         * Init and validate params does what its name say. It extracts the mandatory parameters from the HttpServletRequest
253         * class and checks their validity. Then it fills a queued map with all parameters, that are not defined, because
254         * these are the candidates to be the parameters for the chart
255         * 
256         * @param request
257         * @return instance of the queued map with
258         * @throws IncorrectFormatException
259         * 
260         */
261        @SuppressWarnings("unchecked")
262        protected QueuedMap<String, String> initAndValidateParams( HttpServletRequest request )
263                                throws IncorrectFormatException {
264    
265            QueuedMap<String, String> keyedValues = new QueuedMap<String, String>();
266    
267            Map<String, String> param = KVP2Map.toMap( request );
268    
269            String chart = param.get( "CHART" );
270            String width = param.get( "WIDTH" );
271            String height = param.get( "HEIGHT" );
272            String format = param.get( "FORMAT" );
273    
274            if ( chart == null ) {
275                throw new IncorrectFormatException( Messages.getMessage( "GRA_CHART_ERROR_NULL_PARAM", "chart" ) );
276            }
277    
278            if ( width == null ) {
279                throw new IncorrectFormatException( Messages.getMessage( "GRA_CHART_ERROR_NULL_PARAM", "width" ) );
280            }
281    
282            if ( height == null ) {
283                throw new IncorrectFormatException( Messages.getMessage( "GRA_CHART_ERROR_NULL_PARAM", "height" ) );
284            }
285    
286            if ( format == null ) {
287                throw new IncorrectFormatException( Messages.getMessage( "GRA_CHART_ERROR_NULL_PARAM", "format" ) );
288            } else if ( !MimeTypeMapper.isKnownImageType( format ) ) {
289                throw new IncorrectFormatException( Messages.getMessage( "GRA_CHART_ERROR_UNSUPPORTED_TYPE" ) );
290            }
291    
292            Iterator<String> it = request.getParameterMap().keySet().iterator();
293            while ( it.hasNext() ) {
294                String key = it.next();
295                if ( !key.equals( "chart" ) && !key.equals( "title" ) && !key.equals( "legend" ) && !key.equals( "width" )
296                     && !key.equals( "height" ) && !key.equals( "xAxis" ) && !key.equals( "yAxis" )
297                     && !key.equals( "tooltips" ) && !key.equals( "orientation" ) && !key.equals( "format" )
298                     && !key.equals( "style" ) ) {
299                    keyedValues.put( key, request.getParameter( key ) );
300                }
301            }
302            return keyedValues;
303        }
304    
305        /**
306         * Creates a buffered image that contains an error label. It is used to display an error instead of a chart in case
307         * an error happened
308         * 
309         * @param response
310         */
311        protected void goToErrorPage( HttpServletResponse response ) {
312            if ( errorMsgPath == null ) {
313                LOG.logError( Messages.getMessage( "GRA_CHART_MISSING_ERROR_IMAGE", new Object[] {} ) );
314                return;
315            }
316            try {
317                BufferedImage img = ImageUtils.loadImage( errorMsgPath );
318                ImageUtils.saveImage( img, response.getOutputStream(), "gif", 1.0f );
319    
320            } catch ( Exception e ) {
321                LOG.logError( e.getLocalizedMessage() );
322            }
323        }
324    
325        /**
326         * Takes in all the needed parameters and a queued map containing the values of the chart and returns a
327         * BufferedImage with the chart
328         * 
329         * @param chart
330         * @param title
331         * @param legend
332         * @param width
333         * @param height
334         * @param xAxis
335         * @param yAxis
336         * @param tooltips
337         * @param orientation
338         * @param lblType
339         * @param imagetype
340         * @param params
341         * @param chartConfigs
342         *            to configure the output chart, or null to use the default ChartConfig
343         * @return BufferedImage containing the chart
344         * @throws IncorrectFormatException
345         */
346        protected BufferedImage buildChart( String chart, String title, String legend, String width, String height,
347                                            String xAxis, String yAxis, String tooltips, String orientation,
348                                            String lblType, String imagetype, Map<String, String> params,
349                                            ChartConfig chartConfigs )
350                                throws IncorrectFormatException {
351            if ( configs == null ) {
352                throw new IncorrectFormatException( Messages.getMessage( "GRA_CHART_ERROR_SERVLET_BROKEN", new Object[] {} ) );
353            }
354    
355            ChartsBuilder builder = null;
356            try {
357                builder = new ChartsBuilder( configs );
358            } catch ( Exception e ) {
359                throw new IncorrectFormatException( e.getLocalizedMessage() );
360            }
361            boolean legend2 = "true".equals( legend );
362            int width2 = Integer.parseInt( width );
363            int height2 = Integer.parseInt( height );
364            boolean tooltips2 = "true".equals( tooltips );
365            int orientation2 = "horizontal".equals( orientation ) ? ChartsBuilder.ORIENTATION_HORIZONTAL
366                                                                 : ChartsBuilder.ORIENTATION_VERTICAL;
367            QueuedMap<String, String> map = new QueuedMap<String, String>();
368            map.putAll( params );
369    
370            final String THREE_D = "3D";
371            /*
372             * * Format 1: &CDU=34&SPD=36&Gruene=11 - In this format the key has only one value - This type can only be used
373             * together with the chart type: Pie, Pie3D, Bar, Line and Line3D
374             * 
375             * Format 2: &CDU=23,25,21,26&SPD=42,23,33,36 - In this format multiple values per keys can be given - Either
376             * comma or white spaces can be used as separators - This type can only be used together with the chart type:
377             * Bar, Line and Line3D
378             * 
379             * Format 3: &CDU=1970 23;1974,44;1978,43&SPD=1970,23;1974,44;1978,43 - In this format the key has multiple
380             * value, in which each value is a tuple that contains in turn two tokens representing x and y of a cartesian
381             * point. This format is used to create cartesian de.latlon.charts - comma and white spacescan be used as
382             * separators between tokens, i.e. between x and y, while semi colon shall be used as a separator between
383             * tupels. - This type can only be used together with XYBar and XYBar
384             */
385            if ( chart.equals( "Pie" ) || chart.equals( "Pie3D" ) ) {
386                QueuedMap<String, Double> map2 = new QueuedMap<String, Double>();
387                Iterator<String> it = map.keySet().iterator();
388                while ( it.hasNext() ) {
389                    String key = it.next();
390                    String value = map.get( key );
391                    try {
392                        if ( value != null ) {
393                            map2.put( key, Double.parseDouble( value ) );
394                        }
395                    } catch ( Exception e ) {
396                        // It's not a problem. There might be parameters, that are not values.
397                    }
398                }
399                return builder.createPieChart( title, map2, width2, height2, chart.contains( THREE_D ), legend2, tooltips2,
400                                               lblType, imagetype, chartConfigs );
401            } else if ( chart.equals( "Line" ) || chart.equals( "Line3D" ) ) {
402                return builder.createLineChart( title, map, width2, height2, chart.contains( THREE_D ), legend2, tooltips2,
403                                                orientation2, imagetype, xAxis, yAxis, chartConfigs );
404            } else if ( chart.equals( "Bar" ) || chart.equals( "Bar3D" ) ) {
405                return builder.createBarChart( title, map, width2, height2, chart.contains( THREE_D ), legend2, tooltips2,
406                                               orientation2, imagetype, xAxis, yAxis, chartConfigs );
407            } else if ( chart.equals( "XYLine" ) ) {
408                return builder.createXYLineChart( title, map, width2, height2, legend2, tooltips2, orientation2, imagetype,
409                                                  xAxis, yAxis, chartConfigs );
410            } else {
411                throw new IncorrectFormatException( Messages.getMessage( "GRA_CHART_UNSUPPORTED_TYPE", chart ) );
412            }
413        }
414    }