This page last changed on Jan 16, 2013 by ccancellieri.

The proposal documentation is obsolete. Please refer here for discussion on JSON formats changes

Overview

Adding support to json format to the exception handlers.

Proposed By

Carlo Cancellieri

Assigned to Release

2.2.2

State

Under Discussion, In Progress, Completed, Rejected, Deferred

Motivation

Recently I've added and documented a quite complete set of response handler to handle responses in json (MimeType: application/json) and jsonP (MimeType: text/javascript) formats but it still was not possible to get exceptions in that format.

Proposal

GeoServer currently does not implements for the ExceptionHandler(s) an extension point as provided for the ResponseHandler(s). So the goal of this improvement is to have the exception in JSON and JSONP format with the minimum api impact.

API

The JSONType

The following enum is used by all the Json and JsonP related components (including ResponseHandler, and JUnit tests). This is done to limit code redundancy and to share between all the WMS and WFS developed extensions:

  • MimeType (application/json, text/javascript)
  • Constants (callback, paddingOuptut)
  • Utils (isJsonMimeType, isJsonpMimeType, getCallbackFunction, handleJsonException, ...)

Here is the most important pieces of code from the JSONType enum:

/**
 * Enum to hold the MIME type for JSON and some useful related utils
 * <ul>
 *  <li>application/json</li>
 *  <li>text/javascript</li>
 * </ul>
 *
 * @author Carlo Cancellieri
 */
public enum JSONType {
	JSONP,
	JSON;

	/**
	 * The key value into the optional FORMAT_OPTIONS map
	 */
	public final static String CALLBACK_FUNCTION_KEY = "callback";
	/**
	 * The default value of the callback function
	 */
	public final static String CALLBACK_FUNCTION = "paddingOutput";

	public final static String json="application/json";
	public final static String jsonp="text/javascript";

	/**
	 * Check if the passed MimeType is a valid jsonp
	 * @param type the MimeType string representation to check
	 * @return true if type is equals to {@link #jsonp}
	 */
	public static boolean isJsonpMimeType(String type) {...}

	/**
	 * Check if the passed MimeType is a valid json
	 * @param type the MimeType string representation to check
	 * @return true if type is equals to {@link #json}
	 */
         public static boolean isJsonMimeType(String type) {...}

    /**
     * @param mime the mimetype to check
     * @return the JSNOType enum matching the passed MimeType or null
     * (if no match)
     */
	public static JSONType getJSONType(String mime){...}

	/**
	 * get the MimeType for this object
	 * @return return a string representation of the MimeType
	 */
	public String getMimeType(){...}

	/**
	 * get an array containing all the MimeType handled by this object
	 * @return return a string array of handled MimeType
	 * @return
	 */
	public static String[] getSupportedTypes(){...}

	/**
	 * Can be used when {@link #jsonp} format is specified to
         * resolve the callback parameter into the FORMAT_OPTIONS map
	 * @param kvp the kay value pair map of the request
	 * @return The string name of the callback function or
         * the default {@link #CALLBACK_FUNCTION} if not found.
	 */
	public static String getCallbackFunction(Map kvp){...}


	public static void handleJsonException(Logger LOGGER,
              ServiceException exception, Request request,
              String charset, boolean verbose, boolean isJsonp) {
    	...
    	if (isJsonp) {
    		// jsonp
    		JSONType.writeJsonpException(exception,request,
                                             os,charset,verbose);
    	} else {
    		// json
    		OutputStreamWriter outWriter = null;
    		try {
  		  outWriter = new OutputStreamWriter(os, charset);
    		  JSONType.writeJsonException(exception, request,
                                                 outWriter, verbose);
    		} finally {
    			...
    		}
    	}
    	...
    }

    private static void writeJsonpException(ServiceException exception,
                Request request, OutputStream out, String charset, boolean verbose)
			throws IOException {

		OutputStreamWriter outWriter = new OutputStreamWriter(out, charset);
		final String callback;
    	    	if (request == null) {
			callback=JSONType.CALLBACK_FUNCTION;
		} else {
			callback=JSONType.getCallbackFunction(request.getKvp());
		}
		outWriter.write(callback + "(");

		writeJsonException(exception, request, outWriter, verbose);

		outWriter.write(")");
		outWriter.flush();
		IOUtils.closeQuietly(outWriter);
	}

