001/* 002 * JDrupes Json-B plugins 003 * Copyright (C) 2024 Michael N. Lipp 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <https://www.gnu.org/licenses/>. 017 */ 018 019package org.jdrupes.jsonb.beans; 020 021import jakarta.json.JsonValue; 022import jakarta.json.bind.annotation.JsonbAnnotation; 023import jakarta.json.bind.serializer.JsonbSerializer; 024import jakarta.json.bind.serializer.SerializationContext; 025import jakarta.json.stream.JsonGenerator; 026import java.beans.BeanInfo; 027import java.beans.PropertyDescriptor; 028import java.beans.PropertyEditor; 029import java.lang.reflect.InvocationTargetException; 030import java.lang.reflect.Method; 031import java.util.Arrays; 032import java.util.Collection; 033import java.util.HashMap; 034import java.util.HashSet; 035import java.util.Map; 036import java.util.Optional; 037import java.util.Set; 038 039/// 040/// A serializer that treats all objects as JavaBeans. The JSON 041/// description of an object can have an additional key/value pair with 042/// key "@class" and a class name. This class information is generated 043/// only if it is needed, i.e. if it cannot be derived from the containing 044/// object. 045/// 046/// Given the following classes: 047/// 048/// ```java 049/// public static class Person { 050/// 051/// private String name; 052/// private int age; 053/// private PhoneNumber[] numbers; 054/// 055/// public String getName() { 056/// return name; 057/// } 058/// 059/// public void setName(String name) { 060/// this.name = name; 061/// } 062/// 063/// public int getAge() { 064/// return age; 065/// } 066/// 067/// public void setAge(int age) { 068/// this.age = age; 069/// } 070/// 071/// public PhoneNumber[] getNumbers() { 072/// return numbers; 073/// } 074/// 075/// public void setNumbers(PhoneNumber[] numbers) { 076/// this.numbers = numbers; 077/// } 078/// } 079/// 080/// public static class PhoneNumber { 081/// private String name; 082/// private String number; 083/// 084/// public PhoneNumber() { 085/// } 086/// 087/// public String getName() { 088/// return name; 089/// } 090/// 091/// public void setName(String name) { 092/// this.name = name; 093/// } 094/// 095/// public String getNumber() { 096/// return number; 097/// } 098/// 099/// public void setNumber(String number) { 100/// this.number = number; 101/// } 102/// } 103/// 104/// public static class SpecialNumber extends PhoneNumber { 105/// } 106/// ``` 107/// 108/// A serialization result may look like this: 109/// 110/// ```jsonSample1 111/// { 112/// "age": 42, 113/// "name": "Simon Sample", 114/// "numbers": [ 115/// { 116/// "name": "Home", 117/// "number": "06751 51 56 57" 118/// }, 119/// { 120/// "@class": "test.json.SpecialNumber", 121/// "name": "Work", 122/// "number": "030 77 35 44" 123/// } 124/// ] 125/// } 126/// ``` 127/// 128/// As there is no marker interface for JavaBeans, this serializer 129/// considers all objects to be JavaBeans by default and obtains all 130/// information for serialization from the generated (or explicitly 131/// provided) {@link BeanInfo}. It is, however, possible to exclude 132/// classes from bing handled by this serializer by either annotating them 133/// with {@link JsonbAnnotation} or 134/// 135public class JavaBeanSerializer extends JavaBeanConverter 136 implements JsonbSerializer<Object> { 137 138 private static record Expected(Object[] values, Class<?> type) { 139 } 140 141 private ThreadLocal<Expected> expected = new ThreadLocal<>(); 142 private boolean omitClass; 143 private final Map<Class<?>, String> aliases = new HashMap<>(); 144 private final Set<Class<?>> ignored = new HashSet<>(); 145 146 /// Create a new instance. 147 public JavaBeanSerializer() { 148 // Can be instantiated 149 } 150 151 /// 152 /// Adds an alias for a class. 153 /// 154 /// @param clazz the clazz 155 /// @param alias the alias 156 /// @return the java bean serializer 157 /// 158 public JavaBeanSerializer addAlias(Class<?> clazz, String alias) { 159 aliases.put(clazz, alias); 160 return this; 161 } 162 163 /// 164 /// Sets the expected class for the object passed as parameter 165 /// to {@link jakarta.json.bind.Jsonb#toJson}. 166 /// 167 /// @param type the type 168 /// @return the java bean serializer 169 /// 170 public JavaBeanSerializer setExpected(Class<?> type) { 171 expected.set(new Expected(null, type)); 172 return this; 173 } 174 175 /// 176 /// Don't handle the given type(s) as JavaBeans. Pass them to the 177 /// default serialization mechanism instead. 178 /// 179 /// @param type the type(s) to ignore 180 /// @return the java bean serializer 181 /// 182 public JavaBeanSerializer addIgnored(Class<?>... type) { 183 ignored.addAll(Arrays.asList(type)); 184 return this; 185 } 186 187 @Override 188 public void serialize(Object value, JsonGenerator generator, 189 SerializationContext context) { 190 if (value == null 191 || value instanceof Boolean 192 || value instanceof Byte 193 || value instanceof Number 194 || value.getClass().isArray() 195 || value instanceof Collection<?> 196 || value instanceof Map<?, ?> 197 || value instanceof JsonValue 198 || value.getClass().getAnnotation(JsonbAnnotation.class) != null 199 || ignored.contains(value.getClass())) { 200 context.serialize(value, generator); 201 return; 202 } 203 PropertyEditor propertyEditor = findPropertyEditor(value.getClass()); 204 if (propertyEditor != null) { 205 propertyEditor.setValue(value); 206 generator.write(propertyEditor.getAsText()); 207 return; 208 } 209 BeanInfo beanInfo = findBeanInfo(value.getClass()); 210 if (beanInfo != null && beanInfo.getPropertyDescriptors().length > 0) { 211 writeJavaBean(value, beanInfo, generator, context); 212 return; 213 } 214 } 215 216 @SuppressWarnings("PMD.EmptyCatchBlock") 217 private void writeJavaBean(Object bean, BeanInfo beanInfo, 218 JsonGenerator generator, SerializationContext context) { 219 generator.writeStartObject(); 220 Class<?> expectedType = Optional.ofNullable(expected.get()) 221 .filter(e -> e.values() == null 222 || Arrays.stream(e.values()).anyMatch(v -> v == bean)) 223 .map(e -> e.type()).orElse(null); 224 if (expectedType != null && !bean.getClass().equals(expectedType) 225 && !omitClass) { 226 context.serialize("@class", aliases.computeIfAbsent( 227 bean.getClass(), k -> k.getName()), generator); 228 } 229 for (PropertyDescriptor propDesc : beanInfo 230 .getPropertyDescriptors()) { 231 if (propDesc.getValue("transient") != null) { 232 continue; 233 } 234 Method method = propDesc.getReadMethod(); 235 if (method == null) { 236 continue; 237 } 238 try { 239 Object propValue = method.invoke(bean); 240 Expected old = expected.get(); 241 try { 242 if (propDesc.getPropertyType().isArray()) { 243 expected.set(new Expected((Object[]) propValue, 244 propDesc.getPropertyType().componentType())); 245 } else { 246 expected.set(new Expected(new Object[] { propValue }, 247 propDesc.getPropertyType())); 248 } 249 context.serialize(propDesc.getName(), propValue, generator); 250 } finally { 251 expected.set(old); 252 } 253 continue; 254 } catch (IllegalAccessException | IllegalArgumentException 255 | InvocationTargetException e) { 256 // Bad luck 257 } 258 } 259 generator.writeEnd(); 260 } 261 262}