001/* 002 * SPDX-License-Identifier: Apache-2.0 003 * 004 * Copyright 2024-2026 The Enola <https://enola.dev> Authors 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); 007 * you may not use this file except in compliance with the License. 008 * You may obtain a copy of the License at 009 * 010 * https://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package dev.enola.thing.java; 019 020import com.google.common.reflect.Reflection; 021 022import dev.enola.thing.Thing; 023import dev.enola.thing.impl.IImmutableThing; 024import dev.enola.thing.impl.ImmutableThing; 025 026import org.jspecify.annotations.Nullable; 027 028import java.lang.reflect.InvocationHandler; 029import java.lang.reflect.Method; 030import java.util.Arrays; 031 032public final class ProxyTBF implements TBF { 033 034 private final TBF wrap; 035 036 /** 037 * Constructor. 038 * 039 * @param wrap is a {@link TBF} such as {@link ImmutableThing#FACTORY} or {@link 040 * dev.enola.thing.impl.MutableThing#FACTORY}. 041 */ 042 public ProxyTBF(TBF wrap) { 043 this.wrap = wrap; 044 } 045 046 @Override 047 public boolean handles(Class<?> builderInterface) { 048 // Skip Proxy if wrapped delegate can handle it 049 // This makes TBFChain more efficient, because e.g. 050 // Thing.Builder.class can be "passed through". 051 return !wrap.handles(builderInterface); 052 } 053 054 @Override 055 public boolean handles(String typeIRI) { 056 // ProxyTBF handles any typeIRI, currently. 057 // TODO Make it based on whether create() implementation uses default class or not. 058 return true; 059 } 060 061 @Override 062 @SuppressWarnings("unchecked") 063 public Thing.Builder<Thing> create(String typeIRI) { 064 var classPair = TypeToBuilder.typeToBuilder(typeIRI); 065 return create(classPair.builderClass(), classPair.thingClass()); 066 } 067 068 @Override 069 @SuppressWarnings("unchecked") 070 public <T extends Thing, B extends Thing.Builder<T>> B create( 071 Class<B> builderInterface, Class<T> thingInterface) { 072 return (B) 073 create( 074 wrap, 075 -1, 076 (Class<Thing.Builder<IImmutableThing>>) builderInterface, 077 (Class<IImmutableThing>) thingInterface); 078 } 079 080 @Override 081 @SuppressWarnings("unchecked") 082 public <T extends Thing, B extends Thing.Builder<T>> B create( 083 Class<B> builderInterface, Class<T> thingInterface, int expectedSize) { 084 return (B) 085 create( 086 wrap, 087 expectedSize, 088 (Class<Thing.Builder<IImmutableThing>>) builderInterface, 089 (Class<IImmutableThing>) thingInterface); 090 } 091 092 @SuppressWarnings("unchecked") 093 private static Thing.Builder<? extends IImmutableThing> create( 094 TBF tbf, 095 int expectedSize, 096 Class<Thing.Builder<IImmutableThing>> builderInterface, 097 Class<IImmutableThing> thingInterface) { 098 if (builderInterface.equals(Thing.Builder.class)) 099 return createX(tbf, builderInterface, thingInterface, expectedSize); 100 101 var wrappedBuilder = // an ImmutableThing.Builder or a MutableThing (but NOT another Proxy) 102 (Thing.Builder<? extends IImmutableThing>) 103 createX(tbf, builderInterface, thingInterface, expectedSize); 104 var handler = 105 new BuilderInvocationHandler(tbf, wrappedBuilder, builderInterface, thingInterface); 106 var proxy = Reflection.newProxy(builderInterface, handler); 107 if (!(builderInterface.isInstance(proxy))) 108 throw new IllegalArgumentException(proxy.toString()); 109 return proxy; 110 } 111 112 private static Thing.Builder<? extends IImmutableThing> createX( 113 TBF tbf, 114 Class<Thing.Builder<IImmutableThing>> builderInterface, 115 Class<IImmutableThing> thingInterface, 116 int expectedSize) { 117 if (expectedSize == -1) return tbf.create(builderInterface, thingInterface); 118 else return tbf.create(builderInterface, thingInterface, expectedSize); 119 } 120 121 // TODO Consider using Guava's AbstractInvocationHandler? 122 123 private record BuilderInvocationHandler( 124 TBF tbf, 125 Thing.Builder<? extends IImmutableThing> immutableThingBuilder, 126 Class<Thing.Builder<IImmutableThing>> builderInterface, 127 Class<IImmutableThing> thingInterface) 128 implements InvocationHandler { 129 130 @Override 131 public Object invoke(Object proxy, Method method, @Nullable Object[] args) 132 throws Throwable { 133 if (method.isDefault()) return InvocationHandler.invokeDefault(proxy, method, args); 134 if (method.getName() == "iri" && args != null && args.length > 0) { 135 immutableThingBuilder.iri((String) args[0]); 136 return proxy; 137 } else if (method.getName().equals("build")) { 138 var built = immutableThingBuilder.build(); 139 var handler = 140 new ThingInvocationHandler(tbf, built, builderInterface, thingInterface); 141 return Reflection.newProxy(thingInterface, handler); 142 } else if (method.getName().equals("toString")) return toString(); 143 // else 144 try { 145 return method.invoke(immutableThingBuilder, args); 146 } catch (IllegalArgumentException e) { 147 throw new IllegalArgumentException( 148 "Interface may be missing default method implementations?!" 149 + " immutableThingBuilder=" 150 + immutableThingBuilder 151 + "; proxy=" 152 + proxy 153 + "; method=" 154 + method 155 + "; args=" 156 + Arrays.toString(args), 157 e); 158 } 159 } 160 161 @Override 162 public String toString() { 163 return "ProxyTBF.BuilderInvocationHandler{builderInterface=" 164 + builderInterface 165 + ", builder=" 166 + immutableThingBuilder 167 + "}"; 168 } 169 } 170 171 private record ThingInvocationHandler( 172 TBF tbf, 173 IImmutableThing thing, 174 Class<Thing.Builder<IImmutableThing>> builderInterface, 175 Class<IImmutableThing> thingInterface) 176 implements InvocationHandler { 177 178 @Override 179 public Object invoke(Object proxy, Method method, @Nullable Object[] args) 180 throws Throwable { 181 if (method.isDefault()) return InvocationHandler.invokeDefault(proxy, method, args); 182 if (method.getName().equals("copy")) return copy(); 183 if (method.getName().equals("toString")) return toString(); 184 return method.invoke(thing, args); 185 } 186 187 @SuppressWarnings( 188 "Immutable") // TODO Remove when switching to (TBD) PredicatesObjects.Visitor 189 private Thing.Builder<? extends IImmutableThing> copy() { 190 var size = thing.properties().size(); 191 var builder = create(tbf, size, builderInterface, thingInterface); 192 builder.iri(thing.iri()); 193 thing.properties() 194 .forEach( 195 (predicateIRI, object) -> { 196 var datatypeIRI = thing.datatype(predicateIRI); 197 builder.set(predicateIRI, object, datatypeIRI); 198 }); 199 return builder; 200 } 201 202 @Override 203 public String toString() { 204 return "ProxyTBF.ThingInvocationHandler{thingInterface=" 205 + thingInterface 206 + ", thing=" 207 + thing 208 + "}"; 209 } 210 } 211}