001/*
002 * Copyright 2002-2019 the original author or authors.
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 *      https://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 */
016
017package org.springframework.test.web.client.match;
018
019import java.io.ByteArrayInputStream;
020import java.io.IOException;
021import java.io.InputStream;
022
023import javax.xml.transform.Source;
024import javax.xml.transform.dom.DOMSource;
025
026import org.hamcrest.Matcher;
027import org.w3c.dom.Node;
028
029import org.springframework.http.HttpHeaders;
030import org.springframework.http.HttpInputMessage;
031import org.springframework.http.MediaType;
032import org.springframework.http.client.ClientHttpRequest;
033import org.springframework.http.converter.FormHttpMessageConverter;
034import org.springframework.mock.http.client.MockClientHttpRequest;
035import org.springframework.test.util.JsonExpectationsHelper;
036import org.springframework.test.util.XmlExpectationsHelper;
037import org.springframework.test.web.client.RequestMatcher;
038import org.springframework.util.MultiValueMap;
039
040import static org.hamcrest.MatcherAssert.assertThat;
041import static org.springframework.test.util.AssertionErrors.assertEquals;
042import static org.springframework.test.util.AssertionErrors.assertTrue;
043
044/**
045 * Factory for request content {@code RequestMatcher}'s. An instance of this
046 * class is typically accessed via {@link MockRestRequestMatchers#content()}.
047 *
048 * @author Rossen Stoyanchev
049 * @since 3.2
050 */
051public class ContentRequestMatchers {
052
053        private final XmlExpectationsHelper xmlHelper;
054
055        private final JsonExpectationsHelper jsonHelper;
056
057
058        /**
059         * Class constructor, not for direct instantiation.
060         * Use {@link MockRestRequestMatchers#content()}.
061         */
062        protected ContentRequestMatchers() {
063                this.xmlHelper = new XmlExpectationsHelper();
064                this.jsonHelper = new JsonExpectationsHelper();
065        }
066
067
068        /**
069         * Assert the request content type as a String.
070         */
071        public RequestMatcher contentType(String expectedContentType) {
072                return contentType(MediaType.parseMediaType(expectedContentType));
073        }
074
075        /**
076         * Assert the request content type as a {@link MediaType}.
077         */
078        public RequestMatcher contentType(MediaType expectedContentType) {
079                return request -> {
080                        MediaType actualContentType = request.getHeaders().getContentType();
081                        assertTrue("Content type not set", actualContentType != null);
082                        assertEquals("Content type", expectedContentType, actualContentType);
083                };
084        }
085
086        /**
087         * Assert the request content type is compatible with the given
088         * content type as defined by {@link MediaType#isCompatibleWith(MediaType)}.
089         */
090        public RequestMatcher contentTypeCompatibleWith(String contentType) {
091                return contentTypeCompatibleWith(MediaType.parseMediaType(contentType));
092        }
093
094        /**
095         * Assert the request content type is compatible with the given
096         * content type as defined by {@link MediaType#isCompatibleWith(MediaType)}.
097         */
098        public RequestMatcher contentTypeCompatibleWith(MediaType contentType) {
099                return request -> {
100                        MediaType actualContentType = request.getHeaders().getContentType();
101                        assertTrue("Content type not set", actualContentType != null);
102                        if (actualContentType != null) {
103                                assertTrue("Content type [" + actualContentType + "] is not compatible with [" + contentType + "]",
104                                                actualContentType.isCompatibleWith(contentType));
105                        }
106                };
107        }
108
109        /**
110         * Get the body of the request as a UTF-8 string and apply the given {@link Matcher}.
111         */
112        public RequestMatcher string(Matcher<? super String> matcher) {
113                return request -> {
114                        MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;
115                        assertThat("Request content", mockRequest.getBodyAsString(), matcher);
116                };
117        }
118
119        /**
120         * Get the body of the request as a UTF-8 string and compare it to the given String.
121         */
122        public RequestMatcher string(String expectedContent) {
123                return request -> {
124                        MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;
125                        assertEquals("Request content", expectedContent, mockRequest.getBodyAsString());
126                };
127        }
128
129        /**
130         * Compare the body of the request to the given byte array.
131         */
132        public RequestMatcher bytes(byte[] expectedContent) {
133                return request -> {
134                        MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;
135                        assertEquals("Request content", expectedContent, mockRequest.getBodyAsBytes());
136                };
137        }
138
139        /**
140         * Parse the body as form data and compare to the given {@code MultiValueMap}.
141         * @since 4.3
142         */
143        public RequestMatcher formData(MultiValueMap<String, String> expectedContent) {
144                return request -> {
145                        HttpInputMessage inputMessage = new HttpInputMessage() {
146                                @Override
147                                public InputStream getBody() throws IOException {
148                                        MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;
149                                        return new ByteArrayInputStream(mockRequest.getBodyAsBytes());
150                                }
151                                @Override
152                                public HttpHeaders getHeaders() {
153                                        return request.getHeaders();
154                                }
155                        };
156                        FormHttpMessageConverter converter = new FormHttpMessageConverter();
157                        assertEquals("Request content", expectedContent, converter.read(null, inputMessage));
158                };
159        }
160
161        /**
162         * Parse the request body and the given String as XML and assert that the
163         * two are "similar" - i.e. they contain the same elements and attributes
164         * regardless of order.
165         * <p>Use of this matcher assumes the
166         * <a href="http://xmlunit.sourceforge.net/">XMLUnit</a> library is available.
167         * @param expectedXmlContent the expected XML content
168         */
169        public RequestMatcher xml(String expectedXmlContent) {
170                return new AbstractXmlRequestMatcher() {
171                        @Override
172                        protected void matchInternal(MockClientHttpRequest request) throws Exception {
173                                xmlHelper.assertXmlEqual(expectedXmlContent, request.getBodyAsString());
174                        }
175                };
176        }
177
178        /**
179         * Parse the request content as {@link Node} and apply the given {@link Matcher}.
180         */
181        public RequestMatcher node(Matcher<? super Node> matcher) {
182                return new AbstractXmlRequestMatcher() {
183                        @Override
184                        protected void matchInternal(MockClientHttpRequest request) throws Exception {
185                                xmlHelper.assertNode(request.getBodyAsString(), matcher);
186                        }
187                };
188        }
189
190        /**
191         * Parse the request content as {@link DOMSource} and apply the given {@link Matcher}.
192         * @see <a href="https://code.google.com/p/xml-matchers/">https://code.google.com/p/xml-matchers/</a>
193         */
194        public RequestMatcher source(Matcher<? super Source> matcher) {
195                return new AbstractXmlRequestMatcher() {
196                        @Override
197                        protected void matchInternal(MockClientHttpRequest request) throws Exception {
198                                xmlHelper.assertSource(request.getBodyAsString(), matcher);
199                        }
200                };
201        }
202
203        /**
204         * Parse the expected and actual strings as JSON and assert the two
205         * are "similar" - i.e. they contain the same attribute-value pairs
206         * regardless of formatting with a lenient checking (extensible, and non-strict array
207         * ordering).
208         * <p>Use of this matcher requires the <a
209         * href="https://jsonassert.skyscreamer.org/">JSONassert</a> library.
210         * @param expectedJsonContent the expected JSON content
211         * @since 5.0.5
212         */
213        public RequestMatcher json(String expectedJsonContent) {
214                return json(expectedJsonContent, false);
215        }
216
217        /**
218         * Parse the request body and the given string as JSON and assert the two
219         * are "similar" - i.e. they contain the same attribute-value pairs
220         * regardless of formatting.
221         * <p>Can compare in two modes, depending on {@code strict} parameter value:
222         * <ul>
223         * <li>{@code true}: strict checking. Not extensible, and strict array ordering.</li>
224         * <li>{@code false}: lenient checking. Extensible, and non-strict array ordering.</li>
225         * </ul>
226         * <p>Use of this matcher requires the <a
227         * href="https://jsonassert.skyscreamer.org/">JSONassert</a> library.
228         * @param expectedJsonContent the expected JSON content
229         * @param strict enables strict checking
230         * @since 5.0.5
231         */
232        public RequestMatcher json(String expectedJsonContent, boolean strict) {
233                return request -> {
234                        try {
235                                MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;
236                                this.jsonHelper.assertJsonEqual(expectedJsonContent, mockRequest.getBodyAsString(), strict);
237                        }
238                        catch (Exception ex) {
239                                throw new AssertionError("Failed to parse expected or actual JSON request content", ex);
240                        }
241                };
242        }
243
244
245        /**
246         * Abstract base class for XML {@link RequestMatcher}'s.
247         */
248        private abstract static class AbstractXmlRequestMatcher implements RequestMatcher {
249
250                @Override
251                public final void match(ClientHttpRequest request) throws IOException, AssertionError {
252                        try {
253                                MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;
254                                matchInternal(mockRequest);
255                        }
256                        catch (Exception ex) {
257                                throw new AssertionError("Failed to parse expected or actual XML request content", ex);
258                        }
259                }
260
261                protected abstract void matchInternal(MockClientHttpRequest request) throws Exception;
262        }
263
264}