    private static void writeJsonException(ServiceException exception,
                Request request, OutputStreamWriter outWriter, boolean verbose)
                         throws IOException {
    try {
	final JsonWriter jsonWriter = new JsonWriter(outWriter);
	jsonWriter.startNode("ExceptionReport", String.class);
	jsonWriter.addAttribute("version", request.getVersion());
	jsonWriter.startNode("Exception", String.class);
	jsonWriter.addAttribute("exceptionCode",
              exception.getCode()==null?"noApplicableCode":exception.getCode());
	jsonWriter.addAttribute("exceptionLocator",
              exception.getLocator()==null?"noLocator":exception.getLocator());
	jsonWriter.startNode("ExceptionText", String.class);
	// message
	if ((exception.getMessage() != null)) {
	     StringBuffer sb=new StringBuffer(exception.getMessage().length());
	     OwsUtils.dumpExceptionMessages(exception, sb, false);
	     if (verbose) {
	        ByteArrayOutputStream stackTrace=null;
	        try {
		        stackTrace = new ByteArrayOutputStream();
		        exception.printStackTrace(new PrintStream(stackTrace));
		        sb.append("\nDetails:\n");
		        sb.append(new String(stackTrace.toByteArray()));
	        } finally {
	        	IOUtils.closeQuietly(stackTrace);
	        }
	     }
	     jsonWriter.setValue(sb.toString());
	}
	jsonWriter.endNode();
	jsonWriter.endNode();
	jsonWriter.endNode();

     } catch (JSONException jsonException) {
		ServiceException serviceException =
                           new ServiceException("Error: "
				+ jsonException.getMessage());
		serviceException.initCause(jsonException);
		throw serviceException;
     }
  }

}

NOTE:
The code generating the exception body response is based on the default one.

WMSExceptionHandler changes to support json and jsonp formats

As said, the purpose of this patch is to provide the json and jsonp support with the minimum impact on the GeoServer api.

Changes applied to support json and jsonp formats:

 public void handleServiceException(ServiceException exception,
                                                     Request request) {
        // first of all check what kind of exception
        try {
            exceptions = (String) request.getKvp().get("EXCEPTIONS");
            if (exceptions == null) {
            	// use default
                handleXmlException(exception, request);
                return;
            }
        } catch (Exception e) {
            // width and height might be missing
            handleXmlException(exception, request);
            return;
        }
        boolean verbose=geoServer.getSettings().isVerboseExceptions();
    	String charset=geoServer.getSettings().getCharset();
        if (JSONType.isJsonMimeType(exceptions)){
        	// use Json format
        	JSONType.handleJsonException(LOGGER,exception, request,
                                                  charset,verbose,false);
        	return;
        } else if (JSONType.isJsonpMimeType(exceptions)){
        	// use JsonP format
        	JSONType.handleJsonException(LOGGER,exception, request,
                                                  charset,verbose,true);
        	return;
        } else if (isImageExceptionType(exceptions)) {
            // ok, it's image, then we have to build a text representing the
	    // exception and lay it out in the image
            try {
                width = (Integer) request.getKvp().get("WIDTH");
                height = (Integer) request.getKvp().get("HEIGHT");
                format = (String) request.getKvp().get("FORMAT");
                bgcolor = (Color) request.getKvp().get("BGCOLOR");
                transparent = (Boolean) request.getKvp().get("TRANSPARENT");
                if (width > 0 && height > 0 && FORMATS.contains(format)){
    		        handleImageException(exception, request, width,
                           height, format, exceptions, bgcolor, transparent);
    		        return;
    	        } else {
    	        	// use default
    	            handleXmlException(exception, request);
    	        }
            } catch (Exception e) {
                // width and height might be missing
            	// use default
                handleXmlException(exception, request);
            }
        } else {
        	// use default
            handleXmlException(exception, request);
        }
    }

WFSExceptionHandler

