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}