001/**
002 * Copyright 2005-2015 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.krad.uif.util;
017
018import org.apache.commons.lang.StringEscapeUtils;
019import org.apache.commons.lang.StringUtils;
020import org.apache.commons.lang.math.NumberUtils;
021import org.kuali.rice.core.api.util.type.TypeUtils;
022import org.springframework.util.Assert;
023
024import java.beans.PropertyDescriptor;
025import java.util.ArrayList;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029
030/**
031 * Utility class for generating JavaScript
032 * 
033 * @author Kuali Rice Team (rice.collab@kuali.org)
034 */
035public class ScriptUtils {
036
037    /**
038     * Translates an Object to a String for representing the given Object as a JavaScript value
039     * 
040     * <p>
041     * Handles null, List, Map, and Set collections, along with non quoting for numeric and boolean
042     * types. Complex types are treated as a String value using toString
043     * </p>
044     * 
045     * @param value Object instance to translate
046     * @return JS value
047     */
048    public static String translateValue(Object value) {
049        String jsValue = "";
050
051        if (value == null) {
052            jsValue = "null";
053            return jsValue;
054        }
055
056        if (value instanceof List) {
057            jsValue = "[";
058
059            List<Object> list = (List<Object>) value;
060            for (Object listItem : list) {
061                jsValue += translateValue(listItem);
062                jsValue += ",";
063            }
064            jsValue = StringUtils.removeEnd(jsValue, ",");
065
066            jsValue += "]";
067        } else if (value instanceof Set) {
068            jsValue = "[";
069
070            Set<Object> set = (Set<Object>) value;
071            for (Object setItem : set) {
072                jsValue += translateValue(setItem);
073                jsValue += ",";
074            }
075            jsValue = StringUtils.removeEnd(jsValue, ",");
076
077            jsValue += "]";
078        } else if (value instanceof Map) {
079            jsValue = "{";
080
081            Map<Object, Object> map = (Map<Object, Object>) value;
082            for (Map.Entry<Object, Object> mapEntry : map.entrySet()) {
083                jsValue += "\"" + mapEntry.getKey().toString() + "\":";
084                jsValue += translateValue(mapEntry.getValue());
085                jsValue += ",";
086            }
087            jsValue = StringUtils.removeEnd(jsValue, ",");
088
089            jsValue += "}";
090        } else {
091            Class<?> valueClass = value.getClass();
092            if (TypeUtils.isSimpleType(valueClass) || TypeUtils.isClassClass(valueClass)) {
093                boolean quoteValue = true;
094
095                if (TypeUtils.isBooleanClass(valueClass) ||
096                        TypeUtils.isDecimalClass(valueClass) ||
097                        TypeUtils.isIntegralClass(valueClass)) {
098                    quoteValue = false;
099                }
100
101                if (quoteValue) {
102                    jsValue = "\"";
103                }
104
105                jsValue += value.toString();
106
107                if (quoteValue) {
108                    jsValue += "\"";
109                }
110            } else {
111                // treat as data object
112                jsValue = "{";
113                Map<String, PropertyDescriptor> propertyDescriptors = ObjectPropertyUtils
114                        .getPropertyDescriptors(valueClass);
115                for (String propertyName : propertyDescriptors.keySet()) {
116                    if (ObjectPropertyUtils.isReadableProperty(value, propertyName)
117                            && !"class".equals(propertyName)) {
118                        Object propertyValue = ObjectPropertyUtils.getPropertyValueAsText(value, propertyName);
119                        jsValue += propertyName + ":";
120                        jsValue += translateValue(propertyValue);
121                        jsValue += ",";
122                    }
123                }
124                jsValue = StringUtils.removeEnd(jsValue, ",");
125
126                jsValue += "}";
127            }
128        }
129
130        return jsValue;
131    }
132
133    /**
134     * Builds a JSON string form the given map
135     * 
136     * @param map map to translate
137     * @return String in JSON format
138     */
139    public static String toJSON(Map<String, String> map) {
140        StringBuffer sb = new StringBuffer("{");
141
142        for (String key : map.keySet()) {
143            String optionValue = map.get(key);
144            if (sb.length() > 1) {
145                sb.append(",");
146            }
147            sb.append("\"" + key + "\"");
148
149            sb.append(":");
150            sb.append("\"" + escapeJSONString(optionValue) + "\"");
151        }
152        sb.append("}");
153
154        return sb.toString();
155    }
156
157    /**
158     * Escapes double quotes present in the given string
159     * 
160     * @param jsonString string to escape
161     * @return escaped string
162     */
163    public static String escapeJSONString(String jsonString) {
164        if (jsonString != null) {
165            jsonString = jsonString.replace("\"", "&quot;");
166        }
167
168        return jsonString;
169    }
170
171    /**
172     * Converts a map of string values to a json equivalent by converting the string values through
173     * the convertToJsValue(String) method; this will output a string representation of the map in
174     * json format
175     * 
176     * @param stringMap the map of String values to convert to a simple json object representation
177     * @return the json object as a string (for use in js)
178     */
179    public static String convertToJsValue(Map<String, String> stringMap) {
180        if (stringMap == null || stringMap.isEmpty()) {
181            return "{}";
182        }
183
184        String convertedValue = "{";
185        for (String key : stringMap.keySet()) {
186            convertedValue = convertedValue + "\"" + key + "\":" + convertToJsValue(stringMap.get(key)) + ",";
187        }
188
189        convertedValue = StringUtils.removeEnd(convertedValue, ",");
190        convertedValue = convertedValue + "}";
191
192        return convertedValue;
193    }
194
195    /**
196     * Convert a string to a javascript value - especially for use for options used to initialize
197     * widgets such as the tree and rich table
198     * 
199     * @param value the string to be converted
200     * @return the converted value
201     */
202    public static String convertToJsValue(String value) {
203
204        // save input value to preserve any whitespace formatting
205        String originalValue = value;
206
207        // remove whitespace for correct string matching
208        value = StringUtils.strip(value);
209
210        // If an option value starts with { or [, it would be a nested value
211        // and it should not use quotes around it
212        if (StringUtils.startsWith(value, "{") || StringUtils.startsWith(value, "[")) {
213            return originalValue;
214        }
215        // need to be the base boolean value "false" is true in js - a non
216        // empty string
217        else if (value.equalsIgnoreCase("false") || value.equalsIgnoreCase("true")) {
218            return originalValue.toLowerCase();
219        }
220        // if it is a call back function, do not add the quotes
221        else if (StringUtils.startsWith(value, "function") && StringUtils.endsWith(value, "}")) {
222            return originalValue;
223        }
224        // for numerics
225        else if (NumberUtils.isNumber(value)) {
226            return originalValue;
227        } else {
228            // String values require double quotes
229            return "\"" + originalValue + "\"";
230        }
231    }
232
233    /**
234     * Escapes the ' character present in collection names so it can be properly used in js without
235     * causing javascript errors due to an early completion of a ' string.
236     * 
237     * @param name name to escape
238     * @return name, with single quotes escape
239     */
240    public static String escapeName(String name) {
241        name = name.replace("'", "\\'");
242        return name;
243    }
244
245    /**
246     * Converts a list of string to a valid js string array
247     * 
248     * @param list list of Strings to be converted
249     * @return String representing the js array
250     */
251    public static String convertStringListToJsArray(List<String> list) {
252        String array = "[";
253
254        if (list != null) {
255            for (String s : list) {
256                array = array + "'" + s + "',";
257            }
258            array = StringUtils.removeEnd(array, ",");
259        }
260        array = array + "]";
261
262        return array;
263    }
264
265    /**
266     * escapes a string using {@link org.apache.commons.lang.StringEscapeUtils#escapeHtml(String)}
267     * 
268     * <p>
269     * The apostrophe character is included as <code>StringEscapeUtils#escapeHtml(String)</code>
270     * does not consider it a legal entity.
271     * </p>
272     * 
273     * @param string the string to be escaped
274     * @return the escaped string - useful for embedding in server side generated JS scripts
275     */
276    public static String escapeHtml(String string) {
277        if (string == null) {
278            return null;
279        } else {
280            return StringEscapeUtils.escapeHtml(string).replace("'", "&apos;").replace("&quot;", "\\u0022");
281        }
282    }
283
284    /**
285     * escape an array of strings
286     * 
287     * @param strings an array of strings to escape
288     * @return the array, with the strings escaped
289     */
290    public static List<String> escapeHtml(List<String> strings) {
291        if (strings == null) {
292            return null;
293        } else if (strings.isEmpty()) {
294            return strings;
295        } else {
296            List<String> result = new ArrayList<String>(strings.size());
297            for (String string : strings) {
298                result.add(escapeHtml(string));
299            }
300            return result;
301        }
302    }
303
304    /**
305     * Will append the second script parameter to the first if the first is not empty, also checks
306     * to see if the first script needs an end semi-colon added
307     * 
308     * @param script script that will be added to (null is allowed and converted to empty string)
309     * @param appendScript script to append
310     * @return String result of appending the two script parameters
311     */
312    public static String appendScript(String script, String appendScript) {
313        String appendedScript = script;
314        if (appendedScript == null) {
315            appendedScript = "";
316        } else if (StringUtils.isNotBlank(appendedScript) && !appendedScript.trim().endsWith(";")) {
317            appendedScript += "; ";
318        }
319
320        appendedScript += appendScript;
321
322        return appendedScript;
323    }
324
325    /**
326     * Helper method to build a JS string that will invoke the given function with the given
327     * arguments
328     * 
329     * @param functionName name of the JS function to invoke
330     * @param arguments zero or more arguments to pass, each will be converted to the corresponding
331     *        JS type
332     * @return JS String for invoking the function
333     */
334    public static String buildFunctionCall(String functionName, Object... arguments) {
335        StringBuffer sb = new StringBuffer(functionName).append("(");
336
337        if (arguments != null) {
338            List<String> jsArguments = new ArrayList<String>();
339            for (Object argument : arguments) {
340                jsArguments.add(translateValue(argument));
341            }
342
343            sb.append(StringUtils.join(jsArguments, ","));
344        }
345
346        sb.append(");");
347
348        return sb.toString();
349    }
350
351    /**
352     * Builds the JavaScript string for binding the given script to the component with the given id
353     * for the given event name (using jQuery)
354     * 
355     * @param id id of the element to handle the event for
356     * @param eventName name of the event the script will handle
357     * @param eventScript script to be executed when the event is thrown, if blank an empty string
358     *        will be returned
359     * @return JS event handler script
360     */
361    public static String buildEventHandlerScript(String id, String eventName, String eventScript) {
362        if (StringUtils.isBlank(eventScript)) {
363            return "";
364        }
365
366        Assert.hasLength(id, "Id is required for building event handler script");
367        Assert.hasLength(eventName, "Event name is required for building event handler script");
368
369        StringBuffer sb = new StringBuffer();
370
371        sb.append("jQuery('#" + id + "').on('");
372        sb.append(eventName);
373        sb.append("', function(e) {");
374        sb.append(eventScript);
375        sb.append("}); ");
376
377        return sb.toString();
378    }
379}