/* (c) 2014 Open Source Geospatial Foundation - all rights reserved * (c) 2001 - 2013 OpenPlans * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.wfs.json; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintStream; import java.util.Map; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import net.sf.json.JSONException; import net.sf.json.util.JSONBuilder; import org.apache.commons.io.IOUtils; import org.geoserver.ows.Request; import org.geoserver.ows.util.OwsUtils; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.platform.ServiceException; /** * Enum to hold the MIME type for JSON and some useful related utils * <ul> * <li>JSON: application/json</li> * <li>JSONP: text/javascript</li> * </ul> * * @author Carlo Cancellieri - GeoSolutions */ public enum JSONType { JSONP, JSON; /** * The key value into the optional FORMAT_OPTIONS map */ public final static String CALLBACK_FUNCTION_KEY = "callback"; /** * The key value into the optional FORMAT_OPTIONS map. * * Use <code>null</null> for default feature id generation. Use string to nominate an attribute to use. */ public final static String ID_POLICY = "id_policy"; /** * The default value of the callback function */ public final static String CALLBACK_FUNCTION = "parseResponse"; public final static String json = "application/json"; public final static String simple_json = "json"; public final static String jsonp = "text/javascript"; /** * The key of the property to enable the JSonp responses This property is set default to false. */ public final static String ENABLE_JSONP_KEY = "ENABLE_JSONP"; private static boolean jsonpEnabled = isJsonpPropertyEnabled(); /** * Check if the passed MimeType is a valid jsonp * * @param type the MimeType string representation to check * @return true if type is equalsIgnoreCase to {@link #jsonp} */ public static boolean isJsonpMimeType(String type) { return JSONType.jsonp.equalsIgnoreCase(type); } /** * Check if the passed MimeType is a valid jsonp and if jsonp is enabled * * @param type the MimeType string representation to check * @return true if type is equalsIgnoreCase to {@link #jsonp} and jsonp is enabled * @see {@link JSONType#isJsonMimeType(String)} */ public static boolean useJsonp(String type) { return JSONType.isJsonpEnabled() && JSONType.isJsonpMimeType(type); } private static ReadWriteLock lock = new ReentrantReadWriteLock(true); /** * @return The boolean returned represents the value of the jsonp toggle (if true jsonp is enabled) */ public static boolean isJsonpEnabled() { lock.readLock().lock(); try { return jsonpEnabled; } finally { lock.readLock().unlock(); } } /** * Enable disable the jsonp toggle overriding environment and properties. * * @see {@link JSONType#isJsonpEnabledByEnv()} and {@link JSONType#isJsonpEnabledByProperty()} * @param jsonpEnabled true to enable jsonp */ public static void setJsonpEnabled(boolean jsonpEnabled) { if (jsonpEnabled != JSONType.jsonpEnabled) { lock.writeLock().lock(); try { JSONType.jsonpEnabled = jsonpEnabled; } finally { lock.writeLock().unlock(); } } } /** * Parses the ENABLE_JSONP value as a boolean. * * @return The boolean returned represents the value true if the string argument of the ENABLE_JSONP property is not null and is equal, ignoring * case, to the string "true". */ private static boolean isJsonpPropertyEnabled() { String jsonp = GeoServerExtensions.getProperty(ENABLE_JSONP_KEY); return Boolean.parseBoolean(jsonp); } /** * Check if the passed MimeType is a valid json * * @param type the MimeType string representation to check * @return true if type is equalsIgnoreCase to {@link JSONType#json} or to {@link JSONType#simple_json} */ public static boolean isJsonMimeType(String type) { return JSONType.json.equalsIgnoreCase(type) || JSONType.simple_json.equalsIgnoreCase(type); } /** * Return the JSNOType enum matching the passed MimeType or null (if no match) * * @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) { if (json.equalsIgnoreCase(mime) || simple_json.equalsIgnoreCase(mime)) { return JSON; } else if (jsonp.equalsIgnoreCase(mime)) { return JSONP; } else { return null; // not valid representation } } /** * get the MimeType for this object * * @return return a string representation of the MimeType */ public String getMimeType() { switch (this) { case JSON: return json; case JSONP: return jsonp; default: return null; } } /** * get an array containing all the MimeType handled by this object * * @return return a string array of handled MimeType * */ public static String[] getSupportedTypes() { if (isJsonpEnabled()) return new String[] { json, simple_json, jsonp }; else return new String[] { json, simple_json }; } /** * 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) { if (!(kvp.get("FORMAT_OPTIONS") instanceof Map)) { return JSONType.CALLBACK_FUNCTION; } else { Map<String, String> map = (Map<String, String>) kvp.get("FORMAT_OPTIONS"); String callback = map.get(CALLBACK_FUNCTION_KEY); if (callback != null) { return callback; } else { return JSONType.CALLBACK_FUNCTION; } } } /** * Can be used when {@link #json} format is specified to resolve the id_policy parameter from FORMAT_OPTIONS map * * GeoJSON does not require use of an id for each feature, this format option can be used to surpress the use of id (or nominate * an specifc attribtue to use). * * @param kvp request key value pair map possibly including format options * @return null to use generated feature id, empty string to surpress id generation, or attribute to use */ public static String getIdPolicy(Map kvp) { if (!(kvp.get("FORMAT_OPTIONS") instanceof Map)) { return null; } else { Map<String, String> formatOptions = (Map<String, String>) kvp.get("FORMAT_OPTIONS"); if (formatOptions == null || formatOptions.isEmpty()) { return null; // use fid as id in output } String id_policy = formatOptions.get(ID_POLICY); if (id_policy == null || "true".equals(id_policy)) { return null; // use fid as id in output } if ("false".equals(id_policy) || id_policy.length() == 0) { return ""; // suppress id from output } return id_policy; } } /** * Handle Exception in JSON and JSONP format * * @param LOGGER the logger to use (can be null) * @param exception the exception to write to the response outputStream * @param request the request generated the exception * @param charset the desired charset * @param verbose be verbose * @param isJsonp switch writing json (false) or jsonp (true) */ public static void handleJsonException(Logger LOGGER, ServiceException exception, Request request, String charset, boolean verbose, boolean isJsonp) { final HttpServletResponse response = request.getHttpResponse(); // TODO: server encoding options? response.setCharacterEncoding(charset); ServletOutputStream os = null; try { os = response.getOutputStream(); if (isJsonp) { // jsonp response.setContentType(JSONType.jsonp); JSONType.writeJsonpException(exception, request, os, charset, verbose); } else { // json OutputStreamWriter outWriter = null; try { outWriter = new OutputStreamWriter(os, charset); response.setContentType(JSONType.json); JSONType.writeJsonException(exception, request, outWriter, verbose); } finally { if (outWriter != null) { try { outWriter.flush(); } catch (IOException ioe) { } IOUtils.closeQuietly(outWriter); } } } } catch (Exception e) { if (LOGGER != null && LOGGER.isLoggable(Level.SEVERE)) LOGGER.severe(e.getLocalizedMessage()); } finally { if (os != null) { try { os.flush(); } catch (IOException ioe) { } IOUtils.closeQuietly(os); } } } 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 { JSONBuilder json = new JSONBuilder(outWriter); json.object().key("version").value(request.getVersion()).key("exceptions").array() .object().key("code") .value(exception.getCode() == null ? "noApplicableCode" : exception.getCode()) .key("locator") .value(exception.getLocator() == null ? "noLocator" : exception.getLocator()) .key("text"); // 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); } } json.value(sb.toString()); } json.endObject().endArray().endObject(); } catch (JSONException jsonException) { ServiceException serviceException = new ServiceException("Error: " + jsonException.getMessage()); serviceException.initCause(jsonException); throw serviceException; } } }