001/**
002 * Copyright (C) 2006-2022 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.junit.http.internal.impl;
017
018import static java.util.Optional.ofNullable;
019import static java.util.stream.Collectors.toMap;
020
021import java.io.File;
022import java.io.FileOutputStream;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.OutputStream;
026import java.lang.reflect.ParameterizedType;
027import java.lang.reflect.Type;
028import java.net.MalformedURLException;
029import java.net.URL;
030import java.nio.charset.StandardCharsets;
031import java.util.ArrayList;
032import java.util.Arrays;
033import java.util.Collection;
034import java.util.Map;
035import java.util.Map.Entry;
036import java.util.Objects;
037import java.util.Optional;
038import java.util.function.Predicate;
039import java.util.stream.Stream;
040
041import javax.json.bind.Jsonb;
042import javax.json.bind.JsonbBuilder;
043import javax.json.bind.JsonbConfig;
044import javax.json.bind.config.PropertyOrderStrategy;
045
046import org.talend.sdk.component.junit.http.api.Request;
047import org.talend.sdk.component.junit.http.api.Response;
048import org.talend.sdk.component.junit.http.api.ResponseLocator;
049
050import lombok.Data;
051import lombok.extern.slf4j.Slf4j;
052
053@Data
054@Slf4j
055public class DefaultResponseLocator implements ResponseLocator, AutoCloseable {
056
057    public static final String PREFIX = "talend/testing/http/";
058
059    private static final ParameterizedTypeImpl MODEL_TYPE = new ParameterizedTypeImpl(Collection.class, Model.class);
060
061    private final Jsonb jsonb;
062
063    private String prefix;
064
065    private String test;
066
067    private final Collection<DefaultResponseLocator.Model> capturingBuffer = new ArrayList<>();
068
069    public DefaultResponseLocator(final String prefix, final String test) {
070        this.prefix = prefix;
071        this.test = test;
072        this.jsonb = JsonbBuilder
073                .create(new JsonbConfig()
074                        .withPropertyOrderStrategy(PropertyOrderStrategy.LEXICOGRAPHICAL)
075                        .withFormatting(true)
076                        .setProperty("johnzon.cdi.activated", false));
077    }
078
079    @Override
080    public Optional<Response> findMatching(final Request request, final Predicate<String> headerFilter) {
081        final String pref = ofNullable(prefix).orElse(PREFIX);
082        final ClassLoader loader = Thread.currentThread().getContextClassLoader();
083        final Optional<Response> response = doFind(request, pref, loader, headerFilter, true);
084        if (response.isPresent()) {
085            return response;
086        }
087        return doFind(request, pref, loader, headerFilter, false);
088    }
089
090    protected Optional<Response> doFind(final Request request, final String pref, final ClassLoader loader,
091            final Predicate<String> headerFilter, final boolean exactMatching) {
092        return Stream
093                .of(pref + test + ".json", pref + stripQuery(request.uri()) + ".json")
094                .map(loader::getResource)
095                .filter(Objects::nonNull)
096                .flatMap(url -> {
097                    final Collection<Model> models;
098                    try (final InputStream stream = url.openStream()) {
099                        models = Collection.class.cast(jsonb.fromJson(stream, MODEL_TYPE));
100                    } catch (final IOException e) {
101                        throw new IllegalStateException(e);
102                    }
103                    return models
104                            .stream()
105                            .filter(m -> m.request != null && matches(request, m.request, exactMatching, headerFilter));
106                })
107                .findFirst()
108                .map(model -> new ResponseImpl(model.response.headers, model.response.status, getPayload(model)));
109    }
110
111    protected byte[] getPayload(final Model model) {
112        if (model.response.payload == null) {
113            return null;
114        }
115        return model.response.payload.getBytes(StandardCharsets.UTF_8);
116    }
117
118    private String stripQuery(final String uri) {
119        try {
120            return new URL(uri).getPath();
121        } catch (final MalformedURLException e) {
122            // no-op
123        }
124        return uri;
125    }
126
127    protected boolean matches(final Request request, final RequestModel model, final boolean exact,
128            final Predicate<String> headerFilter) {
129        final String method = ofNullable(model.method).orElse("GET");
130        final String requestUri = request.uri();
131        boolean headLineMatches = requestUri.equals(model.uri) && request.method().equalsIgnoreCase(method);
132        final String payload = request.payload();
133        final boolean headersMatch = doesHeadersMatch(request, model, headerFilter);
134        if (headLineMatches && headersMatch && (model.payload == null || model.payload.equals(payload))) {
135            return true;
136        } else if (exact) {
137            return false;
138        }
139
140        if (log.isDebugEnabled()) {
141            log.debug("Matching test: {} for {}", request, model);
142        }
143
144        if (!headLineMatches && requestUri.contains("?")) { // strip the query
145            headLineMatches = requestUri.substring(0, requestUri.indexOf('?')).equals(model.uri)
146                    && request.method().equalsIgnoreCase(method);
147        }
148
149        return headLineMatches && headersMatch && (model.payload == null
150                || (payload != null && (payload.matches(model.payload) || payload.equals(model.payload))));
151    }
152
153    protected boolean doesHeadersMatch(final Request request, final RequestModel model,
154            final Predicate<String> headerFilter) {
155        final Map<String, String> notcased = request.headers()
156                .entrySet()
157                .stream()
158                .collect(toMap(e -> e.getKey().toLowerCase(), Entry::getValue));
159        return model.headers == null || model.headers
160                .entrySet()
161                .stream()
162                .filter(h -> !headerFilter.test(h.getKey()))
163                .allMatch(h -> h.getValue().equals(notcased.get(h.getKey().toLowerCase())));
164    }
165
166    @Override
167    public void close() {
168        try {
169            jsonb.close();
170        } catch (final Exception e) {
171            throw new IllegalStateException(e);
172        }
173    }
174
175    public void flush(final String baseCapture) {
176        final File output = new File(baseCapture,
177                ofNullable(test).map(t -> prefix + t + ".json").orElseGet(() -> prefix + ".json"));
178        output.getParentFile().mkdirs();
179        try (final OutputStream outputStream = new FileOutputStream(output)) {
180            jsonb.toJson(capturingBuffer, outputStream);
181        } catch (final IOException e) {
182            throw new IllegalStateException(e);
183        } finally {
184            capturingBuffer.clear();
185        }
186    }
187
188    public void addModel(final Model model) {
189        getCapturingBuffer().add(model);
190    }
191
192    @Data
193    public static class Model {
194
195        private RequestModel request;
196
197        private ResponseModel response;
198    }
199
200    @Data
201    public static class RequestModel {
202
203        private String uri;
204
205        private String method;
206
207        private String payload;
208
209        private Map<String, String> headers;
210    }
211
212    @Data
213    public static class ResponseModel {
214
215        private int status;
216
217        private Map<String, String> headers;
218
219        private String payload;
220    }
221
222    private static final class ParameterizedTypeImpl implements ParameterizedType {
223
224        private final Type rawType;
225
226        private final Type[] types;
227
228        private ParameterizedTypeImpl(final Type raw, final Type... types) {
229            this.rawType = raw;
230            this.types = types;
231        }
232
233        @Override
234        public Type[] getActualTypeArguments() {
235            return types.clone();
236        }
237
238        @Override
239        public Type getOwnerType() {
240            return null;
241        }
242
243        @Override
244        public Type getRawType() {
245            return rawType;
246        }
247
248        @Override
249        public int hashCode() {
250            return Arrays.hashCode(types) ^ (rawType == null ? 0 : rawType.hashCode());
251        }
252
253        @Override
254        public boolean equals(final Object obj) {
255            if (this == obj) {
256                return true;
257            } else if (obj instanceof ParameterizedType) {
258                final ParameterizedType that = (ParameterizedType) obj;
259                final Type thatRawType = that.getRawType();
260                return that.getOwnerType() == null
261                        && (rawType == null ? thatRawType == null : rawType.equals(thatRawType))
262                        && Arrays.equals(types, that.getActualTypeArguments());
263            } else {
264                return false;
265            }
266        }
267
268        @Override
269        public String toString() {
270            final StringBuilder buffer = new StringBuilder();
271            buffer.append(((Class<?>) rawType).getSimpleName());
272            final Type[] actualTypes = getActualTypeArguments();
273            if (actualTypes.length > 0) {
274                buffer.append("<");
275                int length = actualTypes.length;
276                for (int i = 0; i < length; i++) {
277                    buffer.append(actualTypes[i].toString());
278                    if (i != actualTypes.length - 1) {
279                        buffer.append(",");
280                    }
281                }
282
283                buffer.append(">");
284            }
285            return buffer.toString();
286        }
287    }
288}