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}