/* * This software is distributed under the terms of the FSF * Gnu Lesser General Public License (see lgpl.txt). * * This program is distributed WITHOUT ANY WARRANTY. See the * GNU General Public License for more details. */ package com.scooterframework.web.util; import java.util.HashMap; import java.util.Map; import java.util.Stack; import com.scooterframework.common.util.Converters; import com.scooterframework.common.util.CurrentThreadCache; import com.scooterframework.common.util.WordUtil; import com.scooterframework.orm.activerecord.ActiveRecord; import com.scooterframework.orm.activerecord.ActiveRecordUtil; import com.scooterframework.orm.sqldataexpress.object.RESTified; /** * <p>F (FormHelper) class has helper methods for http form. </p> * * <p>In order to display validation messages for a form element, * <tt>formForOpen</tt>, <tt>formForClose</tt> and <tt>label</tt> methods * must be used together. If there is error for a field, the field's label * element will be highlighted. </p> * * <p><tt>formProperties</tt> is used to specify properties for * <tt>form</tt> tag, including AJAX properties.</p> * * <p>Ajax Examples:</p> * <pre> * //add a post and then refresh the posts list * formForOpen("posts", null, "data-ajax:true; data-target:#posts_list; data-handler:html") * result: <form action="/posts" class="add_post" data-ajax="true" method="POST" data-target="#posts_list" data-handler="html"> * * //edit a post with id 45 and then refresh the posts list * formForOpen("posts", post, "data-ajax:true; data-target:#posts_list; data-handler:html") * result: <form action="/posts/45" class="edit_post" data-ajax="true" method="POST" data-target="#posts_list" data-handler="html"> * </pre> * * @author (Fei) John Chen * */ public class F { private static final String CURRENT_FORM_OBJECT_STACK = "scooter.current.form.object.stack"; private static final String CURRENT_FORM_OBJECT_KEY_STACK = "scooter.current.form.object.key.stack"; private static RESTified getAndValidateObject(String resource, Object objectKey) { if (objectKey == null) return null; Object object = W.get(objectKey.toString()); if (object != null && !(object instanceof RESTified)) { throw new IllegalArgumentException("The object which maps to key \"" + objectKey + "\" for resource \"" + resource + "\" must be of RESTified type, but instead it is of \"" + object.getClass().getName() + "\" type."); } return (RESTified)object; } private static RESTified validateObject(String resource, Object object) { if (object == null) return null; if (object != null && !(object instanceof RESTified)) { throw new IllegalArgumentException("The object \"" + object + "\" for resource \"" + resource + "\" must be of RESTified type, but instead it is of \"" + object.getClass().getName() + "\" type."); } return (RESTified)object; } private static RESTified validateParentObject(String resource, Object object) { if (object == null) return null; if (object != null && !(object instanceof RESTified) && !(object instanceof String)) { throw new IllegalArgumentException("The object \"" + object + "\" for resource \"" + resource + "\" must be of either String or RESTified type, but instead it is of \"" + object.getClass().getName() + "\" type."); } return (RESTified)object; } private static Object[] validateParentObject(String[] parentResources, Object[] parentRestfuls) { if ((parentResources == null) || (parentRestfuls == null)) return null; if (parentResources.length != parentRestfuls.length) throw new IllegalArgumentException("The length of parent resources " + "must be equal to the length of the parent objects."); int length = parentResources.length; for (int i = 0; i < length; i++) { parentRestfuls[i] = validateParentObject(parentResources[i], parentRestfuls[i]); } return parentRestfuls; } /** * <p>Returns form-open element <tt><form></tt>for a resource. * The <tt>objectKey</tt> is used to retrieve the corresponding object for * the resource. The object must be of RESTified type. If there is no * object mapped to the key, or the object's restful id is null, the form * is for adding a new object. Otherwise it is for editing the object.</p> * * @param resourceName name of the resource * @param objectKey key pointing to the object * @return form-open element for a resource object */ public static String formForOpen(String resourceName, String objectKey) { RESTified object = getAndValidateObject(resourceName, objectKey); storeObjectToCurrentCache(object); storeObjectKeyToCurrentCache(objectKey); return R.formForResource(resourceName, object); } /** * <p>Returns form-open element <tt><form></tt>for a resource. * The <tt>objectKey</tt> is used to retrieve the corresponding object for * the resource. The object must be of RESTified type. If there is no * object mapped to the key, or the object's restful id is null, the form * is for adding a new object. Otherwise it is for editing the object.</p> * * <p><tt>formProperties</tt> refers to properties for * <tt><form></tt>. See top of this class for examples.</p> * * @param resourceName name of the resource * @param objectKey key pointing to the object * @param formProperties string of form related properties * @return form-open element for a resource object */ public static String formForOpen(String resourceName, String objectKey, String formProperties) { return insertFormProperties(formForOpen(resourceName, objectKey), formProperties); } /** * <p>Returns form-open element <tt><form></tt>for a resource. * If the object's restful id is null, the form is for adding a new object. * Otherwise it is for editing the object.</p> * * @param resourceName name of the resource * @param object an ActiveRecord object * @return form-open element for a resource object */ public static String formForOpen(String resourceName, ActiveRecord object) { RESTified record = validateObject(resourceName, object); storeObjectToCurrentCache(object); String objectKey = ActiveRecordUtil.getModelName(object); storeObjectKeyToCurrentCache(objectKey); return R.formForResource(resourceName, record); } /** * <p>Returns form-open element <tt><form></tt>for a resource. * If the object's restful id is null, the form is for adding a new object. * Otherwise it is for editing the object.</p> * * <p><tt>formProperties</tt> refers to properties for * <tt><form></tt>. See top of this class for examples.</p> * * @param resourceName name of the resource * @param object an ActiveRecord object * @param formProperties string of form related properties * @return form-open element for a resource object */ public static String formForOpen(String resourceName, ActiveRecord object, String formProperties) { return insertFormProperties(formForOpen(resourceName, object), formProperties); } /** * Returns form-end element for a resource. * * @param resourceName name of the resource * @return form-end element for a resource object */ public static String formForClose(String resourceName) { removeObjectFromCurrentCache(); removeObjectKeyFromCurrentCache(); return "</form>"; } /** * <p>Returns form-open element <tt><form></tt>for a nested resource. </p> * * <p>The <tt>parentObjectKey</tt> is used to retrieve the corresponding * object for the parent resource. The parent object must be of RESTified * type. </p> * * <p>The <tt>objectKey</tt> is used to retrieve the corresponding object for * the nested resource. The object must be of RESTified type. </p> * * <p>If there is no object mapped to the key, or the object's restful id is * null, the form is for adding a new object. Otherwise it is for editing * the object.</p> * * @param parentResourceName name of the parent resource * @param parentObjectKey key pointing to the parent object * @param resourceName name of the nested resource * @param objectKey key pointing to the object * @return form-open element for a nested resource object */ public static String formForOpen(String parentResourceName, String parentObjectKey, String resourceName, String objectKey) { RESTified parentRecord = getAndValidateObject(parentResourceName, parentObjectKey); RESTified object = getAndValidateObject(resourceName, objectKey); storeObjectToCurrentCache(object); storeObjectKeyToCurrentCache(objectKey); return R.formForNestedResourceRecord(parentResourceName, parentRecord, resourceName, object); } /** * <p>Returns form-open element <tt><form></tt>for a nested resource. </p> * * <p>The <tt>parentObjectKey</tt> is used to retrieve the corresponding * object for the parent resource. The parent object must be of RESTified * type. </p> * * <p>The <tt>objectKey</tt> is used to retrieve the corresponding object for * the nested resource. The object must be of RESTified type. </p> * * <p>If there is no object mapped to the key, or the object's restful id is * null, the form is for adding a new object. Otherwise it is for editing * the object.</p> * * <p><tt>formProperties</tt> refers to properties for * <tt><form></tt>. See top of this class for examples.</p> * * @param parentResourceName name of the parent resource * @param parentObjectKey key pointing to the parent object * @param resourceName name of the nested resource * @param objectKey key pointing to the object * @param formProperties string of form related properties * @return form-open element for a nested resource object */ public static String formForOpen(String parentResourceName, String parentObjectKey, String resourceName, String objectKey, String formProperties) { return insertFormProperties(formForOpen(parentResourceName, parentObjectKey, resourceName, objectKey), formProperties); } /** * <p>Returns form-open element <tt><form></tt>for a nested resource.</p> * * <p>The <tt>parentObjectKey</tt> is used to retrieve the corresponding * object for the parent resource. The parent object must be of RESTified type. </p> * * <p>If ActiveRecord object's restful id is null, the form is for adding a * new object. Otherwise it is for editing the object.</p> * * @param parentResourceName name of the parent resource * @param parentObjectKey key pointing to the parent object * @param resourceName name of the nested resource * @param object an ActiveRecord object * @return form-open element for a nested resource object */ public static String formForOpen(String parentResourceName, String parentObjectKey, String resourceName, ActiveRecord object) { RESTified parentRecord = getAndValidateObject(parentResourceName, parentObjectKey); storeObjectToCurrentCache(object); String objectKey = ActiveRecordUtil.getModelName(object); storeObjectKeyToCurrentCache(objectKey); return R.formForNestedResourceRecord(parentResourceName, parentRecord, resourceName, object); } /** * <p>Returns form-open element <tt><form></tt>for a nested resource.</p> * * <p>The <tt>parentObjectKey</tt> is used to retrieve the corresponding * object for the parent resource. The parent object must be of RESTified type. </p> * * <p>If ActiveRecord object's restful id is null, the form is for adding a * new object. Otherwise it is for editing the object.</p> * * <p><tt>formProperties</tt> refers to properties for * <tt><form></tt>. See top of this class for examples.</p> * * @param parentResourceName name of the parent resource * @param parentObjectKey key pointing to the parent object * @param resourceName name of the nested resource * @param object an ActiveRecord object * @param formProperties string of form related properties * @return form-open element for a nested resource object */ public static String formForOpen(String parentResourceName, String parentObjectKey, String resourceName, ActiveRecord object, String formProperties) { return insertFormProperties(formForOpen(parentResourceName, parentObjectKey, resourceName, object), formProperties); } /** * <p>Returns form-open element <tt><form></tt>for a nested resource. </p> * * <p>The <tt>objectKey</tt> is used to retrieve the corresponding object for * the nested resource. The object must be of RESTified type. </p> * * <p>If there is no object mapped to the key, or the object's restful id is * null, the form is for adding a new object. Otherwise it is for editing * the object.</p> * * @param parentResourceName name of the parent resource * @param parentObject the parent object * @param resourceName name of the nested resource * @param objectKey key pointing to the object * @return form-open element for a nested resource object */ public static String formForOpen(String parentResourceName, RESTified parentObject, String resourceName, String objectKey) { RESTified parentRecord = validateObject(parentResourceName, parentObject); RESTified object = getAndValidateObject(resourceName, objectKey); storeObjectToCurrentCache(object); storeObjectKeyToCurrentCache(objectKey); return R.formForNestedResourceRecord(parentResourceName, parentRecord, resourceName, object); } /** * <p>Returns form-open element <tt><form></tt>for a nested resource. </p> * * <p>The <tt>objectKey</tt> is used to retrieve the corresponding object for * the nested resource. The object must be of RESTified type. </p> * * <p>If there is no object mapped to the key, or the object's restful id is * null, the form is for adding a new object. Otherwise it is for editing * the object.</p> * * <p><tt>formProperties</tt> refers to properties for * <tt><form></tt>. See top of this class for examples.</p> * * @param parentResourceName name of the parent resource * @param parentObject the parent object * @param resourceName name of the nested resource * @param objectKey key pointing to the object * @param formProperties string of form related properties * @return form-open element for a nested resource object */ public static String formForOpen(String parentResourceName, RESTified parentObject, String resourceName, String objectKey, String formProperties) { return insertFormProperties(formForOpen(parentResourceName, parentObject, resourceName, objectKey), formProperties); } /** * <p>Returns form-open element <tt><form></tt>for a nested resource. </p> * * <p>If ActiveRecord object's restful id is null, the form is for adding a * new object. Otherwise it is for editing the object.</p> * * @param parentResourceName name of the parent resource * @param parentObject the parent object * @param resourceName name of the nested resource * @param object an ActiveRecord object * @return form-open element for a nested resource object */ public static String formForOpen(String parentResourceName, RESTified parentObject, String resourceName, ActiveRecord object) { RESTified parentRecord = validateObject(parentResourceName, parentObject); storeObjectToCurrentCache(object); String objectKey = ActiveRecordUtil.getModelName(object); storeObjectKeyToCurrentCache(objectKey); return R.formForNestedResourceRecord(parentResourceName, parentRecord, resourceName, object); } /** * <p>Returns form-open element <tt><form></tt>for a nested resource. </p> * * <p>If ActiveRecord object's restful id is null, the form is for adding a * new object. Otherwise it is for editing the object.</p> * * <p><tt>formProperties</tt> refers to properties for * <tt><form></tt>. See top of this class for examples.</p> * * @param parentResourceName name of the parent resource * @param parentObject the parent object * @param resourceName name of the nested resource * @param object an ActiveRecord object * @param formProperties string of form related properties * @return form-open element for a nested resource object */ public static String formForOpen(String parentResourceName, RESTified parentObject, String resourceName, ActiveRecord object, String formProperties) { return insertFormProperties(formForOpen(parentResourceName, parentObject, resourceName, object), formProperties); } /** * <p>Returns form-open element <tt><form></tt>for a nested resource.</p> * * <p><tt>parentResourceNames</tt> is an array of ancestors. The oldest is * at the beginning of the array. * <tt>parentRestfuls</tt> is an array of either restful id strings or * RESTified records of ancestors. </p> * * <p>The <tt>objectKey</tt> is used to retrieve the corresponding object for * the nested resource. The object must be of RESTified type. </p> * * <p>If there is no object mapped to the key, or the object's restful id is * null, the form is for adding a new object. Otherwise it is for editing * the object.</p> * * @param parentResourceNames names of the parent resources * @param parentRestfuls the parent restful ids or RESTified objects * @param resourceName name of the nested resource * @param objectKey key pointing to the object * @return form-open element for a nested resource object */ public static String formForOpen(String[] parentResourceNames, Object[] parentRestfuls, String resourceName, String objectKey) { Object[] parentRecords = validateParentObject(parentResourceNames, parentRestfuls); RESTified object = getAndValidateObject(resourceName, objectKey); storeObjectToCurrentCache(object); storeObjectKeyToCurrentCache(objectKey); return R.formForNestedResourceRecord(parentResourceNames, parentRecords, resourceName, object); } /** * <p>Returns form-open element <tt><form></tt>for a nested resource.</p> * * <p><tt>parentResourceNames</tt> is an array of ancestors. The oldest is * at the beginning of the array. * <tt>parentRestfuls</tt> is an array of either restful id strings or * RESTified records of ancestors. </p> * * <p>The <tt>objectKey</tt> is used to retrieve the corresponding object for * the nested resource. The object must be of RESTified type. </p> * * <p>If there is no object mapped to the key, or the object's restful id is * null, the form is for adding a new object. Otherwise it is for editing * the object.</p> * * <p><tt>formProperties</tt> refers to properties for * <tt><form></tt>. See top of this class for examples.</p> * * @param parentResourceNames names of the parent resources * @param parentRestfuls the parent restful ids or RESTified objects * @param resourceName name of the nested resource * @param objectKey key pointing to the object * @param formProperties string of form related properties * @return form-open element for a nested resource object */ public static String formForOpen(String[] parentResourceNames, Object[] parentRestfuls, String resourceName, String objectKey, String formProperties) { return insertFormProperties(formForOpen(parentResourceNames, parentRestfuls, resourceName, objectKey), formProperties); } /** * <p>Returns form-open element <tt><form></tt>for a nested resource.</p> * * <p><tt>parentResourceNames</tt> is an array of ancestors. The oldest is * at the beginning of the array. * <tt>parentRestfuls</tt> is an array of either restful id strings or * RESTified records of ancestors. </p> * * <p>If ActiveRecord object's restful id is null, the form is for adding a * new object. Otherwise it is for editing the object.</p> * * @param parentResourceNames names of the parent resources * @param parentRestfuls the parent restful ids or RESTified objects * @param resourceName name of the nested resource * @param object an ActiveRecord object * @return form-open element for a nested resource object */ public static String formForOpen(String[] parentResourceNames, Object[] parentRestfuls, String resourceName, ActiveRecord object) { Object[] parentRecords = validateParentObject(parentResourceNames, parentRestfuls); storeObjectToCurrentCache(object); String objectKey = ActiveRecordUtil.getModelName(object); storeObjectKeyToCurrentCache(objectKey); return R.formForNestedResourceRecord(parentResourceNames, parentRecords, resourceName, object); } /** * <p>Returns form-open element <tt><form></tt>for a nested resource.</p> * * <p><tt>parentResourceNames</tt> is an array of ancestors. The oldest is * at the beginning of the array. * <tt>parentRestfuls</tt> is an array of either restful id strings or * RESTified records of ancestors. </p> * * <p>If ActiveRecord object's restful id is null, the form is for adding a * new object. Otherwise it is for editing the object.</p> * * <p><tt>formProperties</tt> refers to properties for * <tt><form></tt>. See top of this class for examples.</p> * * @param parentResourceNames names of the parent resources * @param parentRestfuls the parent restful ids or RESTified objects * @param resourceName name of the nested resource * @param object an ActiveRecord object * @param formProperties string of form related properties * @return form-open element for a nested resource object */ public static String formForOpen(String[] parentResourceNames, Object[] parentRestfuls, String resourceName, ActiveRecord object, String formProperties) { return insertFormProperties(formForOpen(parentResourceNames, parentRestfuls, resourceName, object), formProperties); } /** * Returns label string for a field of the underlying object. * * This method should not be used without using <tt>formFor</tt> * method first. * * <pre> * Examples: * <label for="post_name" >Name</label> * <div class="fieldWithErrors"><label for="post_title" >Title</label></div><br /> * </pre> * @param field field name * @return error-aware label tag string */ public static String label(String field) { return label(field, null); } /** * Returns label string for a field of the underlying object. * * This method should not be used without using <tt>formFor</tt> * method first. * * <pre> * Examples: * <label for="post_name" >Name</label> * <div class="fieldWithErrors"><label for="post_title" >Title</label></div><br /> * </pre> * @param field field name * @param options options for the label tag * @return error-aware label tag string */ public static String label(String field, Map<String, String> options) { if (options == null) options = new HashMap<String, String>(); Object object = getObjectFromCurrentCache(); Object objectKey = getObjectKeyFromCurrentCache(); options.put("for", tagId(objectKey, field)); return W.taggedContent(object, field, "label", WordUtil.titleize(field), options); } private static String tagId(Object objectKey, String field) { return objectKey + "_" + field; } private static Stack<Object> getCurrentCacheObjectStack() { @SuppressWarnings("unchecked") Stack<Object> stk = (Stack<Object>)CurrentThreadCache.get(CURRENT_FORM_OBJECT_STACK); if (stk == null) { stk = new Stack<Object>(); CurrentThreadCache.set(CURRENT_FORM_OBJECT_STACK, stk); } return stk; } private static Object getObjectFromCurrentCache() { return getCurrentCacheObjectStack().peek(); } private static void storeObjectToCurrentCache(Object object) { getCurrentCacheObjectStack().push(object); } private static void removeObjectFromCurrentCache() { Stack<Object> stk = getCurrentCacheObjectStack(); stk.pop(); if (stk.empty()) { CurrentThreadCache.clear(CURRENT_FORM_OBJECT_STACK); } } private static Stack<Object> getCurrentCacheObjectKeyStack() { @SuppressWarnings("unchecked") Stack<Object> stk = (Stack<Object>)CurrentThreadCache.get(CURRENT_FORM_OBJECT_KEY_STACK); if (stk == null) { stk = new Stack<Object>(); CurrentThreadCache.set(CURRENT_FORM_OBJECT_KEY_STACK, stk); } return stk; } private static Object getObjectKeyFromCurrentCache() { return getCurrentCacheObjectKeyStack().peek(); } private static void storeObjectKeyToCurrentCache(Object objectKey) { getCurrentCacheObjectKeyStack().push(objectKey); } private static void removeObjectKeyFromCurrentCache() { Stack<Object> stk = getCurrentCacheObjectKeyStack(); stk.pop(); if (stk.empty()) { CurrentThreadCache.clear(CURRENT_FORM_OBJECT_KEY_STACK); } } private static String insertFormProperties(String formString, String formProperties) { if (formProperties == null || "".equals(formProperties)) return formString; String token = "<form "; if (formString == null || !formString.startsWith(token)) { throw new IllegalArgumentException("formString input must starts with \"<form \"."); } Map<String, String> properties = Converters.convertStringToMap(formProperties, ":", ";"); StringBuilder sb = new StringBuilder(); sb.append("<form "); for (Map.Entry<String, String> entry : properties.entrySet()) { sb.append(entry.getKey()).append("=\"").append(entry.getValue()).append("\"").append(" "); } sb.append(formString.substring(token.length())); return sb.toString(); } }