/* * Copyright 2017 OmniFaces * * 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. */ package org.omnifaces.util; import static java.lang.String.format; import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.reflect.Array; import java.util.Collection; import java.util.Date; import java.util.Map; import java.util.Map.Entry; /** * A simple JSON encoder. * * @author Bauke Scholtz * @since 1.2 */ 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(Utils.escapeJS(object.toString(), false)).append('"'); } else if (object instanceof Date) { builder.append('"').append(Utils.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( 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( format(ERROR_INVALID_GETTER, property.getName(), bean.getClass()), e); } if (value != null) { if (i++ > 0) { builder.append(','); } encode(property.getName(), builder); builder.append(':'); encode(value, builder); } } builder.append('}'); } }