/* | |
* Licensed to the Apache Software Foundation (ASF) under one | |
* or more contributor license agreements. See the NOTICE file | |
* distributed with this work for additional information | |
* regarding copyright ownership. The ASF licenses this file | |
* to you 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. | |
*/ | |
package org.apache.myfaces.push; | |
import java.beans.BeanInfo; | |
import java.beans.IntrospectionException; | |
import java.beans.Introspector; | |
import java.beans.PropertyDescriptor; | |
import java.lang.reflect.Array; | |
import java.text.ParseException; | |
import java.text.SimpleDateFormat; | |
import java.util.Collection; | |
import java.util.Date; | |
import java.util.Locale; | |
import java.util.Map; | |
import java.util.Map.Entry; | |
import java.util.TimeZone; | |
/** | |
* A simple JSON encoder. | |
* | |
* See org.omnifaces.util.Json version 1.2 file licensed under ASL v2.0 | |
* org.omnifaces.util.Utils | |
* Copyright 2016 OmniFaces and the original author or authors. | |
* | |
* @author Arjan Tijms | |
* @author Bauke Scholtz | |
*/ | |
public final class Json | |
{ | |
// Constants ------------------------------------------------------------------------------------------------------ | |
private static final String ERROR_INVALID_BEAN = "Cannot introspect object of type '%s' as bean."; | |
private static final String ERROR_INVALID_GETTER = "Cannot invoke getter of property '%s' of bean '%s'."; | |
// Constructors --------------------------------------------------------------------------------------------------- | |
private Json() | |
{ | |
// Hide constructor. | |
} | |
// Encode --------------------------------------------------------------------------------------------------------- | |
/** | |
* Encodes the given object as JSON. This supports the standard types {@link Boolean}, {@link Number}, | |
* {@link CharSequence} and {@link Date}. If the given object type does not match any of them, then it will attempt | |
* to inspect the object as a javabean whereby the public properties (with public getters) will be encoded as a JS | |
* object. It also supports {@link Collection}s, {@link Map}s and arrays of them, even nested ones. The {@link Date} | |
* is formatted in RFC 1123 format, so you can if necessary just pass it straight to <code>new Date()</code> in | |
* JavaScript. | |
* | |
* @param object The object to be encoded as JSON. | |
* @return The JSON-encoded representation of the given object. | |
* @throws IllegalArgumentException When the given object or one of its properties cannot be inspected as a bean. | |
*/ | |
public static String encode(Object object) | |
{ | |
StringBuilder builder = new StringBuilder(); | |
encode(object, builder); | |
return builder.toString(); | |
} | |
/** | |
* Method allowing tail recursion (prevents potential stack overflow on deeply nested structures). | |
*/ | |
private static void encode(Object object, StringBuilder builder) | |
{ | |
if (object == null) | |
{ | |
builder.append("null"); | |
} | |
else if (object instanceof Boolean || object instanceof Number) | |
{ | |
builder.append(object.toString()); | |
} | |
else if (object instanceof CharSequence) | |
{ | |
builder.append('"').append(escapeJS(object.toString(), false)).append('"'); | |
} | |
else if (object instanceof Date) | |
{ | |
builder.append('"').append(formatRFC1123((Date) object)).append('"'); | |
} | |
else if (object instanceof Collection<?>) | |
{ | |
encodeCollection((Collection<?>) object, builder); | |
} | |
else if (object.getClass().isArray()) | |
{ | |
encodeArray(object, builder); | |
} | |
else if (object instanceof Map<?, ?>) | |
{ | |
encodeMap((Map<?, ?>) object, builder); | |
} | |
else if (object instanceof Class<?>) | |
{ | |
encode(((Class<?>) object).getName(), builder); | |
} | |
else | |
{ | |
encodeBean(object, builder); | |
} | |
} | |
/** | |
* Encode a Java collection as JS array. | |
*/ | |
private static void encodeCollection(Collection<?> collection, StringBuilder builder) | |
{ | |
builder.append('['); | |
int i = 0; | |
for (Object element : collection) | |
{ | |
if (i++ > 0) | |
{ | |
builder.append(','); | |
} | |
encode(element, builder); | |
} | |
builder.append(']'); | |
} | |
/** | |
* Encode a Java array as JS array. | |
*/ | |
private static void encodeArray(Object array, StringBuilder builder) | |
{ | |
builder.append('['); | |
int length = Array.getLength(array); | |
for (int i = 0; i < length; i++) | |
{ | |
if (i > 0) | |
{ | |
builder.append(','); | |
} | |
encode(Array.get(array, i), builder); | |
} | |
builder.append(']'); | |
} | |
/** | |
* Encode a Java map as JS object. | |
*/ | |
private static void encodeMap(Map<?, ?> map, StringBuilder builder) | |
{ | |
builder.append('{'); | |
int i = 0; | |
for (Entry<?, ?> entry : map.entrySet()) | |
{ | |
if (i++ > 0) | |
{ | |
builder.append(','); | |
} | |
encode(String.valueOf(entry.getKey()), builder); | |
builder.append(':'); | |
encode(entry.getValue(), builder); | |
} | |
builder.append('}'); | |
} | |
/** | |
* Encode a Java bean as JS object. | |
*/ | |
private static void encodeBean(Object bean, StringBuilder builder) | |
{ | |
BeanInfo beanInfo; | |
try | |
{ | |
beanInfo = Introspector.getBeanInfo(bean.getClass()); | |
} | |
catch (IntrospectionException e) | |
{ | |
throw new IllegalArgumentException( | |
String.format(ERROR_INVALID_BEAN, bean.getClass()), e); | |
} | |
builder.append('{'); | |
int i = 0; | |
for (PropertyDescriptor property : beanInfo.getPropertyDescriptors()) | |
{ | |
if (property.getReadMethod() == null || "class".equals(property.getName())) | |
{ | |
continue; | |
} | |
Object value; | |
try | |
{ | |
value = property.getReadMethod().invoke(bean); | |
} | |
catch (Exception e) | |
{ | |
throw new IllegalArgumentException( | |
String.format(ERROR_INVALID_GETTER, property.getName(), bean.getClass()), e); | |
} | |
if (value == null) | |
{ | |
continue; | |
} | |
if (i++ > 0) | |
{ | |
builder.append(','); | |
} | |
encode(property.getName(), builder); | |
builder.append(':'); | |
encode(value, builder); | |
} | |
builder.append('}'); | |
} | |
// Escaping/unescaping -------------------------------------------------------------------------------------------- | |
private static final int UNICODE_3_BYTES = 0xfff; | |
private static final int UNICODE_2_BYTES = 0xff; | |
private static final int UNICODE_1_BYTE = 0xf; | |
private static final int UNICODE_END_PRINTABLE_ASCII = 0x7f; | |
private static final int UNICODE_BEGIN_PRINTABLE_ASCII = 0x20; | |
/** | |
* Escapes the given string according the JavaScript code rules. This escapes among others the special characters, | |
* the whitespace, the quotes and the unicode characters. Useful whenever you want to use a Java string variable as | |
* a JavaScript string variable. | |
* | |
* @param string The string to be escaped according the JavaScript code rules. | |
* @param escapeSingleQuote Whether to escape single quotes as well or not. Set to <code>false</code> if you want to | |
* escape it for usage in JSON. | |
* @return The escaped string according the JavaScript code rules. | |
*/ | |
public static String escapeJS(String string, boolean escapeSingleQuote) | |
{ | |
if (string == null) | |
{ | |
return null; | |
} | |
StringBuilder builder = new StringBuilder(string.length()); | |
for (char c : string.toCharArray()) | |
{ | |
if (c > UNICODE_3_BYTES) | |
{ | |
builder.append("\\u").append(Integer.toHexString(c)); | |
} | |
else if (c > UNICODE_2_BYTES) | |
{ | |
builder.append("\\u0").append(Integer.toHexString(c)); | |
} | |
else if (c > UNICODE_END_PRINTABLE_ASCII) | |
{ | |
builder.append("\\u00").append(Integer.toHexString(c)); | |
} | |
else if (c < UNICODE_BEGIN_PRINTABLE_ASCII) | |
{ | |
escapeJSControlCharacter(builder, c); | |
} | |
else | |
{ | |
escapeJSASCIICharacter(builder, c, escapeSingleQuote); | |
} | |
} | |
return builder.toString(); | |
} | |
private static void escapeJSControlCharacter(StringBuilder builder, char c) | |
{ | |
switch (c) | |
{ | |
case '\b': | |
builder.append('\\').append('b'); | |
break; | |
case '\n': | |
builder.append('\\').append('n'); | |
break; | |
case '\t': | |
builder.append('\\').append('t'); | |
break; | |
case '\f': | |
builder.append('\\').append('f'); | |
break; | |
case '\r': | |
builder.append('\\').append('r'); | |
break; | |
default: | |
if (c > UNICODE_1_BYTE) | |
{ | |
builder.append("\\u00").append(Integer.toHexString(c)); | |
} | |
else | |
{ | |
builder.append("\\u000").append(Integer.toHexString(c)); | |
} | |
break; | |
} | |
} | |
private static void escapeJSASCIICharacter(StringBuilder builder, char c, boolean escapeSingleQuote) | |
{ | |
switch (c) | |
{ | |
case '\'': | |
if (escapeSingleQuote) | |
{ | |
builder.append('\\'); | |
} | |
builder.append('\''); | |
break; | |
case '"': | |
builder.append('\\').append('"'); | |
break; | |
case '\\': | |
builder.append('\\').append('\\'); | |
break; | |
case '/': | |
builder.append('\\').append('/'); | |
break; | |
default: | |
builder.append(c); | |
break; | |
} | |
} | |
// Dates ---------------------------------------------------------------------------------------------------------- | |
private static final String PATTERN_RFC1123_DATE = "EEE, dd MMM yyyy HH:mm:ss zzz"; | |
private static final TimeZone TIMEZONE_GMT = TimeZone.getTimeZone("GMT"); | |
/** | |
* Formats the given {@link Date} to a string in RFC1123 format. This format is used in HTTP headers and in | |
* JavaScript <code>Date</code> constructor. | |
* | |
* @param date The <code>Date</code> to be formatted to a string in RFC1123 format. | |
* @return The formatted string. | |
* @since 1.2 | |
*/ | |
public static String formatRFC1123(Date date) | |
{ | |
SimpleDateFormat sdf = new SimpleDateFormat(PATTERN_RFC1123_DATE, Locale.US); | |
sdf.setTimeZone(TIMEZONE_GMT); | |
return sdf.format(date); | |
} | |
/** | |
* Parses the given string in RFC1123 format to a {@link Date} object. | |
* | |
* @param string The string in RFC1123 format to be parsed to a <code>Date</code> object. | |
* @return The parsed <code>Date</code>. | |
* @throws ParseException When the given string is not in RFC1123 format. | |
* @since 1.2 | |
*/ | |
public static Date parseRFC1123(String string) throws ParseException | |
{ | |
SimpleDateFormat sdf = new SimpleDateFormat(PATTERN_RFC1123_DATE, Locale.US); | |
return sdf.parse(string); | |
} | |
} |