001/*
002 * Copyright 2012-2017 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 *      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 */
016
017package org.springframework.boot.test.json;
018
019import java.io.BufferedReader;
020import java.io.Closeable;
021import java.io.File;
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.InputStreamReader;
025import java.io.Reader;
026import java.io.StringReader;
027import java.lang.reflect.Field;
028
029import org.assertj.core.api.Assertions;
030
031import org.springframework.beans.BeansException;
032import org.springframework.beans.factory.ObjectFactory;
033import org.springframework.core.ResolvableType;
034import org.springframework.core.io.ByteArrayResource;
035import org.springframework.core.io.ClassPathResource;
036import org.springframework.core.io.FileSystemResource;
037import org.springframework.core.io.InputStreamResource;
038import org.springframework.core.io.Resource;
039import org.springframework.util.Assert;
040import org.springframework.util.ReflectionUtils;
041import org.springframework.util.ReflectionUtils.FieldCallback;
042
043/**
044 * Base class for AssertJ based JSON marshal testers. Exposes specific Asserts following a
045 * {@code read}, {@code write} or {@code parse} of JSON content. Typically used in
046 * combination with an AssertJ {@link Assertions#assertThat(Object) assertThat} call. For
047 * example: <pre class="code">
048 * public class ExampleObjectJsonTests {
049 *
050 *     private AbstractJsonTester&lt;ExampleObject&gt; json = //...
051 *
052 *     &#064;Test
053 *     public void testWriteJson() {
054 *         ExampleObject object = //...
055 *         assertThat(json.write(object)).isEqualToJson("expected.json");
056 *         assertThat(json.read("expected.json")).isEqualTo(object);
057 *     }
058 *
059 * }
060 * </pre> For a complete list of supported assertions see {@link JsonContentAssert} and
061 * {@link ObjectContentAssert}.
062 * <p>
063 * To use this library JSONAssert must be on the test classpath.
064 *
065 * @param <T> the type under test
066 * @author Phillip Webb
067 * @since 1.4.0
068 * @see JsonContentAssert
069 * @see ObjectContentAssert
070 */
071public abstract class AbstractJsonMarshalTester<T> {
072
073        private Class<?> resourceLoadClass;
074
075        private ResolvableType type;
076
077        /**
078         * Create a new uninitialized {@link AbstractJsonMarshalTester} instance.
079         */
080        protected AbstractJsonMarshalTester() {
081        }
082
083        /**
084         * Create a new {@link AbstractJsonMarshalTester} instance.
085         * @param resourceLoadClass the source class used when loading relative classpath
086         * resources
087         * @param type the type under test
088         */
089        public AbstractJsonMarshalTester(Class<?> resourceLoadClass, ResolvableType type) {
090                Assert.notNull(resourceLoadClass, "ResourceLoadClass must not be null");
091                Assert.notNull(type, "Type must not be null");
092                initialize(resourceLoadClass, type);
093        }
094
095        /**
096         * Initialize the marshal tester for use.
097         * @param resourceLoadClass the source class used when loading relative classpath
098         * resources
099         * @param type the type under test
100         */
101        protected final void initialize(Class<?> resourceLoadClass, ResolvableType type) {
102                if (this.resourceLoadClass == null && this.type == null) {
103                        this.resourceLoadClass = resourceLoadClass;
104                        this.type = type;
105                }
106        }
107
108        /**
109         * Return the type under test.
110         * @return the type under test
111         */
112        protected final ResolvableType getType() {
113                return this.type;
114        }
115
116        /**
117         * Return class used to load relative resources.
118         * @return the resource load class
119         */
120        protected final Class<?> getResourceLoadClass() {
121                return this.resourceLoadClass;
122        }
123
124        /**
125         * Return {@link JsonContent} from writing the specific value.
126         * @param value the value to write
127         * @return the {@link JsonContent}
128         * @throws IOException on write error
129         */
130        public JsonContent<T> write(T value) throws IOException {
131                verify();
132                Assert.notNull(value, "Value must not be null");
133                String json = writeObject(value, this.type);
134                return new JsonContent<T>(this.resourceLoadClass, this.type, json);
135        }
136
137        /**
138         * Return the object created from parsing the specific JSON bytes.
139         * @param jsonBytes the source JSON bytes
140         * @return the resulting object
141         * @throws IOException on parse error
142         */
143        public T parseObject(byte[] jsonBytes) throws IOException {
144                verify();
145                return parse(jsonBytes).getObject();
146        }
147
148        /**
149         * Return {@link ObjectContent} from parsing the specific JSON bytes.
150         * @param jsonBytes the source JSON bytes
151         * @return the {@link ObjectContent}
152         * @throws IOException on parse error
153         */
154        public ObjectContent<T> parse(byte[] jsonBytes) throws IOException {
155                verify();
156                Assert.notNull(jsonBytes, "JsonBytes must not be null");
157                return read(new ByteArrayResource(jsonBytes));
158        }
159
160        /**
161         * Return the object created from parsing the specific JSON String.
162         * @param jsonString the source JSON string
163         * @return the resulting object
164         * @throws IOException on parse error
165         */
166        public T parseObject(String jsonString) throws IOException {
167                verify();
168                return parse(jsonString).getObject();
169        }
170
171        /**
172         * Return {@link ObjectContent} from parsing the specific JSON String.
173         * @param jsonString the source JSON string
174         * @return the {@link ObjectContent}
175         * @throws IOException on parse error
176         */
177        public ObjectContent<T> parse(String jsonString) throws IOException {
178                verify();
179                Assert.notNull(jsonString, "JsonString must not be null");
180                return read(new StringReader(jsonString));
181        }
182
183        /**
184         * Return the object created from reading from the specified classpath resource.
185         * @param resourcePath the source resource path. May be a full path or a path relative
186         * to the {@code resourceLoadClass} passed to the constructor
187         * @return the resulting object
188         * @throws IOException on read error
189         */
190        public T readObject(String resourcePath) throws IOException {
191                verify();
192                return read(resourcePath).getObject();
193        }
194
195        /**
196         * Return {@link ObjectContent} from reading from the specified classpath resource.
197         * @param resourcePath the source resource path. May be a full path or a path relative
198         * to the {@code resourceLoadClass} passed to the constructor
199         * @return the {@link ObjectContent}
200         * @throws IOException on read error
201         */
202        public ObjectContent<T> read(String resourcePath) throws IOException {
203                verify();
204                Assert.notNull(resourcePath, "ResourcePath must not be null");
205                return read(new ClassPathResource(resourcePath, this.resourceLoadClass));
206        }
207
208        /**
209         * Return the object created from reading from the specified file.
210         * @param file the source file
211         * @return the resulting object
212         * @throws IOException on read error
213         */
214        public T readObject(File file) throws IOException {
215                verify();
216                return read(file).getObject();
217        }
218
219        /**
220         * Return {@link ObjectContent} from reading from the specified file.
221         * @param file the source file
222         * @return the {@link ObjectContent}
223         * @throws IOException on read error
224         */
225        public ObjectContent<T> read(File file) throws IOException {
226                verify();
227                Assert.notNull(file, "File must not be null");
228                return read(new FileSystemResource(file));
229        }
230
231        /**
232         * Return the object created from reading from the specified input stream.
233         * @param inputStream the source input stream
234         * @return the resulting object
235         * @throws IOException on read error
236         */
237        public T readObject(InputStream inputStream) throws IOException {
238                verify();
239                return read(inputStream).getObject();
240        }
241
242        /**
243         * Return {@link ObjectContent} from reading from the specified input stream.
244         * @param inputStream the source input stream
245         * @return the {@link ObjectContent}
246         * @throws IOException on read error
247         */
248        public ObjectContent<T> read(InputStream inputStream) throws IOException {
249                verify();
250                Assert.notNull(inputStream, "InputStream must not be null");
251                return read(new InputStreamResource(inputStream));
252        }
253
254        /**
255         * Return the object created from reading from the specified resource.
256         * @param resource the source resource
257         * @return the resulting object
258         * @throws IOException on read error
259         */
260        public T readObject(Resource resource) throws IOException {
261                verify();
262                return read(resource).getObject();
263        }
264
265        /**
266         * Return {@link ObjectContent} from reading from the specified resource.
267         * @param resource the source resource
268         * @return the {@link ObjectContent}
269         * @throws IOException on read error
270         */
271        public ObjectContent<T> read(Resource resource) throws IOException {
272                verify();
273                Assert.notNull(resource, "Resource must not be null");
274                InputStream inputStream = resource.getInputStream();
275                T object = readObject(inputStream, this.type);
276                closeQuietly(inputStream);
277                return new ObjectContent<T>(this.type, object);
278        }
279
280        /**
281         * Return the object created from reading from the specified reader.
282         * @param reader the source reader
283         * @return the resulting object
284         * @throws IOException on read error
285         */
286        public T readObject(Reader reader) throws IOException {
287                verify();
288                return read(reader).getObject();
289        }
290
291        /**
292         * Return {@link ObjectContent} from reading from the specified reader.
293         * @param reader the source reader
294         * @return the {@link ObjectContent}
295         * @throws IOException on read error
296         */
297        public ObjectContent<T> read(Reader reader) throws IOException {
298                verify();
299                Assert.notNull(reader, "Reader must not be null");
300                T object = readObject(reader, this.type);
301                closeQuietly(reader);
302                return new ObjectContent<T>(this.type, object);
303        }
304
305        private void closeQuietly(Closeable closeable) {
306                try {
307                        closeable.close();
308                }
309                catch (IOException ex) {
310                }
311        }
312
313        private void verify() {
314                Assert.state(this.resourceLoadClass != null,
315                                "Uninitialized JsonMarshalTester (ResourceLoadClass is null)");
316                Assert.state(this.type != null, "Uninitialized JsonMarshalTester (Type is null)");
317        }
318
319        /**
320         * Write the specified object to a JSON string.
321         * @param value the source value (never {@code null})
322         * @param type the resulting type (never {@code null})
323         * @return the JSON string
324         * @throws IOException on write error
325         */
326        protected abstract String writeObject(T value, ResolvableType type)
327                        throws IOException;
328
329        /**
330         * Read from the specified input stream to create an object of the specified type. The
331         * default implementation delegates to {@link #readObject(Reader, ResolvableType)}.
332         * @param inputStream the source input stream (never {@code null})
333         * @param type the resulting type (never {@code null})
334         * @return the resulting object
335         * @throws IOException on read error
336         */
337        protected T readObject(InputStream inputStream, ResolvableType type)
338                        throws IOException {
339                BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
340                return readObject(reader, type);
341        }
342
343        /**
344         * Read from the specified reader to create an object of the specified type.
345         * @param reader the source reader (never {@code null})
346         * @param type the resulting type (never {@code null})
347         * @return the resulting object
348         * @throws IOException on read error
349         */
350        protected abstract T readObject(Reader reader, ResolvableType type)
351                        throws IOException;
352
353        /**
354         * Utility class used to support field initialization. Used by subclasses to support
355         * {@code initFields}.
356         *
357         * @param <M> The marshaller type
358         */
359        protected static abstract class FieldInitializer<M> {
360
361                private final Class<?> testerClass;
362
363                @SuppressWarnings("rawtypes")
364                protected FieldInitializer(
365                                Class<? extends AbstractJsonMarshalTester> testerClass) {
366                        Assert.notNull(testerClass, "TesterClass must not be null");
367                        this.testerClass = testerClass;
368                }
369
370                public void initFields(final Object testInstance, final M marshaller) {
371                        Assert.notNull(testInstance, "TestInstance must not be null");
372                        Assert.notNull(marshaller, "Marshaller must not be null");
373                        initFields(testInstance, new ObjectFactory<M>() {
374
375                                @Override
376                                public M getObject() throws BeansException {
377                                        return marshaller;
378                                }
379
380                        });
381                }
382
383                public void initFields(final Object testInstance,
384                                final ObjectFactory<M> marshaller) {
385                        Assert.notNull(testInstance, "TestInstance must not be null");
386                        Assert.notNull(marshaller, "Marshaller must not be null");
387                        ReflectionUtils.doWithFields(testInstance.getClass(), new FieldCallback() {
388
389                                @Override
390                                public void doWith(Field field)
391                                                throws IllegalArgumentException, IllegalAccessException {
392                                        doWithField(field, testInstance, marshaller);
393                                }
394
395                        });
396                }
397
398                protected void doWithField(Field field, Object test,
399                                ObjectFactory<M> marshaller) {
400                        if (this.testerClass.isAssignableFrom(field.getType())) {
401                                ReflectionUtils.makeAccessible(field);
402                                Object existingValue = ReflectionUtils.getField(field, test);
403                                if (existingValue == null) {
404                                        setupField(field, test, marshaller);
405                                }
406                        }
407                }
408
409                private void setupField(Field field, Object test, ObjectFactory<M> marshaller) {
410                        ResolvableType type = ResolvableType.forField(field).getGeneric();
411                        ReflectionUtils.setField(field, test,
412                                        createTester(test.getClass(), type, marshaller.getObject()));
413                }
414
415                protected abstract AbstractJsonMarshalTester<Object> createTester(
416                                Class<?> resourceLoadClass, ResolvableType type, M marshaller);
417
418        }
419
420}