Rabbit 1.1

Global/Rabbit.js

Summary

Fields and methods of the Rabbit class.


Class Summary
Rabbit Rabbit is a framework for providing kind of rapid application prototyping features in Helma.

//
// Copyright (c) 2006 Tobi Schäfer
// Alle Rechte vorbehalten. All rights reserved.
//
// $Revision: 446 $
// $LastChangedBy: tobi $
// $LastChangedDate: 2007-02-15 20:59:15 +0100 (Thu, 15 Feb 2007) $
// $HeadURL: http://p3k.org/source/rabbit/trunk/docs/overview-summary-Global_Rabbit.js.html $
//

/**
 * @fileoverview Fields and methods of the Rabbit class.
 */

// Resolve dependencies
app.addRepository("lib/ext/hsqldb.jar");
app.addRepository("modules/core/Global");
app.addRepository("modules/core/String");

/**
 * Constructs a new instance of the Rabbit class.
 * @class Rabbit is a framework for providing kind of rapid application 
 * prototyping features in Helma.
 * @param {Boolean} restricted Flag indicating whether some global 
 * and prototype settings will be applied on instantation. If true, the
 * values as described in {@link #registerGlobals} will be set.
 * @constructor
 */
var Rabbit = function(restricted) {
   var self = this;
   var db, meta;
  
   var TYPE = "type.properties";
   var METAFILE = app.dir + "/.rabbit";
   var INFO = "log";

   var log = function(msg, level) {
      if (msg) {
         var lines = msg.split("\n"), m;
         for (var i in lines) {
            (m = lines[i]) && app[level || "debug"](m);
         }
      }
      return;
   };

   var sql = function(s) {
      log("Rabbit SQL: " + s);
      try {
         db.execute(s);
      } catch (e) {
         log(e.toString());
      }
      return;
   }

   var saveMeta = function() {
      if (meta) {
         serialize(meta, METAFILE);
      }
      return;
   };

   var loadMeta = function() {
      try {
         meta = deserialize(METAFILE);
      } catch (e) {
         meta = new Object;
      }
      return;
   };
   
   var createTable = function(name) {
      var column = getColumnName(name, "id");
      sql("create cached table " + getTableName(name) + " (" + column + " " + getSqlType(Rabbit.TEXT) + " not null)");
      return column;
   };
   
   var addColumn = function(prototype, property, type) {
      var column = getColumnName(prototype, property);
      sql("alter table " + getTableName(prototype) + " add column " + column + " " + getSqlType(type));
      return column;
   };

   var dropColumn = function(prototype, property) {
      sql("alter table " + getTableName(prototype) + " drop column " + getColumnName(prototype, property));
      return;
   };
   
   var getTableName = function(prototype) {
      return "T_" + prototype.toUpperCase();
   };
   
   var getColumnName = function(prototype, property) {
      return (prototype + "_" + property).toUpperCase();
   };
   
   var getSqlType = function(type) {
      switch (type) {
         case Rabbit.NUMBER:
         return "numeric";
         
         case Rabbit.DATE:
         return "timestamp";
         
         case Rabbit.FILE:
         case Rabbit.IMAGE:
         return "binary";
      }
      return "varchar";
   };

   var getDir = function(prototype) {
      return app.dir + "/" + prototype;
   };
   
   var getHeader = function(prefix) {
      return prefix + " generated by Rabbit " + Rabbit.VERSION + 
                      " on \n" + prefix + " " + new Date + "\n";
   };
   
   var getCollectionName = function(prototype) {
      var suffix = "s";
      if (prototype.endsWith(suffix)) {
         suffix = "e" + suffix;
      }
      return prototype.toLowerCase() + suffix;
   };
   
   var checkProperty = function(property) {
      return !property.startsWith("_") &&
             !property.startsWith("rabbit") &&
             !property.contains(".");
   };
   
   var checkValue = function(value) {
      return !/object ?\(/.test(value) && 
             !/collection ?\(/.test(value) &&
             !/mountpoint ?[(]/.test(value); // FIXME: variation of syntax to fix highlighting in TextMate
   };
   
   /**
    * Initializes the Rabbit framework. At this point a connection to the
    * HSQL database is made and the db.properties file is written enabling
    * read and write access to the database for the Helma application.
    * @returns An object used for executing a static SQL statement and 
    * returning the results it produces.
    * @type java.sql.Statement
    */
   this.init = function() {
      loadMeta();

      var driver = getProperty("rabbit.driver", "org.hsqldb.jdbcDriver")
      java.lang.Class.forName(driver).newInstance();

      var dir = new java.io.File(app.dir, "hsql.db/db");
      var database = getProperty(
         "rabbit.database", 
         "jdbc:hsqldb:file:" + dir.getCanonicalPath()
      );
      var user = getProperty("rabbit.user", "sa");
      var password = getProperty("rabbit.password", "");

      var config = new Object; 
      config[app.name + ".url"]        = database.replace(/\\/g, "/"); // FIXME: windows path separators are causing an exception 
      config[app.name + ".driver"]     = driver;
      config[app.name + ".user"]       = user;
      config[app.name + ".password"]   = password;
      this.writePropertiesFile(app.dir, "db.properties", config); 

      var connection = java.sql.DriverManager.getConnection(database, user, password);
      db = connection.createStatement();
      return db;
   };

   /**
    * Drops all previously created tables from the database and clear
    * the object cache of the Helma application.
    * @params {String} Arguments are optional with each argument 
    * referring to a prototype whose data will be removed from 
    * database and cache.
    */
   this.reset = function(/* [prototype1 [, prototype2 [, ...]]  */) {
      meta || self.init();
      var i, list;
      if (arguments.length > 0) {
         list = {};
         for (i=0; i<arguments.length; i+=1) {
            list[arguments[i]] = true;
         }
      } else {
         list = meta;
      }
      for (i in list) {
         sql("drop table " + getTableName(i) + " if exists cascade");
      }
      meta = {};
      saveMeta();
      app.clearCache();
      return;
   };

   /**
    * Defines a HopObject prototype and its database mapping. The desired
    * mapping is achieved by creating all necessary tables in the databse
    * as well as writing the corresponding type.properties files for each
    * prototype.
    * @param {String} prototype The name of the prototype.
    * @param {Object} map A map of key/value pairs defining all
    * database-related properties of the prototype's instances
    * optionally including
    * <ul>
    * <li>_db - the name of the database</li>
    * <li>_children - all default and additional named collections</li>
    * <li>_table - the name of the database table containing the data 
    * of the prototype's instances</li>
    * </ul>
    */
   this.setup = function(prototype, map) {
      meta || self.init();
      var name = prototype;

      // Do not create tables etc. for root
      if (name.toLowerCase() != "root") {
         var value;
         var current = self.readPropertiesFile(getDir(name), TYPE);
         for (var property in current) {
            value = map[property];
            if (checkProperty(property) && !value) {
               dropColumn(name, property);
            }
         }   
         if (!map._db) {
            map._db = app.name;
         }
         if (!map._table) {
            map._table = getTableName(name);
         }  
         map._id = createTable(name);
         map.rabbit = "mountpoint(XmlMap)";
         map.rabbit_xml = addColumn(name, "rabbit_xml", "longvarchar");
      }

      for (var property in map) {
         value = map[property];
         if (checkValue(value) && checkProperty(property)) {
            self.setMeta(name, property, value);
            map[property] = addColumn(name, property, value);
         } else if (property.endsWith(".accessname")) {
            // Determining prototype and table column from accessname setting
            var collection = property.split(".")[0];
            var parts = /collection\s*\(\s*([^)]+)\s*\)/.exec(map[collection]);
            var childProto = parts[1];
            if (childProto) {
               map[property] = getColumnName(childProto, value);
            }
         }
      }

      saveMeta();
      self.writePropertiesFile(getDir(name), TYPE, map);
      return;
   };
   
   /**
    * Scaffolding is a different approach of providing
    * viewer, editor and update methods by physically copying all
    * necessary code and skins into the prototype's directory.
   */ /* FIXME: To be done.
   this.scaffold = function(prototype) {
      var setup = self.getSetup(prototype);
      var name = prototype.name;
      var filename = name + ".rabbit.js";
      return;
   };
   */
   
   /**
    * For convenience, easier access and also better performance
    * all settings of a prototype are serialized to a file which serves
    * as object cache. This method retrieves a certain property of the 
    * desired prototype from cache.
    * @param {String} prototype The name of the prototype.
    * @param {String} property The name of the property.
    * @returns The value of the prototype's property or null.
    */
   this.getMeta = function(prototype, property) {
      meta || self.init();
      var p = meta[prototype];
      return p && p[property];
   };
   
   /**
    * For convenience, easier access and also better performance
    * all settings of a prototype are serialized to a file which serves
    * as object cache. This method sets the value of a property of the 
    * desired prototype in cache.
    * @param {String} prototype The name of the prototype.
    * @param {String} property The name of the property.
    * @param {Object} value The future value of the prototype's property.
   */
   this.setMeta = function(prototype, property, value) {
      meta || self.init();
      var p = meta[prototype];
      if (!p) {
         meta[prototype] = {};
      }
      meta[prototype][property] = value;
      return;
   }

   /**
    * Gets a map of key/value pairs defining the database mapping
    * of an arbitrary HopObject instance's prototype.
    * @param {HopObject} object The underlying HopObject
    * @returns The resulting map structure
    * @type Object
    */
   this.getSetup = function(object) {
      meta || self.init();
      return meta[object._prototype];
   };
   
   /**
    * Reads a Java properties file into a JavaScript map.
    * @param {String} dir The directory containing the properties file
    * @param {String} name The name of the properties file
    * @returns {Object} The resulting map structure.
    */
   this.readPropertiesFile = function(dir, name) {
      var data = {};
      var file = new File(dir, name);
      if (file.exists()) {
         var content = file.readAll();
         var line, pairs;
         var parts = content.split("\n");
         for (var i in parts) {
            line = parts[i].trim();
            if (line && !line.startsWith("#")) {
               pairs = line.split("=");
               data[pairs[0].trim()] = pairs[1].trim();
            }
         }
      }
      return data;
   };

   /**
    * Creates a Java properties file from a JavaScript map.
    * @param {String} dir The directory containing the properties file
    * @param {String} name The name of the properties file
    * @param {Object} data The underlying map structure.
    * @returns The contents of the Java properties file
    * @type String
    */
   this.writePropertiesFile = function(dir, name, data) {
      var content = new String;
      if (data) {
         for (var i in data) {
            content += i + " = " + data[i] + "\n";
         }
         var file = new File(dir, name);
         file.mkdir();
         file.remove();
         file.open();
         file.writeln(getHeader("##"));
         file.write(content);
         file.close();
      }
      return content;   
   };

   /**
    * Updates a HopObject instance from a map of key/value pairs 
    * derived from the corresponding Rabbit editor.
    * Only properties which previously were defined via the setup()
    * method will be updated.
    * @param {HopObject} The desired HopObject instance
    * @param {Object} The underlying map data
    */
   this.update = function(object, data) {
   	var setup = self.getSetup(object);
   	var property, value, type, fileName, fileType;
      for (property in setup) {
         value = data[property];
         type = self.getMeta(object._prototype, property);
         
         switch (type) {
            case Rabbit.DATE:
            var i, key, d = {};
            for (i in data) {
               if (i.startsWith(property + ".")) {
                  key = i.split(".")[1];
                  d[key] = data[i];
               }
            }
            value = new Date(d.y, d.M-1, d.d, d.H, d.m, d.s);
            break;
            
            case Rabbit.NUMBER:
            value = !isNaN(value) ? value : null;
            break;

            case Rabbit.FILE:
            case Rabbit.IMAGE:         
            if (value && value.content) {
               fileName = value.name;
               fileType = value.contentType;
               value = value.content;
            }
            break;
         }

         if ((value = object.onSetProperty(property, value)) !== null) {
            object[property] = value;
            if (fileType) {
               Rabbit.setPropertyMeta(object, property, "type", fileType);
            }
            if (fileName) {
               Rabbit.setPropertyMeta(object, property, "name", fileName);
            }
         }
      }
      return;
   };

   /**
    * Renders an editor for a HopObject instance. Each property 
    * will be represented with an appropriate form display 
    * according to its datatype.
    * @param {HopObject} object The underlying HopObject instance.
    */
   this.editor = function(object) {
      Rabbit.snippet("layout:header");
      var setup = self.getSetup(object);
      var property, value, type;
      for (property in setup) {
         value = object[property];
         type = self.getMeta(object._prototype, property);
         
         switch (type) {
            case Rabbit.DATE:
            if (value === null) {
               value = new Date;
            }
            value = Rabbit.dateEditor(property, value, new String);
            break;

            case Rabbit.FILE:
            case Rabbit.IMAGE:         
            value = Rabbit.snippet("html:upload", {
               name: property
            }, new String);
            break;

            case Rabbit.NUMBER:
            case Rabbit.TEXT:
            case Rabbit.EMAIL:
            value = Rabbit.snippet("html:textarea", {
               type: "text",
               name: property,
               value: (value === null) ? "" : value
            }, new String);

            default:
            if (type.constructor == Array) {
               value = Rabbit.chooser(property, setup[property], value, new String);
            }
         }

         Rabbit.snippet("layout:property", {
            name: property.titleize(),
            value: value
         }, "properties");
      }
      Rabbit.snippet("layout:editor");
      Rabbit.snippet("layout:footer");
      return;

   };

   /**
    * Renders a viewer for a HopObject instance. Each property 
    * will be represented with an appropriate display according
    * to its datatype.
    * @param {HopObject} object The underlying HopObject instance.
    */
   this.viewer = function(object) {
      Rabbit.snippet("layout:header");
      var setup = self.getSetup(object);
      var property, value, type;
      for (property in setup) {
         value = object[property];
         type = self.getMeta(object._prototype, property);
         switch (type) {
            case Rabbit.DATE:
            value = value.format(Rabbit.DATEFORMAT);
            break;

            case Rabbit.NUMBER:
            value = value.format("0.##");
            break;

            case Rabbit.FILE:
            case Rabbit.IMAGE:
            value = Rabbit.snippet("html:link", {
               href: object.href("." + property),
               text: Rabbit.getPropertyMeta(object, property, "name")
            }, new String);
            break;

            default:
            if (value === null) {
               value = "";
            }
         }

         Rabbit.snippet("layout:property", {
            name: property.titleize(),
            value: value
         }, "properties");
      }
      Rabbit.snippet("layout:viewer");
      Rabbit.snippet("layout:footer");
      return;
   };

   /**
    * Retrieves the child element of an HopObject instance.
    * Depending on the value of req.data.rabbit, an update 
    * of the child HopObject is carried out, additionally.
    * @param {String} child The name of the child element
    * @returns The resulting Hopobject instance
    * @type HopObject
    */
   this.getChildElement = function(child) {
      var prototype = global[child];
      if (prototype && prototype != Root) {
         var target = new prototype;
         if (req.data.rabbit) {
            self.update(target, req.data);
            this.add(target);
            res.redirect(target.href());
         }
         self.editor(target);
         return new HopObject;
      }
   };
   
   /**
    * Registers some global and HopObject-related fields
    * and methods for convenience. The following values
    * are defined:
    * <ul>
    * <li>global.EMAIL</li>
    * <li>global.DATE</li>
    * <li>global.FILE</li>
    * <li>global.IMAGE</li>
    * <li>global.NUMBER</li>
    * <li>global.TEXT</li>
    * <li>global.registerPrototype</li>
    * <li>global.render</li>
    * <li>global.resetPrototypes</li>
    * <li>HopObject.prototype.edit_action</li>
    * <li>HopObject.prototype.getChildElement</li>
    * <li>HopObject.prototype.main_action</li>
    * </ul>
    * Most of the time this is automatically done by calling
    * the Rabbit constructor without any arguments.
    */
   this.registerGlobals = function() {
      /*
       * Helper method to provide namespace-unintrusive actions
       * for adding and editing HopObject instances generated
       * by the Rabbit framework. Currently supported are:
       * <ul>
       * <li>+ProtoName - Add a new instance of the prototype ProtoName</li>
       * <li>.propName - Edit the property propName of this instance</li>
       * </ul>
       * @ignore
       */
      HopObject.prototype.getChildElement = function(obj) {
         if (obj.startsWith(" ")) {
            return self.getChildElement.call(this, obj.substring(1));
         }
         if (obj.startsWith(".")) {
            return Rabbit.property.call(this, obj.substring(1));
         }
         return this.get(obj);
      };

      HopObject.prototype.main_action = function() {
         // Only create viewers for custom prototypes
         if (this._prototype != "HopObject" && this != root) {
            self.viewer(this);
         }
         return; 
      };

      HopObject.prototype.edit_action = function() {
         if (req.data["rabbit"]) {
            self.update(this, req.data);
            res.redirect(this.href());
         }
         self.editor(this);
         return; 
      };

      global.resetPrototypes = self.reset;
      global.registerPrototype = self.setup;
      global.render = Rabbit.snippet;
      
      var statics = ["number", "text", "email", "date", "file", "image"];
      var i, s;
      for (i in statics) {
         s = statics[i].toUpperCase();
         global[s] = Rabbit[s];
      }
      return;
   };

   if (!restricted) {
      self.registerGlobals();
   }

   return this;
};

/** Constant representing the current version number @final @type Number */
Rabbit.VERSION = 1.1;

/** Constant representing the numeric datatype @final @type Number */
Rabbit.NUMBER  = 1;

/** Constant representing the alphanumeric datatype @final @type Number */
Rabbit.TEXT    = 2;

/** Constant representing the e-mail datatype @final @type Number */
Rabbit.EMAIL   = 3;

/** Constant representing the date datatype @final @type Number */
Rabbit.DATE    = 4;

/** Constant representing the file datatype @final @type Number */
Rabbit.FILE    = 5;

/** Constant representing the image datatype @final @type Number */
Rabbit.IMAGE   = 6;

/** Constant representing the format of the date datatype @final @type String */
Rabbit.DATEFORMAT = "d. MMM. yyyy, HH:mm:ss'h'";

/**
 * Renders a snippet. A snippet can be a skin or a "subskin",
 * depending on the name. A subskin is a part of a skin file
 * which is delimited by an HTML comment structure of the form
 * <code><pre><!snippet:nameOfTheSnippet></pre></code>
 * To enable a skin file containing snippets the file must end
 * with ".snippets.skin". The rendering mechanics comply to
 * Helma's generic skin features with the exception that an
 * additional third argument can be provided to determine a
 * snippet buffer. Snippet buffers are handled by
 * res.data.rendered, ie. the rendered result will be appended
 * to the contents of a named property in res.data.rendered.
 * @param {String} name The name of the snippet. If the name 
 * contains a colon (e.g. "main:header"), the Rabbit framework
 * tries to render the subskin "header" contained in the skin
 * file "main.snippets.skin". Otherwise, common rendering applies.
 * @param {Object} param A generic Helma macro paramater object.
 * @param {String} buffer The name of a snippet buffer. This name
 * will be used to determine a named property in res.data.rendered.
 * E.g. "header" relates to res.data.rendered.header where any
 * additionally rendered snippet using the "header" buffer will 
 * be appended to. 
 * Since snippet buffers reside in res.handlers.rendered, one can
 * refer to them from within skin files via the "rendered" macro,
 * e.g.
 * <code><pre><% rendered.header %></pre></code>
 */
Rabbit.snippet = function(name, param, buffer) {
   var append = function(str) {
      // Do not create a handler for empty String objects
      if (buffer.length === 0) {
         return str;
      }
      if (!res.handlers.rendered) {
         res.handlers.rendered = new Object;
      }
      if (!res.handlers.rendered[buffer]) {
         res.handlers.rendered[buffer] = "";
      }
      return res.handlers.rendered[buffer] += str;
   };

   var DELIMITER = ":";
   var prefix = "Rabbit snippet: ";

   buffer && res.push();
   if (!name.contains(DELIMITER)) {
      app.debug("Rabbit skin: " + name);
      renderSkin(name, param);
   }

   var cache;
   if (!(cache = res.meta["rabbit:snippets"])) {
      cache = res.meta["rabbit:snippets"] = new Object;
   }
   if (cache[name]) {
      app.debug(prefix + name + " (cached)");
      renderSkin(cache[name], param);
      return buffer && append(res.pop());
   }

   var NAME = "snippet";
   var EXTENSION = "." + NAME + "s";
   var PREFIX = "<!" + NAME + DELIMITER;

   var parts = name.split(DELIMITER);
   var skin = app.skinfiles.Global["rabbit." + parts[0] + EXTENSION];
   var snippetName = parts[1];

   if (skin) {
      if (snippetName) {
         var needle = PREFIX + snippetName;
         var offset = skin.indexOf(needle);
         if (offset < 0) {
            return;
         }
         var start = offset + needle.length + 1;
         var end = skin.indexOf(PREFIX, start);
         if (end < 0) {
            end = skin.length;
         }
         var snippet = skin.substring(start, end);
         if (snippet) {
            app.debug(prefix + name);
            cache[name] = createSkin(snippet.trim());
            renderSkin(cache[name], param);
         }
      }
   }

   return buffer && append(res.pop());
};

/**
 * Helper method to render a property of a HopObject instance.
 * Depending on the content type the property will be output
 * as inline display or as downloadable binary.
 * @param {String} property The name of the property.
 * @returns A new HopObject
 * @type HopObject
 */
Rabbit.property = function(property) {
   var content = this._prototype && this[property];
   if (content) {
      res.contentType = Rabbit.getPropertyMeta(this, property, "type");
      res.servletResponse.setContentLength(content.length);
      var parts = res.contentType && res.contentType.split("/");
      if (parts[0].startsWith("application")) {
         res.servletResponse.setHeader(
            "Content-disposition", 
            "attachment; filename=" + Rabbit.getPropertyMeta(this, property, "name")
         );
      }
      res.writeBinary(content);
   }
   return new HopObject;
};

/**
 * Helper method to retrieve property meta data.
 * @param {HopObject} object The HopObject containing the underlying property
 * @param {String} property The name of the underlying property
 * @param {String} name The key of the desired meta value
 * @returns The resulting meta value
 * @type Object
 */
Rabbit.getPropertyMeta = function(object, property, name) {
   return object.rabbit.get(property + ":" + name);
};

/**
 * Helper method to set property meta data.
 * @param {HopObject} object The HopObject containing the underlying property
 * @param {String} property The name of the underlying property
 * @param {String} name The key of the desired meta value
 * @param {Object} value The property's future meta value
 */
Rabbit.setPropertyMeta = function(object, property, name, value) {
   object.rabbit.set(property + ":" + name, value);
   return;
};

/**
 * Helper method to render an HTML drop down element.
 * @param {String} name The name of the element.
 * @param {Object} options A map of key/value pairs containing
 * the names and values of each option.
 * @param {String} selected The currently selected value.
 * @param {String} The name of a snippet buffer (see {@link #snippet}
 * method for details)
 * @returns The rendered HTML code.
 * @type String
 */
Rabbit.chooser = function(name, options, selected, buffer) {
   if (typeof options != "object") {
      return;
   }
   res.push();
   var value;
   for (var i in options) {
      value = options[i];
      Rabbit.snippet("html:option", {
         name: i,
         text: value,
         selected: (selected && value == selected) ? "selected" : ""
      });
   }
   return Rabbit.snippet("html:chooser", {
      name: name,
      options: res.pop()
   }, buffer);
};

/**
 * Helper method to render a combination of HTML drop down elements
 * representing an editor for a datetime value.
 * @param {String} name The name of the element.
 * @param {Date} date The underlying date object.
 * @param {String} The name of a snippet buffer (see {@link #snippet}
 * method for details)
 * @returns The rendered HTML code.
 * @type String
 */
Rabbit.dateEditor = function(name, date, buffer) {
   function range(start, stop, step) {
      var result = [];
      if (!step) {
         step = 1;
         if (!stop) {
            stop = start - 1;
            start = 0;
         }
      }
      var max = String(stop).length;
      for (var i=start; i<=stop; i+=step) {
         result.push(String(i).pad("0", max, String.LEFT));
      }
      return result;
   }

   buffer && res.push();
   Rabbit.chooser(name + ".d", range(1, 31), date.getDate());
   Rabbit.chooser(name + ".M", range(1, 12), date.getMonth() + 1);
   Rabbit.chooser(name + ".y", range(1900, 2099), date.getFullYear());
   Rabbit.chooser(name + ".H", range(0, 23), date.getHours());
   var fifty9 = range(0, 59);
   Rabbit.chooser(name + ".m", fifty9, date.getMinutes());
   Rabbit.chooser(name + ".s", fifty9, date.getSeconds());
   return buffer && res.pop();
};

Rabbit 1.1

Documentation generated by JSDoc on Thu Feb 15 20:58:41 2007