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