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