001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.commons.bean; 018 019import static org.apache.juneau.commons.reflect.ReflectionUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021 022import java.lang.reflect.*; 023import java.util.*; 024 025/** 026 * Provides an {@link InvocationHandler} for creating dynamic proxy instances of bean interfaces. 027 * 028 * <p> 029 * This class enables the creation of bean instances from interfaces without requiring concrete implementations. 030 * When the {@code useInterfaceProxies} setting is enabled in the marshalling context, this handler is used to create 031 * proxy instances that implement bean interfaces. 032 * 033 * <p> 034 * The handler stores bean property values in an internal map and intercepts method calls to: 035 * <ul> 036 * <li><b>Getter methods</b> - Returns values from the internal property map</li> 037 * <li><b>Setter methods</b> - Stores values in the internal property map</li> 038 * <li><b>{@code equals(Object)}</b> - Compares property maps, with special handling for other proxy instances</li> 039 * <li><b>{@code hashCode()}</b> - Returns hash code based on the property map</li> 040 * <li><b>{@code toString()}</b> - Serializes the property map to JSON</li> 041 * </ul> 042 * 043 * <p> 044 * When comparing two proxy instances using {@code equals()}, if both are created with {@code BeanProxyInvocationHandler}, 045 * the comparison is optimized by directly comparing their internal property maps rather than converting to {@link BeanMap}. 046 * 047 * <h5 class='section'>Example:</h5> 048 * <p class='bjava'> 049 * <jc>// Define an interface</jc> 050 * <jk>public interface</jk> Person { 051 * <jk>String</jk> <jsm>getName</jsm>(); 052 * <jk>void</jk> <jsm>setName</jsm>(<jk>String</jk> name); 053 * <jk>int</jk> <jsm>getAge</jsm>(); 054 * <jk>void</jk> <jsm>setAge</jsm>(<jk>int</jk> age); 055 * } 056 * 057 * <jc>// Create a proxy instance</jc> 058 * <jk>var</jk> bc = <jsm>MarshallingContext</jsm>.<jsm>create</jsm>().<jsm>useInterfaceProxies</jsm>().<jsm>build</jsm>(); 059 * <jk>var</jk> person = bc.<jsm>getClassMeta</jsm>(Person.<jk>class</jk>).<jsm>newInstance</jsm>(); 060 * 061 * <jc>// Use it like a regular bean</jc> 062 * person.<jsm>setName</jsm>(<js>"John"</js>); 063 * person.<jsm>setAge</jsm>(25); 064 * <jk>var</jk> name = person.<jsm>getName</jsm>(); <jc>// Returns "John"</jc> 065 * </p> 066 * 067 * <h5 class='topic'>Thread safety</h5> 068 * 069 * Instances are not thread-safe. 070 * Property values are stored in a mutable {@link HashMap} with no synchronization. 071 * 072 * @param <T> The interface class type 073 * @see BeanConfigContext#isUseInterfaceProxies() 074 * @see BeanMeta#getBeanProxyInvocationHandler() 075 * @see Proxy#newProxyInstance(ClassLoader, Class[], InvocationHandler) 076 */ 077@SuppressWarnings({ 078 "java:S3740" // Raw Class/Map types used intentionally for reflective proxy dispatch where generic parameters are erased at runtime 079}) 080public class BeanProxyInvocationHandler<T> implements InvocationHandler { 081 082 private final BeanMeta<T> meta; // The BeanMeta for this instance 083 private final Map<String,Object> beanProps; // The map of property names to bean property values. 084 085 /** 086 * Constructor. 087 * 088 * @param meta The bean metadata for the interface. Must not be <jk>null</jk>. 089 */ 090 public BeanProxyInvocationHandler(BeanMeta<T> meta) { 091 this.meta = meta; 092 this.beanProps = new HashMap<>(); 093 } 094 095 /** 096 * Handles method invocations on the proxy instance. 097 * 098 * <p> 099 * This method intercepts all method calls on the proxy and routes them appropriately: 100 * <ul> 101 * <li>If the method is {@code equals(Object)}, compares property maps</li> 102 * <li>If the method is {@code hashCode()}, returns the hash code of the property map</li> 103 * <li>If the method is {@code toString()}, serializes the property map to JSON</li> 104 * <li>If the method is a getter (identified via {@link BeanMeta#getGetterProps()}), returns the property value</li> 105 * <li>If the method is a setter (identified via {@link BeanMeta#getSetterProps()}), stores the property value</li> 106 * <li>Otherwise, throws {@link UnsupportedOperationException}</li> 107 * </ul> 108 * 109 * <p> 110 * The {@code equals()} method has special optimization: when comparing two proxy instances created with this handler, 111 * it directly compares their internal property maps using {@link Proxy#getInvocationHandler(Object)} to access the 112 * other instance's handler. 113 * 114 * @param proxy The proxy instance on which the method was invoked 115 * @param method The method that was invoked 116 * @param args The arguments passed to the method, or <jk>null</jk> if no arguments 117 * @return The return value of the method invocation 118 * @throws UnsupportedOperationException If the method is not a supported bean method (getter, setter, equals, hashCode, or toString) 119 */ 120 @Override /* Overridden from InvocationHandler */ 121 @SuppressWarnings({ 122 "java:S3776" // Cognitive complexity acceptable for proxy invocation routing 123 }) 124 public Object invoke(Object proxy, Method method, Object[] args) { 125 var mi = info(method); 126 if (mi.hasName("equals") && mi.hasParameterTypes(Object.class)) { 127 var arg = args[0]; 128 if (arg == null) 129 return false; 130 if (proxy == arg) 131 return true; 132 if (eq(proxy.getClass(), arg.getClass())) { 133 var ih = Proxy.getInvocationHandler(arg); 134 if (ih instanceof BeanProxyInvocationHandler ih2) { 135 return beanProps.equals(ih2.beanProps); 136 } 137 } 138 @SuppressWarnings({ 139 "unchecked" // Type erasure on reflective/generic cast; element type is verified at call site 140 }) 141 var argMeta = (BeanMeta<Object>) BeanMeta.of(arg.getClass(), meta.getConfig()); 142 return eq(beanProps, BeanMap.of(arg, argMeta)); 143 } 144 145 if (mi.hasName("hashCode") && mi.getParameterCount() == 0) 146 return Integer.valueOf(this.beanProps.hashCode()); 147 148 if (mi.hasName("toString") && mi.getParameterCount() == 0) 149 return Objects.toString(this.beanProps); 150 151 var prop = meta.getGetterProps().get(method); 152 if (nn(prop)) 153 return beanProps.get(prop); 154 155 prop = meta.getSetterProps().get(method); 156 if (nn(prop)) { 157 beanProps.put(prop, args[0]); 158 return null; 159 } 160 161 throw uoex("Unsupported bean method. method='%s'", method); 162 } 163}