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.reactive.server;
018
019import java.nio.charset.StandardCharsets;
020import java.util.Map;
021import java.util.Optional;
022import java.util.function.Consumer;
023
024import javax.xml.xpath.XPathExpressionException;
025
026import org.hamcrest.Matcher;
027
028import org.springframework.http.HttpHeaders;
029import org.springframework.lang.Nullable;
030import org.springframework.test.util.XpathExpectationsHelper;
031import org.springframework.util.Assert;
032import org.springframework.util.MimeType;
033
034/**
035 * XPath assertions for the {@link WebTestClient}.
036 *
037 * @author Eric Deandrea
038 * @author Rossen Stoyanchev
039 * @since 5.1
040 */
041public class XpathAssertions {
042
043        private final WebTestClient.BodyContentSpec bodySpec;
044
045        private final XpathExpectationsHelper xpathHelper;
046
047
048        XpathAssertions(WebTestClient.BodyContentSpec spec,
049                        String expression, @Nullable Map<String, String> namespaces, Object... args) {
050
051                this.bodySpec = spec;
052                this.xpathHelper = initXpathHelper(expression, namespaces, args);
053        }
054
055        private static XpathExpectationsHelper initXpathHelper(
056                        String expression, @Nullable Map<String, String> namespaces, Object[] args) {
057
058                try {
059                        return new XpathExpectationsHelper(expression, namespaces, args);
060                }
061                catch (XPathExpressionException ex) {
062                        throw new AssertionError("XML parsing error", ex);
063                }
064        }
065
066
067        /**
068         * Delegates to {@link XpathExpectationsHelper#assertString(byte[], String, String)}.
069         */
070        public WebTestClient.BodyContentSpec isEqualTo(String expectedValue) {
071                return assertWith(() -> this.xpathHelper.assertString(getContent(), getCharset(), expectedValue));
072        }
073
074        /**
075         * Delegates to {@link XpathExpectationsHelper#assertNumber(byte[], String, Double)}.
076         */
077        public WebTestClient.BodyContentSpec isEqualTo(Double expectedValue) {
078                return assertWith(() -> this.xpathHelper.assertNumber(getContent(), getCharset(), expectedValue));
079        }
080
081        /**
082         * Delegates to {@link XpathExpectationsHelper#assertBoolean(byte[], String, boolean)}.
083         */
084        public WebTestClient.BodyContentSpec isEqualTo(boolean expectedValue) {
085                return assertWith(() -> this.xpathHelper.assertBoolean(getContent(), getCharset(), expectedValue));
086        }
087
088        /**
089         * Delegates to {@link XpathExpectationsHelper#exists(byte[], String)}.
090         */
091        public WebTestClient.BodyContentSpec exists() {
092                return assertWith(() -> this.xpathHelper.exists(getContent(), getCharset()));
093        }
094
095        /**
096         * Delegates to {@link XpathExpectationsHelper#doesNotExist(byte[], String)}.
097         */
098        public WebTestClient.BodyContentSpec doesNotExist() {
099                return assertWith(() -> this.xpathHelper.doesNotExist(getContent(), getCharset()));
100        }
101
102        /**
103         * Delegates to {@link XpathExpectationsHelper#assertNodeCount(byte[], String, int)}.
104         */
105        public WebTestClient.BodyContentSpec nodeCount(int expectedCount) {
106                return assertWith(() -> this.xpathHelper.assertNodeCount(getContent(), getCharset(), expectedCount));
107        }
108
109        /**
110         * Delegates to {@link XpathExpectationsHelper#assertString(byte[], String, Matcher)}.
111         * @since 5.1
112         */
113        public WebTestClient.BodyContentSpec string(Matcher<? super String> matcher){
114                return assertWith(() -> this.xpathHelper.assertString(getContent(), getCharset(), matcher));
115        }
116
117        /**
118         * Delegates to {@link XpathExpectationsHelper#assertNumber(byte[], String, Matcher)}.
119         * @since 5.1
120         */
121        public WebTestClient.BodyContentSpec number(Matcher<? super Double> matcher){
122                return assertWith(() -> this.xpathHelper.assertNumber(getContent(), getCharset(), matcher));
123        }
124
125        /**
126         * Delegates to {@link XpathExpectationsHelper#assertNodeCount(byte[], String, Matcher)}.
127         * @since 5.1
128         */
129        public WebTestClient.BodyContentSpec nodeCount(Matcher<Integer> matcher){
130                return assertWith(() -> this.xpathHelper.assertNodeCount(getContent(), getCharset(), matcher));
131        }
132
133        /**
134         * Consume the result of the XPath evaluation as a String.
135         * @since 5.1
136         */
137        public WebTestClient.BodyContentSpec string(Consumer<String> consumer){
138                return assertWith(() -> {
139                        String value = this.xpathHelper.evaluateXpath(getContent(), getCharset(), String.class);
140                        consumer.accept(value);
141                });
142        }
143
144        /**
145         * Consume the result of the XPath evaluation as a Double.
146         * @since 5.1
147         */
148        public WebTestClient.BodyContentSpec number(Consumer<Double> consumer){
149                return assertWith(() -> {
150                        Double value = this.xpathHelper.evaluateXpath(getContent(), getCharset(), Double.class);
151                        consumer.accept(value);
152                });
153        }
154
155        /**
156         * Consume the count of nodes as result of the XPath evaluation.
157         * @since 5.1
158         */
159        public WebTestClient.BodyContentSpec nodeCount(Consumer<Integer> consumer){
160                return assertWith(() -> {
161                        Integer value = this.xpathHelper.evaluateXpath(getContent(), getCharset(), Integer.class);
162                        consumer.accept(value);
163                });
164        }
165
166        private WebTestClient.BodyContentSpec assertWith(CheckedExceptionTask task) {
167                try {
168                        task.run();
169                }
170                catch (Exception ex) {
171                        throw new AssertionError("XML parsing error", ex);
172                }
173                return this.bodySpec;
174        }
175
176        private byte[] getContent() {
177                byte[] body = this.bodySpec.returnResult().getResponseBody();
178                Assert.notNull(body, "Expected body content");
179                return body;
180        }
181
182        private String getCharset() {
183                return Optional.of(this.bodySpec.returnResult())
184                                .map(EntityExchangeResult::getResponseHeaders)
185                                .map(HttpHeaders::getContentType)
186                                .map(MimeType::getCharset)
187                                .orElse(StandardCharsets.UTF_8)
188                                .name();
189        }
190
191
192        @Override
193        public boolean equals(Object obj) {
194                throw new AssertionError("Object#equals is disabled " +
195                                "to avoid being used in error instead of XPathAssertions#isEqualTo(String).");
196        }
197
198        @Override
199        public int hashCode() {
200                return super.hashCode();
201        }
202
203
204        /**
205         * Lets us be able to use lambda expressions that could throw checked exceptions, since
206         * {@link XpathExpectationsHelper} throws {@link Exception} on its methods.
207         */
208        private interface CheckedExceptionTask {
209
210                void run() throws Exception;
211
212        }
213}