/*
  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.sql.SQLException;
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.NSKeyValueCoding;
import org.opengroupware.jope.foundation.NSObject;
import org.opengroupware.jope.foundation.UString;
import org.opengroupware.jope.foundation.kvc.MissingPropertyException;

/**
 * JoSimpleJSONRenderer
 * <p>
 * This is a simple renderer which can render plist objects into JSON. It checks
 * whether the client accepts JSON requests by:<br>
 * a) checking the 'accept' HTTP header (must allow application/json)<br>
 * b) checking for a 'format' form parameter with value 'json'<br>
 * <p>
 * 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;
    if (_object instanceof Throwable) 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);
    
    if (_object instanceof Throwable)
      return this.appendExceptionToString((Throwable)_object, _sb);
      
    return this.appendCustomObjectToString(_object, _sb);
  }
  
  /* specific appenders */
  
  public Exception appendExceptionToString(Throwable _ex, StringBuilder _sb) {
    if (_ex == null) {
      _sb.append("null");
      return null;
    }
    
    Exception error;
    _sb.append('{');
    
    /* add message code */
    
    Object v = null;
    
    if (v == null) {
      try {
        v = NSKeyValueCoding.Utility.valueForKey(_ex, "code");
      }
      catch (MissingPropertyException e) { } /* we do not care and continue */
    }
    if (v == null) {
      try {
        v = NSKeyValueCoding.Utility.valueForKey(_ex, "errorCode");
      }
      catch (MissingPropertyException e) { } /* we do not care and continue */
    }

    if (_ex instanceof SQLException)
      v = "sql" + ((SQLException)_ex).getSQLState();
    
    if (v == null)
      v = _ex.getClass().getName();

    if (v == null)
      v = "unknown";
    
    error = this.appendKeyValuePair("error", v, true, _sb);
    if (error != null) return error;
    
    /* added messages */
    
    String pm = _ex.getMessage();
    String sm = _ex.getLocalizedMessage();
    if (pm == sm || (pm != null && sm != null && pm.equals(sm)))
      sm = null;
    
    error = this.appendKeyValuePair("message", pm, false, _sb);
    if (error != null) return error;
    
    error = this.appendKeyValuePair("localizedMessage", sm, false, _sb);
    if (error != null) return error;
    
    /* additional standard keys */

    v = null;
    try { v = NSKeyValueCoding.Utility.valueForKey(_ex, "httpStatus"); }
    catch (MissingPropertyException e) { } /* we do not care and continue */
    
    error = this.appendKeyValuePair("httpStatus", v, false, _sb);
    if (error != null) return error;
    
    if (_ex instanceof SQLException) {
      error = this.appendKeyValuePair
        ("sqlstate", ((SQLException)_ex).getSQLState(), false, _sb);
      if (error != null) return error;
    }
    
    _sb.append('}');
    return null;
  }
  
  public Exception appendKeyValuePair
    (Object _key, Object _value, boolean _isFirst, StringBuilder _sb)
  {
    if (_value == null) return null;
    
    if (!_isFirst) _sb.append(",");
    Exception error = this.appendObjectToString(_key, _sb);
    if (error != null) return error;
    _sb.append(':');
    return this.appendObjectToString(_value, _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(':');
    }
    
    _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");
  }
}
