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; 018 019import static org.apache.juneau.commons.reflect.ReflectionUtils.*; 020import static org.apache.juneau.commons.utils.ThrowableUtils.*; 021import static org.apache.juneau.commons.utils.Utils.*; 022 023import java.lang.reflect.*; 024import java.util.*; 025 026import org.apache.juneau.json.*; 027 028/** 029 * Provides an {@link InvocationHandler} for creating dynamic proxy instances of bean interfaces. 030 * 031 * <p> 032 * This class enables the creation of bean instances from interfaces without requiring concrete implementations. 033 * When the {@code useInterfaceProxies} setting is enabled in {@link BeanContext}, this handler is used to create 034 * proxy instances that implement bean interfaces. 035 * 036 * <p> 037 * The handler stores bean property values in an internal map and intercepts method calls to: 038 * <ul> 039 * <li><b>Getter methods</b> - Returns values from the internal property map</li> 040 * <li><b>Setter methods</b> - Stores values in the internal property map</li> 041 * <li><b>{@code equals(Object)}</b> - Compares property maps, with special handling for other proxy instances</li> 042 * <li><b>{@code hashCode()}</b> - Returns hash code based on the property map</li> 043 * <li><b>{@code toString()}</b> - Serializes the property map to JSON</li> 044 * </ul> 045 * 046 * <p> 047 * When comparing two proxy instances using {@code equals()}, if both are created with {@code BeanProxyInvocationHandler}, 048 * the comparison is optimized by directly comparing their internal property maps rather than converting to {@link BeanMap}. 049 * 050 * <h5 class='section'>Example:</h5> 051 * <p class='bjava'> 052 * <jc>// Define an interface</jc> 053 * <jk>public interface</jk> Person { 054 * <jk>String</jk> <jsm>getName</jsm>(); 055 * <jk>void</jk> <jsm>setName</jsm>(<jk>String</jk> name); 056 * <jk>int</jk> <jsm>getAge</jsm>(); 057 * <jk>void</jk> <jsm>setAge</jsm>(<jk>int</jk> age); 058 * } 059 * 060 * <jc>// Create a proxy instance</jc> 061 * <jk>var</jk> bc = <jsm>BeanContext</jsm>.<jsm>create</jsm>().<jsm>useInterfaceProxies</jsm>().<jsm>build</jsm>(); 062 * <jk>var</jk> person = bc.<jsm>getClassMeta</jsm>(Person.<jk>class</jk>).<jsm>newInstance</jsm>(); 063 * 064 * <jc>// Use it like a regular bean</jc> 065 * person.<jsm>setName</jsm>(<js>"John"</js>); 066 * person.<jsm>setAge</jsm>(25); 067 * <jk>var</jk> name = person.<jsm>getName</jsm>(); <jc>// Returns "John"</jc> 068 * </p> 069 * 070 * @param <T> The interface class type 071 * @see BeanContext#isUseInterfaceProxies() 072 * @see BeanMeta#getBeanProxyInvocationHandler() 073 * @see Proxy#newProxyInstance(ClassLoader, Class[], InvocationHandler) 074 */ 075public class BeanProxyInvocationHandler<T> implements InvocationHandler { 076 077 private final BeanMeta<T> meta; // The BeanMeta for this instance 078 private Map<String,Object> beanProps; // The map of property names to bean property values. 079 080 /** 081 * Constructor. 082 * 083 * @param meta The bean metadata for the interface. Must not be <jk>null</jk>. 084 */ 085 public BeanProxyInvocationHandler(BeanMeta<T> meta) { 086 this.meta = meta; 087 this.beanProps = new HashMap<>(); 088 } 089 090 /** 091 * Handles method invocations on the proxy instance. 092 * 093 * <p> 094 * This method intercepts all method calls on the proxy and routes them appropriately: 095 * <ul> 096 * <li>If the method is {@code equals(Object)}, compares property maps</li> 097 * <li>If the method is {@code hashCode()}, returns the hash code of the property map</li> 098 * <li>If the method is {@code toString()}, serializes the property map to JSON</li> 099 * <li>If the method is a getter (identified via {@link BeanMeta#getGetterProps()}), returns the property value</li> 100 * <li>If the method is a setter (identified via {@link BeanMeta#getSetterProps()}), stores the property value</li> 101 * <li>Otherwise, throws {@link UnsupportedOperationException}</li> 102 * </ul> 103 * 104 * <p> 105 * The {@code equals()} method has special optimization: when comparing two proxy instances created with this handler, 106 * it directly compares their internal property maps using {@link Proxy#getInvocationHandler(Object)} to access the 107 * other instance's handler. 108 * 109 * @param proxy The proxy instance on which the method was invoked 110 * @param method The method that was invoked 111 * @param args The arguments passed to the method, or <jk>null</jk> if no arguments 112 * @return The return value of the method invocation 113 * @throws UnsupportedOperationException If the method is not a supported bean method (getter, setter, equals, hashCode, or toString) 114 */ 115 @Override /* Overridden from InvocationHandler */ 116 public Object invoke(Object proxy, Method method, Object[] args) { 117 var mi = info(method); 118 if (mi.hasName("equals") && mi.hasParameterTypes(Object.class)) { 119 var arg = args[0]; 120 if (arg == null) 121 return false; 122 if (proxy == arg) 123 return true; 124 if (eq(proxy.getClass(), arg.getClass())) { 125 var ih = Proxy.getInvocationHandler(arg); 126 if (ih instanceof BeanProxyInvocationHandler ih2) { 127 return beanProps.equals(ih2.beanProps); 128 } 129 } 130 return eq(beanProps, meta.getBeanContext().toBeanMap(arg)); 131 } 132 133 if (mi.hasName("hashCode") && mi.getParameterCount() == 0) 134 return Integer.valueOf(this.beanProps.hashCode()); 135 136 if (mi.hasName("toString") && mi.getParameterCount() == 0) 137 return Json5Serializer.DEFAULT.toString(this.beanProps); 138 139 var prop = meta.getGetterProps().get(method); 140 if (nn(prop)) 141 return beanProps.get(prop); 142 143 prop = meta.getSetterProps().get(method); 144 if (nn(prop)) { 145 beanProps.put(prop, args[0]); 146 return null; 147 } 148 149 throw unsupportedOp("Unsupported bean method. method=''{0}''", method); 150 } 151}