001/** 002 * Copyright (C) 2006-2024 Talend Inc. - www.talend.com 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.talend.sdk.component.runtime.manager.service.http; 017 018import static java.util.stream.Collectors.joining; 019import static java.util.stream.Collectors.toList; 020import static java.util.stream.Stream.of; 021import static org.talend.sdk.component.runtime.base.lang.exception.InvocationExceptionWrapper.toRuntimeException; 022 023import java.io.ObjectStreamException; 024import java.io.Serializable; 025import java.lang.reflect.InvocationHandler; 026import java.lang.reflect.InvocationTargetException; 027import java.lang.reflect.Method; 028import java.lang.reflect.Proxy; 029import java.util.ArrayList; 030import java.util.Collection; 031import java.util.Map; 032import java.util.concurrent.ConcurrentHashMap; 033import java.util.concurrent.ConcurrentMap; 034import java.util.stream.Stream; 035 036import javax.json.bind.Jsonb; 037 038import org.talend.sdk.component.api.service.http.HttpClient; 039import org.talend.sdk.component.api.service.http.HttpClientFactory; 040import org.talend.sdk.component.api.service.http.Request; 041import org.talend.sdk.component.runtime.manager.proxy.SerializationHandlerReplacer; 042import org.talend.sdk.component.runtime.manager.reflect.Copiable; 043import org.talend.sdk.component.runtime.manager.reflect.ReflectionService; 044import org.talend.sdk.component.runtime.reflect.Defaults; 045import org.talend.sdk.component.runtime.serialization.SerializableService; 046 047import lombok.AllArgsConstructor; 048import lombok.RequiredArgsConstructor; 049import lombok.ToString; 050 051@AllArgsConstructor 052public class HttpClientFactoryImpl implements HttpClientFactory, Serializable { 053 054 private final String plugin; 055 056 private final ReflectionService reflections; 057 058 private final Jsonb jsonb; 059 060 private final Map<Class<?>, Object> services; 061 062 public static <T> Collection<String> createErrors(final Class<T> api) { 063 final Collection<String> errors = new ArrayList<>(); 064 final Collection<Method> methods = 065 of(api.getMethods()).filter(m -> m.getDeclaringClass() == api && !m.isDefault()).collect(toList()); 066 067 if (!HttpClient.class.isAssignableFrom(api)) { 068 errors.add(api.getCanonicalName() + " should extends HttpClient"); 069 } 070 errors 071 .addAll(methods 072 .stream() 073 .filter(m -> !m.isAnnotationPresent(Request.class)) 074 .map(m -> "No @Request on " + m) 075 .collect(toList())); 076 return errors; 077 } 078 079 @Override 080 public <T> T create(final Class<T> api, final String base) { 081 if (!api.isInterface()) { 082 throw new IllegalArgumentException(api + " is not an interface"); 083 } 084 validate(api); 085 final HttpHandler handler = 086 new HttpHandler(api.getName(), plugin, new RequestParser(reflections, jsonb, services)); 087 final T instance = api 088 .cast(Proxy 089 .newProxyInstance(api.getClassLoader(), 090 Stream 091 .of(api, HttpClient.class, Serializable.class, Copiable.class) 092 .distinct() 093 .toArray(Class[]::new), 094 handler)); 095 HttpClient.class.cast(instance).base(base); 096 return instance; 097 } 098 099 private <T> void validate(final Class<T> api) { 100 final Collection<String> errors = createErrors(api); 101 if (!errors.isEmpty()) { 102 throw new IllegalArgumentException( 103 "Invalid Http Proxy specification:\n" + errors.stream().collect(joining("\n- ", "- ", "\n"))); 104 } 105 } 106 107 Object writeReplace() throws ObjectStreamException { 108 return new SerializableService(plugin, HttpClientFactory.class.getName()); 109 } 110 111 @ToString 112 @RequiredArgsConstructor 113 private static class HttpHandler implements InvocationHandler, Serializable { 114 115 private final String proxyType; 116 117 private final String plugin; 118 119 private String base; 120 121 private final RequestParser requestParser; 122 123 private volatile Map<Class<?>, Object> jaxbContexts; 124 125 private volatile ConcurrentMap<Method, ExecutionContext> invokers; 126 127 @Override 128 public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { 129 if (Copiable.class == method.getDeclaringClass()) { 130 final HttpHandler httpHandler = new HttpHandler(proxyType, plugin, requestParser); 131 httpHandler.base = base; 132 return Proxy 133 .newProxyInstance(proxy.getClass().getClassLoader(), proxy.getClass().getInterfaces(), 134 httpHandler); 135 } 136 if (Defaults.isDefaultAndShouldHandle(method)) { 137 return Defaults.handleDefault(method.getDeclaringClass(), method, proxy, args); 138 } 139 140 final String methodName = method.getName(); 141 if (Object.class == method.getDeclaringClass()) { 142 switch (methodName) { 143 case "equals": 144 return args[0] != null && Proxy.isProxyClass(args[0].getClass()) 145 && equals(Proxy.getInvocationHandler(args[0])); 146 case "toString": 147 return "@Request " + base; 148 default: 149 return delegate(method, args); 150 } 151 } else if (HttpClient.class == method.getDeclaringClass()) { 152 switch (methodName) { 153 case "base": 154 this.base = String.valueOf(args[0]); 155 return null; 156 default: 157 throw new UnsupportedOperationException("HttpClient." + methodName); 158 } 159 } 160 161 if (!method.isAnnotationPresent(Request.class)) { 162 return delegate(method, args); 163 } 164 165 if (invokers == null) { 166 synchronized (this) { 167 if (invokers == null) { 168 invokers = new ConcurrentHashMap<>(); 169 jaxbContexts = new ConcurrentHashMap<>(); 170 } 171 } 172 } 173 174 return invokers.computeIfAbsent(method, this.requestParser::parse).apply(this.base, args); 175 } 176 177 Object writeReplace() throws ObjectStreamException { 178 return new SerializationHandlerReplacer(plugin, proxyType); 179 } 180 181 private Object delegate(final Method method, final Object[] args) { 182 try { 183 return method.invoke(this, args); 184 } catch (final InvocationTargetException ite) { 185 throw toRuntimeException(ite); 186 } catch (IllegalAccessException e) { 187 throw new IllegalStateException(e); 188 } 189 } 190 } 191 192}