Same thing is done for the WFS:

    /**
     * Encodes a ogc:ServiceExceptionReport to output.
     */
    public void handleServiceException(ServiceException exception,
                                                           Request request) {

        boolean verbose=gs.getSettings().isVerboseExceptions();
    	String charset=gs.getSettings().getCharset();
    	// first of all check what kind of exception handling we must perform
        final String exceptions;
        try {
            exceptions = (String) request.getKvp().get("EXCEPTIONS");
            if (exceptions == null) {
            	// use default
            	handleDefault(exception, request, charset, verbose);
                return;
            }
        } catch (Exception e) {
            // width and height might be missing
            handleDefault(exception, request, charset, verbose);
            return;
        }
        if (JSONType.isJsonMimeType(exceptions)){
        	// use Json format
        	JSONType.handleJsonException(LOGGER,exception, request,
                                                      charset, verbose, false);
        } else if (JSONType.isJsonpMimeType(exceptions)){
        	// use JsonP format
        	JSONType.handleJsonException(LOGGER,exception, request,
                                                       charset, verbose, true);
        } else {
        	handleDefault(exception,request,charset, verbose);
        }

    }

Note that the code handling the json and jsonp exception is the same for the WFS and WMS ExceptionHandler(s) (see JSONType).

Examples

WFS

Example DescribeFeatureType getting exception in JSONP

The following example shows how to get the exception in JsonP format, the callback function is specified into the format_options using the callback key:

http://localhost:8080/geoserver/wfs/?service=wfs
&version=1.1.0
&request=DescribeFeatureType
&typeName=topp:group
&outputFormat=text/javascript
&format_options=callback%3AgetLayerFeatures_MY
&EXCEPTIONS=text/javascript

JSONP Result

getLayerFeatures_MY({"ExceptionReport": {
"@version": "1.1.0",
"Exception": {
"@exceptionCode": "noApplicableCode",
"@exceptionLocator": "noLocator",
"ExceptionText": "Could not find type: {http://www.openplans.org/topp}group"
}
}})

Note:
The format_options callback parameter is NOT mandatory in that case (JSONP for response or exception) the default one is used "paddingOutput" (see JSONType).

Example DescribeFeatureType getting exception in JSON

http://localhost:8080/geoserver/wfs/?service=wfs
&version=1.1.0
&request=DescribeFeatureType
&typeName=topp:group&outputFormat=text/javascript
&format_options=callback%3AgetLayerFeatures_MY
&EXCEPTIONS=application/json

JSON Result

{"ExceptionReport": {
"@version": "1.1.0",
"Exception": {
"@exceptionCode": "noApplicableCode",
"@exceptionLocator": "noLocator",
"ExceptionText": "Could not find type: {http://www.openplans.org/topp}group"
}
}}

Example DescribeFeatureType getting exception in default format

The following example is to show that the default format for the exceptio is independent from the specified outputFormat (which is still text/javascript:

http://localhost:8080/geoserver/wfs/?service=wfs
&version=1.1.0
&request=DescribeFeatureType
&typeName=topp:group
&outputFormat=text/javascript

XML (Default) result

<ows:ExceptionReport xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ows="http://www.opengis.net/ows" version="1.0.0"xsi:schemaLocation="http://www.opengis.net/ows http://localhost:8080/geoserver/schemas/ows/1.0.0/owsExceptionReport.xsd">
<ows:Exception exceptionCode="NoApplicableCode">
<ows:ExceptionText>
Could not find type: {http://www.openplans.org/topp}group
</ows:ExceptionText>
</ows:Exception>
</ows:ExceptionReport>

Limitations

We don't have a specific format_option for exception callback right now, so the exception callback function name will be the same of the response (if an exceptions occurs).
Do we really need a different one?

What's next

A second step I think will be introducing a pluggable exception response handler mechanism as provided for the ResponseHandler(s).

Feedback

This section should contain feedback provided by PSC members who may have a problem with the proposal.

Backwards Compatibility

State here any backwards compatibility issues.

Voting

Andrea Aime: +1
Alessio Fabiani: +1
Ben Caradoc-Davies: +1
Gabriel Roldán:
Justin Deoliveira: +1
Jody Garnett: +1
Mark Leslie:
Phil Scadden: +1
Rahkonen Jukka: +1
[~roba]:
Simone Giannecchini: +0

backport 2.2

Andrea Aime: +1
Alessio Fabiani: +1
Ben Caradoc-Davies: +1
Christian Mueller: +1
Gabriel Roldán: +0
Justin Deoliveira:
Jody Garnett:
Mark Leslie:
Phil Scadden:
Rahkonen Jukka: +0
[~roba]:
Simone Giannecchini: +1

Links

JIRA Task
Patches
JSONP
Documentation
[Email Discussion|]
[Wiki Page|]

Document generated by Confluence on May 14, 2014 23:00