//
// Jala Project [http://opensvn.csie.org/traccgi/jala]
//
// Copyright 2004 ORF Online und Teletext GmbH
//
// Licensed under the Apache License, Version 2.0 (the ``License'');
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an ``AS IS'' BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// $Revision$
// $LastChangedBy$
// $LastChangedDate$
// $HeadURL$
//


/**
 * @fileoverview Fields and methods of the jala.XmlWriter class.
 */


// Define the global namespace for Jala modules
if (!global.jala) {
   global.jala = {};
}


/**
 * Construct a new XML writer.
 * @class This class defines a generic interface to write
 * arbitrary and validating XML source code. This is done
 * by first applying data objects onto template objects,
 * both in a specified format. Then, the resulting object
 * tree is transformed into XML. Moreover, template objects
 * can be extended with other template objects to provide
 * full flexibility in inheriting subclasses.
 * @param {String} header An optional XML header.
 * @returns A new XML writer.
 * @constructor
 */
jala.XmlWriter = function(header) {
   var self = this;
   var XMLHEADER = header ||
                   '<?xml version="1.0" encoding="iso-8859-15"?>';
   var LOCALE = java.util.Locale.ENGLISH;

   var write = function(str) {
      return res.write(str);
   };

   var writeln = function(str) {
      res.write(str);
      res.write("\n");
      return;
   };

   var getString = function(data, format) {
      if (data == null)
         return;
      switch (data.constructor) {
         case String:
            return encodeXml(data);
         case Number:
         case Date:
            if (format && data.format)
               return encodeXml(data.format(format, LOCALE));
            else if (data.toUTCString)
               return encodeXml(data.toUTCString());
            else
               return encodeXml(data.toString());
            break;
         case Object:
            return null;
      }
      return encodeXml(data.toString());
   };

   /** @ignore */
   var XmlElement = function(data) {
      if (!data)
         throw Error("Insufficient arguments to create XmlElement");

      var children = {};
      var properties = [
         "name",
         "attributes",
         "type",
         "required",
         "format",
         "readonly"
      ];

      if (data.value) {
         if (data.value.constructor == Object) {
            this.value = [new XmlElement(data.value)];
         } else if (data.value.constructor == Array) {
            this.value = [];
            for (var i in data.value) {
               this.value[i] = new XmlElement(data.value[i]);
            }
         } else
            throw Error("Cannot handle unknown type of template value");
      }

      for (var i in properties) {
         var key = properties[i];
         this[key] = data[key] || null;
      }

      if (this.attributes) {
         this.attributes = self.clone(this.attributes);
         if (this.attributes.constructor == Object)
            this.attributes = [this.attributes];
      } else {
         this.attributes = [];
      }

      return this;
   };

   /** @ignore */
   XmlElement.toString = function() {
      return "[XmlElement constructor]";
   };

   /** @ignore */
   XmlElement.prototype.setValue = function(element) {
      if (element.constructor != this.constructor)
         throw Error("Invalid type for XmlElement addition");
      if (!this.value)
         this.value = [];
      else {
         var pos = this.contains(element);
         if (pos > -1)
            this.value.splice(pos, 1);
      }
      this.addValue(element);
      return this;
   };

   /** @ignore */
   XmlElement.prototype.addValue = function(element) {
      if (element.constructor != this.constructor)
         throw Error("Invalid type for XmlElement addition");
      if (!this.value)
         this.value = [];
      this.value.push(element);
      return this;
   };

   /** @ignore */
   XmlElement.prototype.contains = function(element) {
      if (!this.value || !element)
         return -1;
      for (var i in this.value) {
         if (this.value[i].name == element.name)
            return i;
      }
      return -1;
   };

   /** @ignore */
   XmlElement.prototype.populate = function(data) {
      if (this.attributes) {
         var value;
         for (var i in this.attributes) {
            var attr = this.attributes[i];
            if (!attr.name)
               throw Error("Cannot populate unnamed attribute entry");
            if (data && data.attributes)
               value = data.attributes[attr.name];
            if (data && (data.value || data.attributes) && !value && attr.required) {
               throw Error('Missing required ' + (attr.type || Object).name + ' attribute "' +
                           attr.name + '" in element &lt;' + this.name + '&gt; (' + value + ")");
            }
            if (value && attr.type && attr.type != value.constructor) {
               throw Error('Type mismatch in attribute "' +
                           this.name + ":" + attr.name + '"');
            }
            if (value) {
               app.debug("populating attribute " + attr.name +
                         " with " + value.constructor.name + ": " + value.toSource());
            }
            if (!attr.readonly) {
               attr.value = getString(value, attr.format) || attr.value ;
            }
         }
      }

      if (data && data.value) // && data.value.constructor == Object)
         data = data.value;

      if (this.value && data) {
         for (var i in this.value) {
            var element = this.value[i];
            element.populate(data[element.name]);
         }
      } else {
         if (!data && this.required)
            throw Error('Missing required element "' + this.name + '"');
         if (data && this.type && this.type != data.constructor) {
            throw Error('Type mismatch in element "' + this.name + '"');
         }
         if (data) {
            app.debug("populating element &lt;" + this.name +  "&gt; with " +
                      (this.type || Object).name + ": " + data.toSource());
         }
         if (!this.readonly)
            this.value = getString(data, this.format) || this.value;
      }

     return;
   };

   /** @ignore */
   XmlElement.prototype.write = function(path) {
      if (!path)
         path = "";

      if (!this.value && !this.attributes)
         return;

      var attrBuffer = new java.lang.StringBuffer();
      if (this.attributes) {
         for (var a in this.attributes) {
            var attr = this.attributes[a];
            if (attr.value) {
               attrBuffer.append(" " + attr.name + '="');
               attrBuffer.append(attr.value);
               attrBuffer.append('"');
            }
         }
      }

      var attrSize = attrBuffer.length();
      if (!this.value && attrSize < 1)
         return;

      write("<" + this.name);
      if (attrSize > 0) {
         display = true;
         write(attrBuffer.toString());
      }
      if (this.value) {
         write(">");
         if (this.value && this.value.constructor == Array) {
            for (var i in this.value)
               this.value[i].write(path+"/"+this.name);
         } else
            write(this.value);
         write("</" + this.name + ">\n");
      } else
         write("/>\n");
      return;
   };

   /** @ignore */
   XmlElement.prototype.toString = function() {
      return "[XmlElement: " + this.toSource() + "]";
   };

   /**
    * Get a newly created XML element.
    * @param {Object} data The XML data as object tree.
    * @returns The resulting XML element.
    * @type jala.XmlWriter.XmlElement
    */
   this.createElement = function(data) {
      return new XmlElement(data);
   };

   /**
    * Get the root XML element of this writer.
    * @returns The root XML element.
    * @type jala.XmlWriter.XmlElement
    */
   this.getRoot = function() {
      return new XmlElement({});
   };

   /**
    * Extend a template object.
    * @param {Object} template The template object.
    * @param {Object} ext The extension object.
    * @returns The XML writer.
    * @type jala.XmlWriter
    */
   this.extend = function(template, ext) {
      if (ext.constructor == Object)
         ext = [ext];
      if (ext.constructor == Array) {
         for (var i in ext)
            template.value.push(ext[i]);
      }
      return this;
   };

   /**
    * Add a namespace to this writer.
    * @param {String} name The name of the namespace.
    * @param {String} url The URL string of the namespace.
    * @returns The XML root element.
    * @type jala.XmlWriter.XmlElement
    */
   this.addNamespace = function(name, url) {
      var ref = this.getRoot();
      ref.attributes.push({
         name: "xmlns:" + name,
         value: url
      });
      return ref;
   };

   /**
    * Write the XML to the response buffer.
    */
   this.write = function() {
      res.contentType = "text/xml";
      writeln(XMLHEADER);
      this.getRoot().write();
      return;
   };

   /**
    * Get the XML output as string.
    * @returns The XML output.
    * @type String
    */
   this.toString = function() {
      res.push();
      this.write();
      return res.pop();
   };

   /**
    * Clone this XML writer.
    * @param {Object} The clone templare.
    * @returns The cloned XML writer.
    * @type jala.XmlWriter
    */
   this.clone = function(obj) {
      if (!obj || typeof obj != "object")
         return obj;
      var copy = new obj.constructor;
      for (var i in obj) {
         if (obj[i].constructor == Object ||
             obj[i].constructor == Array)
            copy[i]= this.clone(obj[i]);
         else
            copy[i] = obj[i];
      }
      return copy;
   };

   return this;
};