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}