001/* 002 * This file is part of the JDrupes JSON utilities project. 003 * Copyright (C) 2017, 2018 Michael N. Lipp 004 * 005 * This program is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU Lesser General Public License as published 007 * by the Free Software Foundation; either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, but 011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 013 * License for more details. 014 * 015 * You should have received a copy of the GNU Lesser General Public License along 016 * with this program; if not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jdrupes.json; 020 021import com.fasterxml.jackson.core.JsonGenerator; 022 023import java.beans.BeanInfo; 024import java.beans.PropertyDescriptor; 025import java.beans.PropertyEditor; 026import java.beans.Transient; 027import java.io.Closeable; 028import java.io.Flushable; 029import java.io.IOException; 030import java.io.StringWriter; 031import java.io.Writer; 032import java.lang.reflect.Array; 033import java.lang.reflect.InvocationTargetException; 034import java.lang.reflect.Method; 035import java.math.BigDecimal; 036import java.math.BigInteger; 037import java.util.Collection; 038import java.util.HashMap; 039import java.util.HashSet; 040import java.util.Map; 041import java.util.Set; 042 043/** 044 * Encoder for converting a Java object graph to JSON. Objects may be arrays, 045 * collections, maps and JavaBeans. 046 * 047 * Arrays and collections are converted to JSON arrays. The 048 * type information is lost. Maps and JavaBeans are converted 049 * to JSON objects. 050 * 051 * The generated JSON objects can have an additional key/value pair with 052 * key "class" and a class name. The class information is generated 053 * only if it is needed, i.e. if it cannot be derived from the containing 054 * object. 055 * 056 * Given the following classes: 057 * 058 * ```java 059 * public static class Person { 060 * 061 * private String name; 062 * private int age; 063 * private PhoneNumber[] numbers; 064 * 065 * public String getName() { 066 * return name; 067 * } 068 * 069 * public void setName(String name) { 070 * this.name = name; 071 * } 072 * 073 * public int getAge() { 074 * return age; 075 * } 076 * 077 * public void setAge(int age) { 078 * this.age = age; 079 * } 080 * 081 * public PhoneNumber[] getNumbers() { 082 * return numbers; 083 * } 084 * 085 * public void setNumbers(PhoneNumber[] numbers) { 086 * this.numbers = numbers; 087 * } 088 * } 089 * 090 * public static class PhoneNumber { 091 * private String name; 092 * private String number; 093 * 094 * public PhoneNumber() { 095 * } 096 * 097 * public String getName() { 098 * return name; 099 * } 100 * 101 * public void setName(String name) { 102 * this.name = name; 103 * } 104 * 105 * public String getNumber() { 106 * return number; 107 * } 108 * 109 * public void setNumber(String number) { 110 * this.number = number; 111 * } 112 * } 113 * 114 * public static class SpecialNumber extends PhoneNumber { 115 * } 116 * ``` 117 * 118 * A serialization result may look like this: 119 * 120 * ```json 121 * { 122 * "age": 42, 123 * "name": "Simon Sample", 124 * "numbers": [ 125 * { 126 * "name": "Home", 127 * "number": "06751 51 56 57" 128 * }, 129 * { 130 * "class": "test.json.SpecialNumber", 131 * "name": "Work", 132 * "number": "030 77 35 44" 133 * } 134 * ] 135 * } 136 * ``` 137 * 138 * 139 */ 140public class JsonBeanEncoder extends JsonCodec 141 implements Flushable, Closeable { 142 143 private static final Set<String> EXCLUDED_DEFAULT = new HashSet<>(); 144 145 static { 146 // See https://issues.apache.org/jira/browse/GROOVY-8284 147 EXCLUDED_DEFAULT.add("groovy.lang.MetaClass"); 148 } 149 150 private Map<Class<?>, String> aliases = new HashMap<>(); 151 private Set<String> excluded = EXCLUDED_DEFAULT; 152 private boolean omitClass; 153 private JsonGenerator gen; 154 private StringWriter writer = null; 155 156 @Override 157 public JsonBeanEncoder addAlias(Class<?> clazz, String alias) { 158 aliases.put(clazz, alias); 159 return this; 160 } 161 162 /** 163 * Configure the encoder to not generate the `class` information 164 * even when needed to properly restore the Object graph. 165 * 166 * While this contradicts the initial objective to provide JSON 167 * persistence for JavaBeans, this is a valid option if the generated 168 * JSON is used for transferring information to an environment where 169 * the information provided by `class` isn't useful. 170 * 171 * @return the encoder for easy chaining 172 */ 173 public JsonBeanEncoder omitClass() { 174 omitClass = true; 175 return this; 176 } 177 178 /** 179 * Add a type to excude from encoding, usually because it cannot 180 * be converted to JSON. Properties of such types should be 181 * marked as {@link Transient}. However, sometimes base types 182 * don't follow the rules. 183 * 184 * @param className 185 * @return the encoder for easy chaining 186 */ 187 public JsonBeanEncoder addExcluded(String className) { 188 if (excluded == EXCLUDED_DEFAULT) { 189 excluded = new HashSet<>(EXCLUDED_DEFAULT); 190 } 191 excluded.add(className); 192 return this; 193 } 194 195 /** 196 * Create a new encoder using a default {@link JsonGenerator}. 197 * 198 * @param out the sink 199 * @return the encoder 200 */ 201 public static JsonBeanEncoder create(Writer out) { 202 try { 203 return new JsonBeanEncoder(defaultFactory().createGenerator(out)); 204 } catch (IOException e) { 205 throw new IllegalArgumentException(); 206 } 207 } 208 209 /** 210 * Create a new encoder using a default {@link JsonGenerator} 211 * that writes to an internally created {@link StringWriter}. 212 * The result can be obtained by invoking {@link #toJson()}. 213 * 214 * @return the encoder 215 */ 216 public static JsonBeanEncoder create() { 217 return new JsonBeanEncoder(); 218 } 219 220 /** 221 * Create a new encoder using the given {@link JsonGenerator}. 222 * 223 * @param generator the generator 224 * @return the encoder 225 */ 226 public static JsonBeanEncoder create(JsonGenerator generator) { 227 return new JsonBeanEncoder(generator); 228 } 229 230 private JsonBeanEncoder() { 231 writer = new StringWriter(); 232 try { 233 gen = defaultFactory().createGenerator(writer); 234 } catch (IOException e) { 235 throw new IllegalArgumentException(); 236 } 237 } 238 239 private JsonBeanEncoder(JsonGenerator generator) { 240 gen = generator; 241 } 242 243 @Override 244 public void flush() throws IOException { 245 gen.flush(); 246 } 247 248 @Override 249 public void close() throws IOException { 250 gen.close(); 251 } 252 253 /** 254 * Returns the text written to the output. Can only be used 255 * if the encoder has been created with {@link #JsonBeanEncoder()}. 256 * 257 * @return the result 258 */ 259 public String toJson() { 260 if (writer == null) { 261 throw new IllegalStateException( 262 "JsonBeanEncoder has been created without a known writer."); 263 } 264 try { 265 gen.flush(); 266 } catch (IOException e) { 267 throw new IllegalStateException(e); 268 } 269 return writer.toString(); 270 } 271 272 public JsonBeanEncoder writeArray(Object... items) throws IOException { 273 doWriteObject(items, items.getClass()); 274 return this; 275 } 276 277 public JsonBeanEncoder writeObject(Object obj) throws IOException { 278 doWriteObject(obj, obj.getClass()); 279 return this; 280 } 281 282 private void doWriteObject(Object obj, Class<?> expectedType) 283 throws IOException { 284 if (obj == null) { 285 gen.writeNull(); 286 return; 287 } 288 if (obj instanceof Boolean) { 289 gen.writeBoolean((Boolean) obj); 290 return; 291 } 292 if (obj instanceof Byte) { 293 gen.writeNumber(((Byte) obj).intValue()); 294 return; 295 } 296 if (obj instanceof Number) { 297 if (obj instanceof Short) { 298 gen.writeNumber((Short) obj); 299 return; 300 } 301 if (obj instanceof Integer) { 302 gen.writeNumber((Integer) obj); 303 return; 304 } 305 if (obj instanceof Long) { 306 gen.writeNumber((Long) obj); 307 return; 308 } 309 if (obj instanceof BigInteger) { 310 gen.writeNumber((BigInteger) obj); 311 return; 312 } 313 if (obj instanceof BigDecimal) { 314 gen.writeNumber((BigDecimal) obj); 315 return; 316 } 317 gen.writeNumber((Double) obj); 318 return; 319 } 320 PropertyEditor propertyEditor = findPropertyEditor(obj.getClass()); 321 if (propertyEditor != null) { 322 propertyEditor.setValue(obj); 323 gen.writeString(propertyEditor.getAsText()); 324 return; 325 } 326 if (obj.getClass().isArray()) { 327 gen.writeStartArray(); 328 Class<?> compType = null; 329 if (expectedType != null && expectedType.isArray()) { 330 compType = expectedType.getComponentType(); 331 } 332 for (int i = 0; i < Array.getLength(obj); i++) { 333 doWriteObject(Array.get(obj, i), compType); 334 } 335 gen.writeEndArray(); 336 ; 337 return; 338 } 339 if (obj instanceof Collection) { 340 gen.writeStartArray(); 341 for (Object item : (Collection<?>) obj) { 342 doWriteObject(item, null); 343 } 344 gen.writeEndArray(); 345 return; 346 } 347 if (obj instanceof Map) { 348 @SuppressWarnings("unchecked") 349 Map<String, Object> map = (Map<String, Object>) obj; 350 gen.writeStartObject(); 351 for (Map.Entry<String, Object> e : map.entrySet()) { 352 gen.writeFieldName(e.getKey()); 353 doWriteObject(e.getValue(), null); 354 } 355 gen.writeEndObject(); 356 return; 357 } 358 BeanInfo beanInfo = findBeanInfo(obj.getClass()); 359 if (beanInfo != null && beanInfo.getPropertyDescriptors().length > 0) { 360 gen.writeStartObject(); 361 if (!obj.getClass().equals(expectedType) && !omitClass) { 362 gen.writeStringField("class", aliases.computeIfAbsent( 363 obj.getClass(), k -> k.getName())); 364 } 365 for (PropertyDescriptor propDesc : beanInfo 366 .getPropertyDescriptors()) { 367 if (propDesc.getValue("transient") != null) { 368 continue; 369 } 370 if (excluded.contains(propDesc.getPropertyType().getName())) { 371 continue; 372 } 373 Method method = propDesc.getReadMethod(); 374 if (method == null) { 375 continue; 376 } 377 try { 378 Object value = method.invoke(obj); 379 gen.writeFieldName(propDesc.getName()); 380 doWriteObject(value, propDesc.getPropertyType()); 381 continue; 382 } catch (IllegalAccessException | IllegalArgumentException 383 | InvocationTargetException e) { 384 // Bad luck 385 } 386 } 387 gen.writeEndObject(); 388 return; 389 } 390 // Last resort 391 gen.writeString(obj.toString()); 392 } 393 394}