/*
  Copyright (C) 2007 Helge Hess

  This file is part of JOPE.

  JOPE is free software; you can redistribute it and/or modify it under
  the terms of the GNU Lesser General Public License as published by the
  Free Software Foundation; either version 2, or (at your option) any
  later version.

  JOPE is distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
  License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with JOPE; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/
package org.opengroupware.jope.appserver.publisher;

import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opengroupware.jope.appserver.core.WOContext;
import org.opengroupware.jope.appserver.core.WORequest;
import org.opengroupware.jope.appserver.core.WOResponse;
import org.opengroupware.jope.foundation.NSObject;
import org.opengroupware.jope.foundation.UString;

/*
 * JoSimpleJSONRenderer
 * 
 * This is a simple renderer which can render plist objects into JSON. It checks
 * whether the client accepts JSON requests by:
 * a) checking the 'accept' HTTP header (must allow application/json)
 * b) checking for a 'format' form parameter with value 'json'
 * 
 * If you want to enforce rendering using JSON, wrap your object in a
 * JoJSONResult object.
 */
public class JoSimpleJSONRenderer extends NSObject
  implements IJoObjectRenderer
{
  protected static final Log log = LogFactory.getLog("JoSimpleJSONRenderer");
  
  /* control rendering */
  
  public boolean isJSONRequest(WORequest _rq) {
    if (_rq == null)
      return false;
    
    if (_rq.acceptsContentType("application/json", false /* no wildcard */))
      return true;
    
    String fmt = _rq.stringFormValueForKey("format");
    if (fmt != null && "json".equals(fmt))
      return true;
    
    return false;
  }
  
  public boolean canRenderObjectInContext(Object _object, WOContext _ctx) {
    /* enforce JSON */
    if (_object instanceof JoJSONResult)
      return true;
    
    /* check whether the client accepts JSON */
    
    WORequest rq = _ctx.request();
    if (rq == null) {
      log.warn("missing request in context: " + _ctx);
      return false;
    }
    
    if (!this.isJSONRequest(rq)) {
      if (log.isInfoEnabled())
        log.info("request accepts no JSON: " + rq);
      return false;
    }
    
    /* Note: do *NOT* move up, first we need to check whether the client
     *       actually wants JSON!
     */
    if (_object == null) return true;

    if (_object instanceof String)  return true;
    if (_object instanceof Boolean) return true;
    if (_object instanceof Number)  return true;
    if (_object instanceof List)    return true;
    if (_object instanceof Map)     return true;
    
    log.warn("object type unsupported by this renderer: " + _object.getClass());
    return false;
  }
  
  /* rendering */
  
  public Exception renderObjectInContext(Object _object, WOContext _ctx) {
    StringBuilder json = new StringBuilder(4096);
    Exception error = this.appendObjectToString(_object, json);
    if (error != null) return error;
    
    WOResponse r = _ctx.response();
    r.setContentEncoding("utf8");
    r.setHeaderForKey("application/json; utf-8", "content-type");
    r.enableStreaming();
    r.appendContentString(json.toString());
    return null;
  }
  
  /* JSON rendering */

  public static final String[] JSEscapeList = {
    "\\", "\\\\",
    "'", "\\'",
    "\"", "\\\"",
    "\n", "\\n",
    "\b", "\\b",
    "\f", "\\f",
    "\r", "\\r",
    "\t", "\\t",
  };
  
  public Exception appendObjectToString(Object _object, StringBuilder _sb) {
    if (_object == null) {
      _sb.append("null");
      return null;
    }
    
    if (_object instanceof JoJSONResult)
      return this.appendObjectToString(((JoJSONResult)_object).result(), _sb);
    
    if (_object instanceof String) {
      String s = (String)_object;
      _sb.append("\"");
      _sb.append(UString.replaceInSequence(s, JSEscapeList));
      _sb.append("\"");
      return null;
    }
    
    if (_object instanceof Boolean) {
      _sb.append(((Boolean)_object).booleanValue() ? "true" : "false");
      return null;
    }
    
    if (_object instanceof Number) {
      _sb.append(_object);
      return null;
    }
    
    if (_object instanceof List)
      return this.appendListToString((List)_object, _sb);
      
    if (_object instanceof Map)
      return this.appendMapToString((Map)_object, _sb);
      
    return this.appendCustomObjectToString(_object, _sb);
  }
  
  public Exception appendListToString(List _list, StringBuilder _sb) {
    if (_list == null) {
      _sb.append("null");
      return null;
    }
    
    _sb.append('(');
    
    boolean isFirst = true;
    for (Object value: _list) {
      if (isFirst) isFirst = false;
      else _sb.append(',');
      
      Exception error = this.appendObjectToString(value, _sb);
      if (error != null) return error;
    }
    
    _sb.append(')');
    return null;
  }
  
  public Exception appendMapToString(Map _map, StringBuilder _sb) {
    if (_map == null) {
      _sb.append("null");
      return null;
    }
    
    _sb.append('{');
    
    boolean isFirst = true;
    for (Object key: _map.keySet()) {
      if (isFirst) isFirst = false;
      else _sb.append(',');
      
      Exception error = this.appendObjectToString(key, _sb);
      if (error != null) return error;
      
      _sb.append(':');
      error = this.appendObjectToString(_map.get(key), _sb);
      if (error != null) return error;
    }
    
    _sb.append('}');
    return null;
  }
  
  public Exception appendCustomObjectToString(Object _obj, StringBuilder _sb) {
    if (_obj instanceof Exception)
      return (Exception)_obj;
    
    log.warn("cannot render object as JSON: " + _obj);
    return new JoInternalErrorException("cannot render given object as JSON");
  }
}
