diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cfbd418 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.class +.settings +.project +.classpath +target +bin +#intellij +.idea/ +*.iml +*.iws +spy.log +*.log +src/test/java/generated +*/generated-test-sources diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1db72c8 --- /dev/null +++ b/README.md @@ -0,0 +1,140 @@ + +# Restdoc Helper + +## Overview + +The main goal of this project is to simplify the documentation process of your RESTFul services by combining hand-written using [Asciidoctor][3] docs, [Sprinfox static docs][1] generated docs or [Spring REST Docs][10] generated docs. +Restdoc helper is a Maven plugin that generates [Asciidoctor][3] files and [FieldDescriptors][7] from [Swagger 2][6] annotations + +The purpose of [Spring REST Docs][10] is to document your API with auto-generated snippets produced with [Spring MVC Test][4]. +To achieve that, it's necessary to document all fields of data classes involved in your call-flows. If as me you use [Swagger 2][6] to describe your API contracts, the job is almost done. +What'is missing is the translation of your swagger annotations to a [FieldDescriptors][7]. This is the purpose of this contribution. +It's also possible to generate asciidoctor files only that describe your model from custom annotation. + +## Doc generation + +### Add dependencies to your pom + +``` + + com.pconil.restdoc + restdoc-annotation + 1.0-SNAPSHOT + ` + + ... + + + com.pconil.restdoc + restdoc-helper-plugin + 1.0-SNAPSHOT + + ${basedir}/target/classes/ + com.pconil.restdoc.model + ${basedir}/target/generated-test-sources + ${basedir}/target/generated-snippets + + + + generate + generate-test-sources + + restdoc-helper + + + + + + org.springframework + spring-web + 4.3.3.RELEASE + + + + ... + +``` + +### Howto Generate adoc files from swagger annotations + +As simple as adding @InspectToDocument to your model class + +``` +@InspectToDocument(description = "This is the Class1 description") +public class ClassSwaggerDTO { + @ApiModelProperty(required = true, value="This is the field field1") + private String field1; + + @ApiModelProperty(value="This is the optional field field2") + private String field2 = null; + + @ApiModelProperty(required = true, value="This is the field field3") + private String field3 = null; + + public ClassSwaggerDTO(String field1, String field2) { + this.field1 = field1; + this.field2 = field2; + } +} +``` + + +### And If you don't use swagger or want to document classes that doesn't contain @ApiModelProperty + +Add a @AsciidocAnnotation to your fields + +``` +@InspectToDocument(description = "This is the Class1 description") +public class Class1DTO { + @AsciidocAnnotation(description="This field describe name of Class1DTO", constraints = "Length must be between 4 and 6") + String field1; + + @AsciidocAnnotation(description="field 2") + String field2 = null; + + public Class1DTO(String field1, String field2) { + this.field1 = field1; + this.field2 = field2; + } +} +``` + +## Go further + +See the springboot-sample to see how it works. + +## Prerequisite + +Java 8, maven 3 + +## Building from source + +You will need Java 8 or later to build restdoc-helper. It is built using [maven][2]: + +``` +mvn clean install +``` + +## Contributing + +There are several ways to contribute to restdoc-helper: + + - Open a [pull request][12]. + - Ask and answer questions on Stack Overflow using the [`restdoc-helper`][15] tag. + +## Licence + +Restdoc helper is open source software released under the [Apache 2.0 license][14]. + + +[1]: https://springfox.github.io/springfox/docs/snapshot/#configuring-springfox-staticdocs +[2]: https://maven.apache.org/download.cgi +[3]: http://asciidoctor.org +[4]: http://docs.spring.io/spring-framework/docs/4.1.x/spring-framework-reference/htmlsingle/#spring-mvc-test-framework +[5]: https://developer.github.com/v3/ +[6]: http://swagger.io/specification/ +[7]: http://docs.spring.io/spring-restdocs/docs/current/reference/html5/#documenting-your-api-request-response-payloads-reusing-field-descriptors +[10]: http://docs.spring.io/spring-restdocs/docs/current/reference/html5/ +[12]: https://help.github.com/articles/using-pull-requests/ +[14]: http://www.apache.org/licenses/LICENSE-2.0.html +[15]: http://stackoverflow.com/tags/restdoc-helper diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..891e1ad --- /dev/null +++ b/pom.xml @@ -0,0 +1,132 @@ + + + 4.0.0 + + restdoc-annotation + restdoc-helper-plugin + springboot-sample + + + com.pconil.restdoc + restdoc-helper + 1.0-SNAPSHOT + pom + + + UTF-8 + UTF-8 + 1.8 + 1.4.0.RELEASE + 4.3.2.RELEASE + 2.5.0 + 4.2.5.RELEASE + 1.1.2.RELEASE + 4.12 + 1.5.9 + + + + + + org.springframework.restdocs + spring-restdocs-mockmvc + ${spring-restdoc.version} + + + + io.springfox + springfox-swagger2 + ${springfox.version} + + + io.swagger + swagger-models + ${swagger-model.version} + + + io.springfox + springfox-staticdocs + ${springfox.version} + + + + junit + junit + ${junit.version} + + + org.hamcrest + hamcrest-all + 1.3 + + + org.mockito + mockito-core + 1.10.19 + + + com.jayway.jsonpath + json-path + 2.0.0 + + + org.codehaus.plexus + plexus-utils + 2.0.5 + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 1.4.1 + + + enforce + + + + + + + enforce + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + ${java.version} + ${java.version} + + + + org.springframework.boot + spring-boot-maven-plugin + 1.4.1.RELEASE + + + + + + + spring-releases + https://repo.spring.io/libs-release + + + central + http://repo1.maven.org/maven2/ + + + + \ No newline at end of file diff --git a/restdoc-annotation/pom.xml b/restdoc-annotation/pom.xml new file mode 100644 index 0000000..77c6d09 --- /dev/null +++ b/restdoc-annotation/pom.xml @@ -0,0 +1,14 @@ + + + + restdoc-helper + com.pconil.restdoc + 1.0-SNAPSHOT + + 4.0.0 + + restdoc-annotation + + \ No newline at end of file diff --git a/restdoc-annotation/src/main/java/com/pconil/restdoc/annotation/AsciidocAnnotation.java b/restdoc-annotation/src/main/java/com/pconil/restdoc/annotation/AsciidocAnnotation.java new file mode 100644 index 0000000..5e78c30 --- /dev/null +++ b/restdoc-annotation/src/main/java/com/pconil/restdoc/annotation/AsciidocAnnotation.java @@ -0,0 +1,54 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to document field. + * @author patrice_conil + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Inherited +@Documented +public @interface AsciidocAnnotation { + /** + * Contains description of the annotated field. + * + * @return the description of the annotated field + */ + String description() default ""; + + /** + * Contains constraints to apply to the annotated field. + * + * @return the constrains that apply to the annotated field + */ + String constraints() default ""; + + /** + * Tells if the field is mandatory or not. + * + * @return true if field is mandatory, false otherwise + */ + boolean required() default true; +} diff --git a/restdoc-annotation/src/main/java/com/pconil/restdoc/annotation/InspectToDocument.java b/restdoc-annotation/src/main/java/com/pconil/restdoc/annotation/InspectToDocument.java new file mode 100644 index 0000000..70d5fc8 --- /dev/null +++ b/restdoc-annotation/src/main/java/com/pconil/restdoc/annotation/InspectToDocument.java @@ -0,0 +1,39 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used to tell AnnotationParser to generate Class.adoc file for this class. + * @author patrice_conil + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +@Documented +public @interface InspectToDocument { + /** + * Description of the class in restdoc corresponding file. + * @return the description of the class + */ + String description() default ""; +} diff --git a/restdoc-helper-plugin/pom.xml b/restdoc-helper-plugin/pom.xml new file mode 100644 index 0000000..eff342f --- /dev/null +++ b/restdoc-helper-plugin/pom.xml @@ -0,0 +1,256 @@ + + + 4.0.0 + + + com.pconil.restdoc + restdoc-helper + 1.0-SNAPSHOT + + + restdoc-helper-plugin + maven-plugin + + + + org.apache.maven + maven-plugin-api + 3.3.9 + + + org.apache.maven + maven-model + 3.3.9 + + + org.apache.maven.plugin-testing + maven-plugin-testing-harness + 3.3.0 + test + + + org.apache.maven + maven-core + 3.3.9 + + + com.google.guava + guava + + + test + + + com.google.guava + guava + 18.0 + + + org.apache.maven + maven-compat + 3.3.9 + test + + + junit + junit + test + + + io.swagger + swagger-annotations + 1.5.10 + + + org.slf4j + slf4j-api + + + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + + org.slf4j + slf4j-api + 1.7.21 + + + org.slf4j + slf4j-jdk14 + 1.7.21 + + + org.assertj + assertj-core + + 3.5.2 + test + + + com.pconil.restdoc + restdoc-annotation + 1.0-SNAPSHOT + + + org.apache.maven.plugins + maven-plugin-plugin + 3.3 + + + org.apache.maven + maven-plugin-api + + + org.apache.maven + maven-settings + + + org.apache.maven + maven-plugin-annotations + + + org.codehaus.plexus + plexus-component-annotations + + + org.codehaus.plexus + plexus-container-default + + + org.apache.maven.doxia + doxia-sink-api + + + org.apache.maven.doxia + doxia-logging-api + + + org.codehaus.plexus + plexus-velocity + + + org.codehaus.plexus + plexus-interpolation + + + org.apache.maven + maven-core + + + org.apache.maven.reporting + maven-reporting-impl + + + org.apache.maven.wagon + wagon-provider-api + + + org.apache.maven + maven-repository-metadata + + + org.apache.maven + maven-model + + + org.apache.maven + maven-artifact + + + org.apache.velocity + velocity + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.5.1 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-plugin-plugin + 3.5 + + + true + + + + mojo-descriptor + + descriptor + + process-classes + + + + help-goal + + helpmojo + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 1.5.3 + + + generate-docs + + prepare-package + + process-asciidoc + + + html + book + + + ${project.build.directory}/generated-snippets + + + + + + + + maven-resources-plugin + + + copy-resources + prepare-package + + copy-resources + + + ${project.build.outputDirectory}/static/docs + + + ${project.build.directory}/generated-docs + + + + + + + + + + \ No newline at end of file diff --git a/restdoc-helper-plugin/src/main/asciidoc/ApiDocumentation.adoc b/restdoc-helper-plugin/src/main/asciidoc/ApiDocumentation.adoc new file mode 100644 index 0000000..e58e231 --- /dev/null +++ b/restdoc-helper-plugin/src/main/asciidoc/ApiDocumentation.adoc @@ -0,0 +1,26 @@ += Live API Getting Started Guide +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 4 +:sectlinks: + +[introduction] += Introduction + Live API is a RESTful microservice to retrieve data from TV Live Domain (Channels, Programs and so on) + +[[com.pconil.restdoc.model]] += Models +Object models used to describe resources + +:leveloffset: +1 +include::{snippets}/Class1DTO.adoc[] +include::{snippets}/Class2DTO.adoc[] + +include::{snippets}/SimilarContent.adoc[] + +:leveloffset: -1 + + + diff --git a/restdoc-helper-plugin/src/main/java/com/pconil/restdoc/AbstractParser.java b/restdoc-helper-plugin/src/main/java/com/pconil/restdoc/AbstractParser.java new file mode 100644 index 0000000..afb9c42 --- /dev/null +++ b/restdoc-helper-plugin/src/main/java/com/pconil/restdoc/AbstractParser.java @@ -0,0 +1,259 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc; + +import com.pconil.restdoc.annotation.InspectToDocument; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.List; +import java.util.regex.Pattern; + +/** + * This parser produces target/ClassName.restdoc files for all classes matching package-name or its subpackages. + * + * @author patrice_conil + */ +@InspectToDocument(description = "Parser used to generate restdoc files from swagger annotation") +abstract class AbstractParser { + + + /** + * The logger. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractParser.class); + + /** + * File where to write restdoc file. + */ + FileOutputStream adocFile = null; + + /** + * Java dir. + */ + String targetJavaDirName = null; + + /** + * Directory where we want to put restdoc files. + */ + String targetAdocDirName = null; + + /** + * Directory that contains .class to parse. + */ + File sourceDir; + + /** + * Directory where we want to put java file. + */ + File targetJavaDir = null; + + /** + * Directory where we want to put adoc file. + */ + File targetAdocDir = null; + + /** + * Package name. + */ + String packageName; + + /** + * AbstractParser Constructor. + * + * @param packageName name of package that we want to parse + * @param adocDirName project for which we want to generate restdoc and java files + * @param javaDirName project for which we want to generate restdoc and java files + * @param source directory where .class will be found + * @throws ParserException if packageName is malformed or target creation isn't possible + */ + AbstractParser(String packageName, String adocDirName, String javaDirName, String source) throws ParserException { + + this.packageName = packageName; + targetAdocDirName = adocDirName; + targetJavaDirName = javaDirName; + + targetJavaDir = new File(targetJavaDirName); + targetAdocDir = new File(targetAdocDirName); + + sourceDir = new File(source); + + if (!sourceDir.isDirectory()) { + logErrorAndThrowParserException(String.format(" %s is not a directory", targetJavaDirName)); + + } + validatePackageName(packageName); + + if (!targetJavaDir.exists() && !targetJavaDir.mkdirs()) { + logErrorAndThrowParserException(String.format("Can't create directory %s for java sources", + targetJavaDirName)); + } + if (!targetAdocDir.exists() && !targetAdocDir.mkdirs()) { + logErrorAndThrowParserException(String.format("Can't create directory %s for restdoc files", + targetAdocDirName)); + } + } + + + /** + * Parses packageName and its sub packages to generate Class.restdoc files for Class tagged @InspectToDocument. + * Class.restdoc files are generated in target/generated-snippet/com.pconil.restdoc.model targetJavaDir. + * + * @throws ParserException if we can't create Class.adhoc files in target/generated-snippets/com.pconil.restdoc + * .model. + */ + void parse() throws ParserException { + + List classes; + boolean hasDocumentedField; + + ParserClassLoader classLoader = null; + + try { + // Add source url to local classloader + classLoader = new ParserClassLoader(sourceDir); + classes = classLoader.getClasses(sourceDir.getAbsolutePath(), packageName); + + for (Class c : classes) { + LOGGER.debug("Parsing class {} to generate java code", c.getSimpleName()); + if (c.isAnnotationPresent(InspectToDocument.class)) { + hasDocumentedField = false; + // Parse all fields declared in this class + initializeFields(); + for (Field field : c.getDeclaredFields()) { + hasDocumentedField = parseField(c, field, hasDocumentedField); + } + if (hasDocumentedField) { + completeFields(); + completeClass(); + writeClass(); + } else { + LOGGER.debug(String.format("You declared your class %s as documented " + + "but there is no documented field in it", + c.getSimpleName())); + } + } + } + classLoader.restoreLoader(); + } catch (ClassNotFoundException | IOException e) { + logErrorAndThrowParserException("Class not found while parsing your model"); + } + } + + /** + * Logs the error message and send a ParserException. + * + * @param message the error message + */ + protected void logErrorAndThrowParserException(String message) { + LOGGER.error(message); + throw new ParserException(message); + } + + /** + * Parses current field to check if it's a field to document. + * + * @param c the class we're parsing + * @param field the current field to parse + * @param hasDocumentation tells if class c already have a documented field + * @return hasDocumented || this field is documented + */ + protected abstract boolean parseField(Class c, Field field, boolean hasDocumentation); + + /** + * Initializes header(s) for fields. + * + * @param c the classe for which we initialize field header + */ + protected abstract void writeFieldStart(Class c); + + /** + * Initializes info buffer for class c. + * + * @param c the class we're parsing + * @throws IOException if write failed + */ + protected abstract void writeClassStart(Class c) throws IOException; + + /** + * Initializes writing for fields. + * + * @throws IOException if something goes wrong with file creation + */ + protected abstract void initializeFields() throws IOException; + + /** + * Generates the file. + * + * @return the FileOutputStream if all is ok. + */ + protected abstract FileOutputStream writeClass(); + + + /** + * Ends class writing. + * + * @throws IOException if write failed + */ + protected abstract void completeClass() throws IOException; + + /** + * Ends field writing. + * + * @throws IOException if write failed + */ + protected abstract void completeFields() throws IOException; + + /** + * Check if packageName is well formed ... but not if its a java keyword. + * + * @param packageName the name of the package to check + * @throws ParserException if the package name is invalid + */ + private void validatePackageName(String packageName) throws ParserException { + Pattern p = Pattern.compile("^[a-zA-Z_\\$][\\w\\$]*(?:\\.[a-zA-Z_\\$][\\w\\$]*)*$"); + if (!p.matcher(packageName).matches()) { + logErrorAndThrowParserException(String.format("invalid package name: %s", packageName)); + } + } + + /** + * Create file targetDir/simpleName+suffix. + * + * @param simpleName the class name + * @param targetDir the target directory + * @param suffix the file suffix to add + * @return a file descriptor + * @throws ParserException if creation fail + */ + FileOutputStream createClassFile(String simpleName, String targetDir, String suffix) throws ParserException { + FileOutputStream file = null; + String classFileName = targetDir + "/" + simpleName + suffix; + try { + file = new FileOutputStream(classFileName, false); + LOGGER.debug("File {} created", classFileName); + } catch (FileNotFoundException e) { + logErrorAndThrowParserException(String.format("Can't create file %s", classFileName)); + } + return file; + } + +} diff --git a/restdoc-helper-plugin/src/main/java/com/pconil/restdoc/Annotation2AsciiDocParser.java b/restdoc-helper-plugin/src/main/java/com/pconil/restdoc/Annotation2AsciiDocParser.java new file mode 100644 index 0000000..157afca --- /dev/null +++ b/restdoc-helper-plugin/src/main/java/com/pconil/restdoc/Annotation2AsciiDocParser.java @@ -0,0 +1,196 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc; + +import com.pconil.restdoc.annotation.AsciidocAnnotation; +import com.pconil.restdoc.annotation.InspectToDocument; +import io.swagger.annotations.ApiModelProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; + +/** + * This parser produces target/ClassName.restdoc files for all classes matching package-name or its subpackages. + * + * @author patrice_conil + */ +@InspectToDocument(description = "Parser used to generate restdoc files from swagger annotation") +public class Annotation2AsciiDocParser extends AbstractParser { + + /** + * Logger. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(Annotation2AsciiDocParser.class); + + + /** + * Annotation2AsciiDocParser Constructor. + * + * @param packageName name of package that we want to parse + * @param adocDirName project for which we want to generate restdoc and java files + * @param javaDirName project for which we want to generate restdoc and java files + * @param source directory where .class will be found + * @throws ParserException if packageName is malformed or target creation isn't possible + */ + public Annotation2AsciiDocParser(String packageName, String adocDirName, String javaDirName, + String source) throws ParserException { + super(packageName, adocDirName, javaDirName, source); + } + + @Override + protected boolean parseField(Class c, Field field, boolean hasDocumentation) { + boolean isDocumented = false; + if (field.isAnnotationPresent(AsciidocAnnotation.class) || field.isAnnotationPresent(ApiModelProperty.class)) { + try { + //Is it the first documented field of the class? + if (!hasDocumentation) { + isDocumented = true; + writeClassStart(c); + writeFieldStart(c); + } + // Iterates all the annotations available in the method + for (Annotation anno : field.getDeclaredAnnotations()) { + LOGGER.debug("\tAnnotation {} in class {} for field {} \n", + anno.annotationType().getSimpleName(), + c.getSimpleName(), field.getName()); + if (anno instanceof AsciidocAnnotation) { + writeFieldInfo(field, (AsciidocAnnotation) anno, adocFile); + } else if (anno instanceof ApiModelProperty) { + writeFieldInfo(field, (ApiModelProperty) anno, adocFile); + } + } + } catch (Throwable ex) { + ex.printStackTrace(); + } + } + return hasDocumentation || isDocumented; + } + + @Override + protected void writeFieldStart(Class c) { + + } + + @Override + protected void writeClassStart(Class c) throws IOException { + adocFile = createClassFile(c.getSimpleName(), targetAdocDirName, ".adoc"); + writeClassHeader(c, adocFile); + writeFieldHeader(adocFile); + } + + @Override + protected void initializeFields() throws IOException { + } + + @Override + protected FileOutputStream writeClass() { + return null; + } + + @Override + protected void completeClass() throws IOException { + + } + + @Override + protected void completeFields() throws IOException { + writeFieldFooter(adocFile); + } + + /** + * Write field footer to stream. + * + * @param stream file where to write + * @throws IOException If something goes wrong wile writing to file + */ + private void writeFieldFooter(FileOutputStream stream) throws IOException { + byte[] content = Constant.FIELD_FOOTER.getBytes(StandardCharsets.UTF_8); + stream.write(content); + } + + /** + * Writes Field informations contained in anno in restdoc file. + * + * @param field Field + * @param anno An AsciidocAnnotation + * @param stream file where to write + * @throws IOException If something goes wrong wile writing to file + */ + private void writeFieldInfo(Field field, AsciidocAnnotation anno, FileOutputStream stream) throws IOException { + byte[] content = String.format(Constant.FIELD_FORMAT, field.getName(), field.getType().getSimpleName(), + anno.description(), anno.constraints()) + .getBytes(StandardCharsets.UTF_8); + stream.write(content); + stream.flush(); + } + + /** + * Writes Field informations contained in anno in restdoc file. + * + * @param field Field + * @param anno An ApiModelProperty annotation + * @param stream file where to write + * @throws IOException If something goes wrong wile writing to file + */ + private void writeFieldInfo(Field field, ApiModelProperty anno, + FileOutputStream stream) throws IOException { + byte[] content = String.format(Constant.FIELD_FORMAT, field.getName(), field.getType().getSimpleName(), + anno.value(), anno.required() ? "required" : "optional") + .getBytes(StandardCharsets.UTF_8); + stream.write(content); + stream.flush(); + } + + /** + * Writes Field header in restdoc file. + * + * @param stream file where to write + * @throws IOException If something goes wrong wile writing to file + */ + private void writeFieldHeader(FileOutputStream stream) throws IOException { + byte[] content = Constant.FIELD_HEADER_FORMAT.getBytes(StandardCharsets.UTF_8); + stream.write(content); + stream.flush(); + } + + /** + * Writes Class header in restdoc file. + * + * @param c class targeted + * @param stream file where to write + * @throws IOException If something goes wrong wile writing to file + */ + private void writeClassHeader(Class c, FileOutputStream stream) throws IOException { + byte[] content; + InspectToDocument anno = (InspectToDocument) c.getAnnotation(InspectToDocument.class); + // Write class header + if ("".equals(anno.description())) { + + content = String.format(Constant.CLASS_HEADER_FORMAT, c.getSimpleName(), c.getSimpleName()) + .getBytes(StandardCharsets.UTF_8); + } else { + content = String.format(Constant.CLASS_HEADER_WITH_DESCRIPTION_FORMAT, c.getSimpleName(), c.getSimpleName(), + anno.description()).getBytes(StandardCharsets.UTF_8); + } + stream.write(content); + stream.flush(); + } +} diff --git a/restdoc-helper-plugin/src/main/java/com/pconil/restdoc/Annotation2JavaParser.java b/restdoc-helper-plugin/src/main/java/com/pconil/restdoc/Annotation2JavaParser.java new file mode 100644 index 0000000..f985739 --- /dev/null +++ b/restdoc-helper-plugin/src/main/java/com/pconil/restdoc/Annotation2JavaParser.java @@ -0,0 +1,243 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc; + +import com.pconil.restdoc.annotation.AsciidocAnnotation; +import io.swagger.annotations.ApiModelProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; + +import static com.pconil.restdoc.Constant.IN_LIST; +import static com.pconil.restdoc.Constant.OUT_OF_LIST; + + +/** + * This parser produces target/ClassName.restdoc files for all classes matching package-name or its subpackages. + * + * @author patrice_conil + */ +public class Annotation2JavaParser extends AbstractParser { + + /** + * Logger. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(Annotation2JavaParser.class); + + /** + * Used to construct field information. + */ + private StringBuilder sbField = null; + + /** + * Used to construct field information for list retrieval. + */ + private StringBuilder sbListField = null; + + /** + * Classloader. + */ + private ParserClassLoader classLoader = null; + + /** + * File where we write java code. + */ + private FileOutputStream classFile = null; + + /** + * Annotation2JavaParser Constructor. + * + * @param packageName name of package that we want to parse + * @param adocDirName project for which we want to generate restdoc and java files + * @param javaDirName project for which we want to generate restdoc and java files + * @param source directory where .class will be found + * @throws ParserException if packageName is malformed or target creation isn't possible + */ + public Annotation2JavaParser(String packageName, String adocDirName, String javaDirName, String source) throws ParserException { + super(packageName, adocDirName, javaDirName, source); + } + + @Override + protected boolean parseField(Class c, Field field, boolean hasDocumentation) { + boolean isDocumented = false; + boolean firstField = false; + if (field.isAnnotationPresent(ApiModelProperty.class) || field.isAnnotationPresent(AsciidocAnnotation.class)) { + try { + //Is it the first documented field of the class? + if (!hasDocumentation) { + firstField = true; + isDocumented = true; + writeClassStart(c); + sbField.append(String.format(Constant.FIELD_START, c.getSimpleName())); + sbListField.append(String.format(Constant.FIELD_LIST_START, c.getSimpleName())); + } + // Iterates all the annotations available in the method + for (Annotation anno : field.getDeclaredAnnotations()) { + LOGGER.debug("\tAnnotation {} in class {} for field {} \n", + anno.annotationType().getSimpleName(), + c.getSimpleName(), field.getName()); + if (anno instanceof ApiModelProperty) { + sbField.append(writeFieldInfo(field, (ApiModelProperty) anno, firstField, OUT_OF_LIST)); + sbListField.append(writeFieldInfo(field, (ApiModelProperty) anno, firstField, IN_LIST)); + firstField = false; + } else if (anno instanceof AsciidocAnnotation) { + sbField.append(writeFieldInfo(field, (AsciidocAnnotation) anno, firstField, OUT_OF_LIST)); + sbListField.append(writeFieldInfo(field, (AsciidocAnnotation) anno, firstField, IN_LIST)); + firstField = false; + } + } + } catch (Throwable ex) { + ex.printStackTrace(); + } + } + return hasDocumentation || isDocumented; + } + + @Override + protected void writeFieldStart(Class c) { + + } + + @Override + protected void writeClassStart(Class c) throws IOException { + byte[] content = String.format(Constant.CLASS_HEADER, packageName, c.getSimpleName()).getBytes(StandardCharsets.UTF_8); + classFile = createClassFile(c.getSimpleName(), targetJavaDirName, "FieldDescriptor.java"); + classFile.write(content); + classFile.flush(); + } + + @Override + protected void initializeFields() { + sbField = new StringBuilder(""); + sbListField = new StringBuilder(""); + } + + @Override + protected FileOutputStream writeClass() { + return null; + } + + @Override + protected void completeClass() throws IOException { + writeClassEnd(classFile); + } + + @Override + protected void completeFields() throws IOException { + sbField.append(Constant.FIELD_END); + sbListField.append(Constant.FIELD_END); + writeFieldDescriptors(classFile, sbField.toString(), sbListField.toString()); + } + + + /** + * Write class footer to stream. + * + * @param stream file where to write + * @throws IOException If something goes wrong wile writing to file + */ + private void writeClassEnd(FileOutputStream stream) throws IOException { + byte[] content = Constant.CLASS_END.getBytes(StandardCharsets.UTF_8); + stream.write(content); + } + + /** + * Write field footer to stream. + * + * @param stream file where to write + * @param fd the string that describe fields + * @param fdList the string that describe fields for list retrieval + * @throws IOException If something goes wrong wile writing to file + */ + private void writeFieldDescriptors(FileOutputStream stream, String fd, String fdList) throws IOException { + byte[] content = (fd + "\n\n" + fdList + "\n\n").getBytes(StandardCharsets.UTF_8); + stream.write(content); + } + + /** + * Construct field info. + * + * @param field Field + * @param anno An ApiModelProperty annotation + * @param first is it the first documented field of the class ? + * @param list are we writing for list retrieval ? + * @throws IOException If something goes wrong wile writing to file + * @return the string completed with field info + */ + private String writeFieldInfo(Field field, ApiModelProperty anno, boolean first, boolean list) throws IOException { + String fieldName; + String content = ""; + + // We prefix the name with "[]." in case of list to comply with restdoc behavior. + if (list) { + fieldName = "[]." + field.getName(); + } else { + fieldName = field.getName(); + } + + // If it is not the first field we need to write separator. + if (!first) { + content += Constant.FIELD_SEPARATOR; + } + // This is a required field + if (((ApiModelProperty) anno).required()) { + content += String.format(Constant.REQUIRED_FIELD_FORMAT, fieldName, ((ApiModelProperty) anno).value()); + } else { + content += String.format(Constant.OPTIONAL_FIELD_FORMAT, fieldName, ((ApiModelProperty) anno).value()); + } + return content; + } + + /** + * Construct field info. + * + * @param field Field + * @param anno An AsciidocAnnotation annotation + * @param first is it the first documented field of the class ? + * @param list are we writing for list retrieval ? + * @throws IOException If something goes wrong wile writing to file + * @return the string completed with field info + */ + private String writeFieldInfo(Field field, AsciidocAnnotation anno, boolean first, boolean list) throws IOException { + String fieldName; + String content = ""; + + // We prefix the name with "[]." in case of list to comply with restdoc behavior. + if (list) { + fieldName = "[]." + field.getName(); + } else { + fieldName = field.getName(); + } + + // If it is not the first field we need to write separator. + if (!first) { + content += Constant.FIELD_SEPARATOR; + } + // This is a required field + if ( anno.required() ) { + content += String.format(Constant.REQUIRED_FIELD_FORMAT, fieldName, anno.description()); + } else { + content += String.format(Constant.OPTIONAL_FIELD_FORMAT, fieldName, anno.description()); + } + return content; + } + +} diff --git a/restdoc-helper-plugin/src/main/java/com/pconil/restdoc/Constant.java b/restdoc-helper-plugin/src/main/java/com/pconil/restdoc/Constant.java new file mode 100644 index 0000000..a3be625 --- /dev/null +++ b/restdoc-helper-plugin/src/main/java/com/pconil/restdoc/Constant.java @@ -0,0 +1,105 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc; + +/** + * All constants. + * + * @author patrice_conil + */ +public class Constant { + + // Constant to generate AsciiDoc generation. + + /** + * Format of line that must be generated as class-CLASS_HEADER_FORMAT header. + */ + static final String CLASS_HEADER_FORMAT = "[%s]\n= %s\n|===\n"; + + /** + * Format of line that must be generated as class-CLASS_HEADER_FORMAT header. + */ + static final String CLASS_HEADER_WITH_DESCRIPTION_FORMAT = "[%s]\n= %s\n%s\n|===\n"; + + /** + * Format of line that must be generated as field CLASS_HEADER_FORMAT. + */ + static final String FIELD_FORMAT = "\n| %s\n| %s\n| %s\n| %s\n"; + + /** + * Format of line that must be generated as field-header CLASS_HEADER_FORMAT. + */ + static final String FIELD_HEADER_FORMAT = "| Field| Type| Description| Constraints\n"; + + /** + * Line to add to terminate table of field properly. + */ + static final String FIELD_FOOTER = "|===\n\n"; + + /** + * Constants that describe if a field is in a list or not. + */ + static final boolean IN_LIST = true; + static final boolean OUT_OF_LIST = false; + + + + // Java classes generation. + /** + * Class start. + */ + static final String CLASS_HEADER = + "package %s;\n\n" + + "import org.springframework.restdocs.payload.FieldDescriptor; \n" + + "import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;\n\n" + + "public class %sFieldDescriptor {\n"; + + /** + * Class end. + */ + static final String CLASS_END = "}\n"; + + /** + * Field start. + */ + static final String FIELD_LIST_START = " public static FieldDescriptor[] fd%sList = new FieldDescriptor[] {\n"; + + /** + * Field list start. + */ + static final String FIELD_START = " public static FieldDescriptor[] fd%s = new FieldDescriptor[] {\n"; + + /** + * Format for a required field. + */ + static final String REQUIRED_FIELD_FORMAT = " fieldWithPath(\"%s\").description(\"%s\")"; + + /** + * Format for an optional field. + */ + static final String OPTIONAL_FIELD_FORMAT = " fieldWithPath(\"%s\").description(\"%s\").optional()"; + + /** + * Field end. + */ + static final String FIELD_END = " };\n"; + + /** + * Field separator. + */ + static final String FIELD_SEPARATOR = ",\n"; + +} diff --git a/restdoc-helper-plugin/src/main/java/com/pconil/restdoc/ParserClassLoader.java b/restdoc-helper-plugin/src/main/java/com/pconil/restdoc/ParserClassLoader.java new file mode 100644 index 0000000..14deaa4 --- /dev/null +++ b/restdoc-helper-plugin/src/main/java/com/pconil/restdoc/ParserClassLoader.java @@ -0,0 +1,170 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +/** + * This class is used to include directory in classpath and to load java .class. + * + * @author patrice_conil + */ +class ParserClassLoader { + + /** + * Logger. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(ParserClassLoader.class); + + /** + * The previous Classloader that must be used to restore context. + */ + private ClassLoader previous = null; + + /** + * The current one used to load and parse classes. + */ + private ClassLoader current = null; + + /** + * Constructor that adds the URL sourceDir to the classpath. + * @param sourceDir where to find classes + * @throws MalformedURLException if something goes wrong with URL construction. + */ + ParserClassLoader(File sourceDir) throws MalformedURLException { + previous = Thread.currentThread().getContextClassLoader(); + current = changeLoader(new URL[]{new URL("file:" + sourceDir.getAbsolutePath() + "/")}); + } + + /** + * Adds urls to classLoader. + * + * @param urls urls to add to classpath + * @return updated ClassLoader + * @throws MalformedURLException if one of urls is bad formatted + */ + private ClassLoader changeLoader(URL[] urls) throws MalformedURLException { + + // Create class loader using given codebase + // Use prevCl as parent to maintain current visibility + ClassLoader newCl = URLClassLoader.newInstance(urls, previous); + + // Sets class loader + Thread.currentThread().setContextClassLoader(newCl); + + return newCl; + } + + + /** + * Restores previous classLoader. + * @throws MalformedURLException if something goes wrong while restoring classloader + */ + void restoreLoader() throws MalformedURLException { + + ClassLoader tmp = current; + Thread.currentThread().setContextClassLoader(previous); + current = previous; + previous = tmp; + + } + + /** + * Scans all classes accessible from the context class loader which belong to the given package and subpackages. + * + * @param packageName The base package + * @param dir the directory where to find classes + * @return The classes + * @throws ClassNotFoundException if no class are found + * @throws IOException if an IO error occur + */ + List getClasses(String dir, String packageName) + throws ClassNotFoundException, IOException { + + ArrayList classes = new ArrayList<>(); + try { + String path = packageName.replace('.', '/'); + Enumeration resources = current.getResources(path); + List dirs = new ArrayList<>(); + while (resources.hasMoreElements()) { + URL resource = resources.nextElement(); + dirs.add(new File(resource.getFile())); + } + + for (File dire : dirs) { + classes.addAll(findClasses(current, dire, packageName)); + } + + for (Class clazz : classes) { + LOGGER.debug("Found class {}", clazz.getSimpleName()); + } + } catch (IOException cnfe) { + cnfe.printStackTrace(); + } + return classes; + } + + + /** + * Recursive method used to find all classes in a given directory and subdirs. + * + * @param cl The ClassLoader to load class into + * @param directory The base directory + * @param packageName The package name for classes found inside the base directory + * @return The classes + * @throws ClassNotFoundException if no class are found + */ + private List findClasses(ClassLoader cl, File directory, + String packageName) throws ClassNotFoundException { + List classes = new ArrayList<>(); + String classSuffix = ".class"; + if (!directory.exists()) { + return classes; + } + File[] files = directory.listFiles(); + assert files != null; + for (File file : files) { + if (file.isDirectory()) { + assert !file.getName().contains("."); + if ("".equals(packageName)) { + classes.addAll(findClasses(cl, file, file.getName())); + } else { + classes.addAll(findClasses(cl, file, packageName + "." + file.getName())); + } + } else if (file.getName().endsWith(classSuffix)) { + String className = packageName + '.' + file.getName().substring(0, file.getName().length() + - classSuffix.length()); + try { + classes.add(cl.loadClass(className)); + } catch (ClassNotFoundException | NoClassDefFoundError e) { + LOGGER.error("Unable to load classes"); + } + } + } + return classes; + } + +} diff --git a/restdoc-helper-plugin/src/main/java/com/pconil/restdoc/ParserException.java b/restdoc-helper-plugin/src/main/java/com/pconil/restdoc/ParserException.java new file mode 100644 index 0000000..3cc53b2 --- /dev/null +++ b/restdoc-helper-plugin/src/main/java/com/pconil/restdoc/ParserException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc; + +/** + * Exception that indicates an error while creating the parser. + * + * @author patrice_conil + */ +public class ParserException extends RuntimeException { + + public String getMessage() { + return message; + } + + /** + * Explanation of error. + */ + private String message = null; + + /** + * Constructor. + * + * @param message the messages that explains the error. + */ + public ParserException(String message) { + this.message = message; + } +} diff --git a/restdoc-helper-plugin/src/main/java/com/pconil/restdoc/RestdocMojo.java b/restdoc-helper-plugin/src/main/java/com/pconil/restdoc/RestdocMojo.java new file mode 100644 index 0000000..6d81ac0 --- /dev/null +++ b/restdoc-helper-plugin/src/main/java/com/pconil/restdoc/RestdocMojo.java @@ -0,0 +1,75 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.pconil.restdoc; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +/** + * Generate restdoc files and java fieldDescriptor classes. + * + * @author patrice_conil + */ +@Mojo(name = "restdoc-helper") +public class RestdocMojo extends AbstractMojo { + + /** + * Where we want to start searching sources. + */ + @Parameter + private String basePackageName; + + /** + * Where to find sources (including subdirs). + */ + @Parameter(defaultValue = "target/classes/") + private String sourceDir; + + /** + * Where to write adoc snippets. + */ + @Parameter(defaultValue = "target/generated-snippets") + private String adocDir; + + /** + * Where to write java sources. + */ + @Parameter(defaultValue = "target/generated-test-sources") + private String javaDir; + + + /** + * Launches the parsers to generate restdoc and java files. + * @throws MojoExecutionException if something goes wrong + */ + public void execute() throws MojoExecutionException { + + getLog().info("restdoc-helper starts with basePackageName=" + basePackageName + ", sourceDir=" + sourceDir + + ", adocDir=" + adocDir + ", javaDir=" + javaDir + "\n"); + Annotation2JavaParser parser = new Annotation2JavaParser(basePackageName, adocDir, javaDir, sourceDir); + parser.parse(); + + Annotation2AsciiDocParser annotation2AsciiDocParser = new Annotation2AsciiDocParser(basePackageName, adocDir, javaDir, sourceDir); + annotation2AsciiDocParser.parse(); + getLog().info("restdoc-helper ends."); + } + +} + + diff --git a/restdoc-helper-plugin/src/site/LICENSE-2.0.txt b/restdoc-helper-plugin/src/site/LICENSE-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/restdoc-helper-plugin/src/site/LICENSE-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/restdoc-helper-plugin/src/site/markdown/README.md b/restdoc-helper-plugin/src/site/markdown/README.md new file mode 100644 index 0000000..1db72c8 --- /dev/null +++ b/restdoc-helper-plugin/src/site/markdown/README.md @@ -0,0 +1,140 @@ + +# Restdoc Helper + +## Overview + +The main goal of this project is to simplify the documentation process of your RESTFul services by combining hand-written using [Asciidoctor][3] docs, [Sprinfox static docs][1] generated docs or [Spring REST Docs][10] generated docs. +Restdoc helper is a Maven plugin that generates [Asciidoctor][3] files and [FieldDescriptors][7] from [Swagger 2][6] annotations + +The purpose of [Spring REST Docs][10] is to document your API with auto-generated snippets produced with [Spring MVC Test][4]. +To achieve that, it's necessary to document all fields of data classes involved in your call-flows. If as me you use [Swagger 2][6] to describe your API contracts, the job is almost done. +What'is missing is the translation of your swagger annotations to a [FieldDescriptors][7]. This is the purpose of this contribution. +It's also possible to generate asciidoctor files only that describe your model from custom annotation. + +## Doc generation + +### Add dependencies to your pom + +``` + + com.pconil.restdoc + restdoc-annotation + 1.0-SNAPSHOT + ` + + ... + + + com.pconil.restdoc + restdoc-helper-plugin + 1.0-SNAPSHOT + + ${basedir}/target/classes/ + com.pconil.restdoc.model + ${basedir}/target/generated-test-sources + ${basedir}/target/generated-snippets + + + + generate + generate-test-sources + + restdoc-helper + + + + + + org.springframework + spring-web + 4.3.3.RELEASE + + + + ... + +``` + +### Howto Generate adoc files from swagger annotations + +As simple as adding @InspectToDocument to your model class + +``` +@InspectToDocument(description = "This is the Class1 description") +public class ClassSwaggerDTO { + @ApiModelProperty(required = true, value="This is the field field1") + private String field1; + + @ApiModelProperty(value="This is the optional field field2") + private String field2 = null; + + @ApiModelProperty(required = true, value="This is the field field3") + private String field3 = null; + + public ClassSwaggerDTO(String field1, String field2) { + this.field1 = field1; + this.field2 = field2; + } +} +``` + + +### And If you don't use swagger or want to document classes that doesn't contain @ApiModelProperty + +Add a @AsciidocAnnotation to your fields + +``` +@InspectToDocument(description = "This is the Class1 description") +public class Class1DTO { + @AsciidocAnnotation(description="This field describe name of Class1DTO", constraints = "Length must be between 4 and 6") + String field1; + + @AsciidocAnnotation(description="field 2") + String field2 = null; + + public Class1DTO(String field1, String field2) { + this.field1 = field1; + this.field2 = field2; + } +} +``` + +## Go further + +See the springboot-sample to see how it works. + +## Prerequisite + +Java 8, maven 3 + +## Building from source + +You will need Java 8 or later to build restdoc-helper. It is built using [maven][2]: + +``` +mvn clean install +``` + +## Contributing + +There are several ways to contribute to restdoc-helper: + + - Open a [pull request][12]. + - Ask and answer questions on Stack Overflow using the [`restdoc-helper`][15] tag. + +## Licence + +Restdoc helper is open source software released under the [Apache 2.0 license][14]. + + +[1]: https://springfox.github.io/springfox/docs/snapshot/#configuring-springfox-staticdocs +[2]: https://maven.apache.org/download.cgi +[3]: http://asciidoctor.org +[4]: http://docs.spring.io/spring-framework/docs/4.1.x/spring-framework-reference/htmlsingle/#spring-mvc-test-framework +[5]: https://developer.github.com/v3/ +[6]: http://swagger.io/specification/ +[7]: http://docs.spring.io/spring-restdocs/docs/current/reference/html5/#documenting-your-api-request-response-payloads-reusing-field-descriptors +[10]: http://docs.spring.io/spring-restdocs/docs/current/reference/html5/ +[12]: https://help.github.com/articles/using-pull-requests/ +[14]: http://www.apache.org/licenses/LICENSE-2.0.html +[15]: http://stackoverflow.com/tags/restdoc-helper diff --git a/restdoc-helper-plugin/src/site/site.xml b/restdoc-helper-plugin/src/site/site.xml new file mode 100644 index 0000000..f25edbc --- /dev/null +++ b/restdoc-helper-plugin/src/site/site.xml @@ -0,0 +1,27 @@ + + + + restdoc-helper-plugin + http://maven.apache.org/images/apache-maven-project.png + https://github.com/patrice-conil/restdoc-helper + + + http://maven.apache.org/images/maven-small.gif + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/Annotation2JavaParserTest.java b/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/Annotation2JavaParserTest.java new file mode 100644 index 0000000..57849b3 --- /dev/null +++ b/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/Annotation2JavaParserTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc; + +import org.junit.Test; + +import java.io.File; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test java generation from swagger ApiModelProperty. + * + * @author patrice_conil + */ +public class Annotation2JavaParserTest { + public static final String PACKAGE = "com.pconil.restdoc"; + public static final String ADOC_DIR = "target/generated-snippets"; + public static final String JAVA_DIR = "target/generated-test-sources"; + /** + * Generates java sources for classes in the com.pconil.restdoc.model package and checks the result. + * @throws Exception if the result isn't the expected one + */ + @Test + public void generateJavaFromAnnotation() throws Exception { + Annotation2JavaParser parser = new Annotation2JavaParser(PACKAGE, ADOC_DIR, JAVA_DIR, "target/classes"); + parser.parse(); + + //Verify generation from ApiModelProperty annotation + String filename = "./target/generated-test-sources/ClassSwaggerDTOFieldDescriptor.java"; + File java = new File(filename); + File expected = new File("./src/test/resources/ClassSwaggerDTOFieldDescriptor.java.expected"); + assertThat(java).exists(); + assertThat(java).hasSameContentAs(expected); + + //Verify generation from AsciidocAnnotation annotation + filename = "./target/generated-test-sources/ClassWithoutApiModelPropertyFieldDescriptor.java"; + java = new File(filename); + expected = new File("./src/test/resources/ClassWithoutApiModelPropertyFieldDescriptor.java.expected"); + assertThat(java).exists(); + assertThat(java).hasSameContentAs(expected); + } +} \ No newline at end of file diff --git a/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/ParserTest.java b/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/ParserTest.java new file mode 100644 index 0000000..74b4404 --- /dev/null +++ b/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/ParserTest.java @@ -0,0 +1,136 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc; + +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.EnumSet; +import java.util.Set; + +import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests Annotation2AsciiDocParser. + * + * @author patrice_conil + */ +public class ParserTest { + + public static final String PACKAGE = "com.pconil.restdoc"; + public static final String ADOC_DIR = "target/generated-snippets"; + public static final String JAVA_DIR = "target/generated-test-sources"; + + /** + * Parses java class that contains only description on its fields. + * + * @throws Exception if something goes wrong + */ + @Test + public void parseClassWithDescription() throws Exception { + Annotation2AsciiDocParser annotation2AsciiDocParser = new Annotation2AsciiDocParser(PACKAGE, ADOC_DIR, JAVA_DIR, + "target/classes"); + annotation2AsciiDocParser.parse(); + String filename = "target/generated-snippets/Class1DTO.adoc"; + File adhoc = new File(filename); + assertThat(adhoc).exists(); + File expected = new File("./src/test/resources/Class1DTOExpected.adoc"); + assertThat(adhoc).hasSameContentAs(expected); + } + + /** + * Parses java class that contains description and constraint on its fields. + * + * @throws Exception if something goes wrong + */ + @Test + public void parseClassWithDescriptionAndConstraints() throws Exception { + Annotation2AsciiDocParser annotation2AsciiDocParser = new Annotation2AsciiDocParser(PACKAGE, ADOC_DIR, JAVA_DIR, + "target/classes"); + annotation2AsciiDocParser.parse(); + String filename = "target/generated-snippets/SimilarContent.adoc"; + File adhoc = new File(filename); + assertThat(adhoc).exists(); + File expected = new File("./src/test/resources/SimilarContent.adoc"); + assertThat(adhoc).hasSameContentAs(expected); + } + + /** + * Parses java class that contains no description. + * + * @throws Exception if something goes wrong + */ + @Test + public void parseClassWithoutDescription() throws Exception { + Annotation2AsciiDocParser annotation2AsciiDocParser = new Annotation2AsciiDocParser(PACKAGE, ADOC_DIR, JAVA_DIR, + "target/classes"); + annotation2AsciiDocParser.parse(); + String filename = "target/generated-snippets/Class2DTO.adoc"; + File adhoc = new File(filename); + assertThat(adhoc).exists(); + File expected = new File("./src/test/resources/Class2DTOExpected.adoc"); + assertThat(adhoc).hasSameContentAs(expected); + } + + /** + * Parses java class that contains swagger description. + * + * @throws Exception if something goes wrong + */ + @Test + public void parseClassWithSwaggerAnnotationOnly() throws Exception { + Annotation2AsciiDocParser annotation2AsciiDocParser = new Annotation2AsciiDocParser(PACKAGE, ADOC_DIR, JAVA_DIR, + "target/classes"); + annotation2AsciiDocParser.parse(); + String filename = "target/generated-snippets/ClassSwaggerDTO.adoc"; + File adhoc = new File(filename); + assertThat(adhoc).exists(); + File expected = new File("./src/test/resources/ClassSwaggerDTOExpected.adoc"); + assertThat(adhoc).hasSameContentAs(expected); + } + + @Test(expected = ParserException.class) + public void badPackageName() throws Exception { + new Annotation2AsciiDocParser("com.pconil/restdoc", ADOC_DIR, JAVA_DIR, "target/classes"); + } + + + @Test(expected = ParserException.class) + public void unwritableTargetName() throws Exception { + createTmpDirWithoutWriteRight("target/unwritableDir"); + new Annotation2AsciiDocParser(PACKAGE, "target/unwritableDir/test", "target/unwritableDir/test", "target/classes"); + } + + /** + * Creates tmp dir that doesn't permit write access. + * + * @param name + */ + private void createTmpDirWithoutWriteRight(String name) throws IOException { + Path path = Paths.get(name); + Set perms = + EnumSet.of(OWNER_READ); + Files.deleteIfExists(path); + Files.createFile(path, PosixFilePermissions.asFileAttribute(perms)); + } +} \ No newline at end of file diff --git a/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/RestdocMojoTest.java b/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/RestdocMojoTest.java new file mode 100644 index 0000000..33f9509 --- /dev/null +++ b/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/RestdocMojoTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc; + +import org.apache.maven.plugin.testing.MojoRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.File; + +import static org.codehaus.plexus.PlexusTestCase.getTestFile; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Mojo execution test. + * + * @author patrice_conil + */ +@RunWith(JUnit4.class) +public class RestdocMojoTest { + + @Rule + public MojoRule rule = new MojoRule() { + @Override + protected void before() throws Throwable { + } + + @Override + protected void after() { + } + }; + + @Test + public void execute() throws Exception { + File pom = getTestFile("src/test/resources/pom.xml"); + assertNotNull(pom); + assertTrue(pom.exists()); + RestdocMojo myMojo = (RestdocMojo) rule.lookupMojo("restdoc-helper", pom); + assertNotNull(myMojo); + myMojo.execute(); + } + +} \ No newline at end of file diff --git a/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/Utils.java b/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/Utils.java new file mode 100644 index 0000000..2374bc6 --- /dev/null +++ b/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/Utils.java @@ -0,0 +1,56 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; + +/** + * Utilities. + * + * @author patrice_conil + */ +public class Utils { + + /** + * Logger. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class); + + /** + * Load a resource as an UTF-8 string. + * + * @param name name of the resource + * @return the content of the resource + */ + public static String loadResource(String name) { + try { + URL url = Utils.class.getClassLoader().getResource(name); + if (url == null) { + return ""; + } + return new String(Files.readAllBytes(Paths.get(url.toURI())), "UTF-8"); + } catch (Exception e) { + return ""; + } + } + + +} diff --git a/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/model/Class1DTO.java b/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/model/Class1DTO.java new file mode 100644 index 0000000..b83da82 --- /dev/null +++ b/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/model/Class1DTO.java @@ -0,0 +1,38 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc.model; + +import com.pconil.restdoc.annotation.AsciidocAnnotation; +import com.pconil.restdoc.annotation.InspectToDocument; + +/** + * Annotations to generate restdoc file from pojo classes. + * + * @author patrice_conil + */ +@InspectToDocument(description = "This is the Class1 description") +public class Class1DTO { + @AsciidocAnnotation(description="This field describe name of Class1DTO", constraints = "Length must be between 4 and 6") + String field1; + + @AsciidocAnnotation(description="field 2") + String field2 = null; + + public Class1DTO(String field1, String field2) { + this.field1 = field1; + this.field2 = field2; + } +} diff --git a/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/model/Class2DTO.java b/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/model/Class2DTO.java new file mode 100644 index 0000000..a2b22ad --- /dev/null +++ b/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/model/Class2DTO.java @@ -0,0 +1,38 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc.model; + +import com.pconil.restdoc.annotation.AsciidocAnnotation; +import com.pconil.restdoc.annotation.InspectToDocument; + +/** + * Test class. + * + * @author patrice_conil + */ +@InspectToDocument +public class Class2DTO { + @AsciidocAnnotation(description="field 1") + private String field1; + + @AsciidocAnnotation(description="field 2") + private String field2 = null; + + public Class2DTO(String field1, String field2) { + this.field1 = field1; + this.field2 = field2; + } +} diff --git a/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/model/ClassSwaggerDTO.java b/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/model/ClassSwaggerDTO.java new file mode 100644 index 0000000..6488444 --- /dev/null +++ b/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/model/ClassSwaggerDTO.java @@ -0,0 +1,41 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc.model; + +import com.pconil.restdoc.annotation.InspectToDocument; +import io.swagger.annotations.ApiModelProperty; + +/** + * Annotations to generate restdoc file from pojo classes. + * + * @author patrice_conil + */ +@InspectToDocument(description = "This is the Class1 description") +public class ClassSwaggerDTO { + @ApiModelProperty(required = true, value="This is the field field1") + private String field1; + + @ApiModelProperty(value="This is the optional field field2") + private String field2 = null; + + @ApiModelProperty(required = true, value="This is the field field3") + private String field3 = null; + + public ClassSwaggerDTO(String field1, String field2) { + this.field1 = field1; + this.field2 = field2; + } +} diff --git a/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/model/ClassWithoutApiModelProperty.java b/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/model/ClassWithoutApiModelProperty.java new file mode 100755 index 0000000..91a61d6 --- /dev/null +++ b/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/model/ClassWithoutApiModelProperty.java @@ -0,0 +1,412 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.pconil.restdoc.annotation.AsciidocAnnotation; +import com.pconil.restdoc.annotation.InspectToDocument; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Object that 'summarize' a Program. + * + * @author patrice_conil + */ +@InspectToDocument(description = "Electronic Program Guide Item") +public class ClassWithoutApiModelProperty { + + /** + * Program identifier. + */ + @AsciidocAnnotation(description = "Program identifier", required = true) + private String id = null; + + /** + * Program type. + */ + @AsciidocAnnotation(description = "Program type", required = true) + private String programType = null; + + /** + * Program title. + */ + @AsciidocAnnotation(description = "Program title", required = true) + private String title = null; + + /** + * Id of channel broadcasting this program. + */ + @AsciidocAnnotation(description = "Id of channel broadcasting this program", required = true) + private String channelId = null; + + /** + * Diffusion date in seconds. + */ + @AsciidocAnnotation(description = "Diffusion date in seconds", required = true) + private Long diffusionDate = null; + + /** + * Program duration in second. + */ + @AsciidocAnnotation(description = "Program duration in second", required = true) + private Long duration = null; + + /** + * Program CSA level. + */ + @AsciidocAnnotation(description = "Program CSA level", required = true) + private Long csa = null; + + /** + * Kind of program (ex: variety). + */ + @AsciidocAnnotation(description = "Kind of program (ex: comedy)", required = true) + private String kind = null; + + /** + * More detailed kind (e.g Comedy-drama). + */ + @AsciidocAnnotation(description = "More detailed kind (e.g Comedy-drama)", required = true) + private String kindDetailed = null; + + /** + * Program sysnopsis. + */ + @AsciidocAnnotation(description = "Program sysnopsis", required = true) + private String synopsis = null; + + /** + * Program language. + */ + @AsciidocAnnotation(description = "Program language", required = true) + private String languageVersion = null; + + /** + * Indicates if the program has some hearing-impaired subtitle. + */ + @AsciidocAnnotation(description = "Indicates if the program has some hearing-impaired subtitle", required = true) + private Boolean hearingImpaired = null; + + /** + * Indicates if the program has an audio description track. + */ + @AsciidocAnnotation(description = "Indicates if the program has an audio description track", required = true) + private Boolean audioDescription = null; + + /** + * If the type of the program is EPISODE, gives the season number. + */ + @AsciidocAnnotation(description = "If the type of the program is EPISODE, gives the season number", required = true) + private Long season = null; + + /** + * If the type of the program is EPISODE, gives the position of the episode in the season. + */ + @AsciidocAnnotation(description = "If the type of the program is EPISODE, gives the position of the episode in " + + "the season", required = true) + private String episodeNumber = null; + + /** + * Program definition. + */ + @AsciidocAnnotation(description = "Program definition", required = true, constraints = "SD or HD") + private String definition = null; + + /** + * List of links which points to entity in relation with the current program. + */ + @AsciidocAnnotation(description = "List of links which points to entities in relation with" + + " the current program", required = true) + private List links = new ArrayList<>(); + + /** + * Part of day when program is broadcast(ed). + */ + @AsciidocAnnotation(description = "Part of day when program start", required = true, constraints = "PT1, PT2, PT3, OTHER") + private String dayPart = null; + + /** + * Identifier of the catchup program related to this live program. + */ + @AsciidocAnnotation(description = "Identifier of the catchup program related to this live program", required = true) + private String catchupId = null; + + + // + /** + * Full param constructor. + */ + @SuppressWarnings("all") + @JsonIgnore + public ClassWithoutApiModelProperty(String id, String programType, String title, String channelId, + Long diffusionDate, Long duration, Long csa, String kind, String kindDetailed, + String synopsis, + String languageVersion, Boolean hearingImpaired, Boolean audioDescription, + Long season, + String episodeNumber, String definition, List links, + String dayPart, + String catchupId) { + this.id = id; + this.programType = programType; + this.title = title; + this.channelId = channelId; + this.diffusionDate = diffusionDate; + this.duration = duration; + this.csa = csa; + this.kind = kind; + this.kindDetailed = kindDetailed; + this.synopsis = synopsis; + this.languageVersion = languageVersion; + this.hearingImpaired = hearingImpaired; + this.audioDescription = audioDescription; + this.season = season; + this.episodeNumber = episodeNumber; + this.definition = definition; + this.links = links; + this.dayPart = dayPart; + this.catchupId = catchupId; + } + + /** + * Default constructor. + */ + public ClassWithoutApiModelProperty() { + + } + // + + // + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getProgramType() { + return programType; + } + + public void setProgramType(String programType) { + this.programType = programType; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getChannelId() { + return channelId; + } + + public void setChannelId(String channelId) { + this.channelId = channelId; + } + + public Long getDiffusionDate() { + return diffusionDate; + } + + public void setDiffusionDate(Long diffusionDate) { + this.diffusionDate = diffusionDate; + } + + public Long getDuration() { + return duration; + } + + public void setDuration(Long duration) { + this.duration = duration; + } + + public Long getCsa() { + return csa; + } + + public void setCsa(Long csa) { + this.csa = csa; + } + + public String getKind() { + return kind; + } + + public void setKind(String kind) { + this.kind = kind; + } + + public String getKindDetailed() { + return kindDetailed; + } + + public void setKindDetailed(String kindDetailed) { + this.kindDetailed = kindDetailed; + } + + public String getSynopsis() { + return synopsis; + } + + public void setSynopsis(String synopsis) { + this.synopsis = synopsis; + } + + public String getLanguageVersion() { + return languageVersion; + } + + public void setLanguageVersion(String languageVersion) { + this.languageVersion = languageVersion; + } + + public Boolean getHearingImpaired() { + return hearingImpaired; + } + + public void setHearingImpaired(Boolean hearingImpaired) { + this.hearingImpaired = hearingImpaired; + } + + public Boolean getAudioDescription() { + return audioDescription; + } + + public void setAudioDescription(Boolean audioDescription) { + this.audioDescription = audioDescription; + } + + public Long getSeason() { + return season; + } + + public void setSeason(Long season) { + this.season = season; + } + + public String getEpisodeNumber() { + return episodeNumber; + } + + public void setEpisodeNumber(String episodeNumber) { + this.episodeNumber = episodeNumber; + } + + public String getDefinition() { + return definition; + } + + public void setDefinition(String definition) { + this.definition = definition; + } + + public List getLinks() { + return links; + } + + public void setLinks(List links) { + this.links = links; + } + + public String getDayPart() { + return dayPart; + } + + public void setDayPart(String dayPart) { + this.dayPart = dayPart; + } + + public String getCatchupId() { + return catchupId; + } + + public void setCatchupId(String catchupId) { + this.catchupId = catchupId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ClassWithoutApiModelProperty programLight = (ClassWithoutApiModelProperty) o; + return Objects.equals(id, programLight.id) + && Objects.equals(programType, programLight.programType) + && Objects.equals(title, programLight.title) + && Objects.equals(channelId, programLight.channelId) + && Objects.equals(diffusionDate, programLight.diffusionDate) + && Objects.equals(duration, programLight.duration) + && Objects.equals(csa, programLight.csa) + && Objects.equals(kind, programLight.kind) + && Objects.equals(kindDetailed, programLight.kindDetailed) + && Objects.equals(synopsis, programLight.synopsis) + && Objects.equals(languageVersion, programLight.languageVersion) + && Objects.equals(hearingImpaired, programLight.hearingImpaired) + && Objects.equals(audioDescription, programLight.audioDescription) + && Objects.equals(season, programLight.season) + && Objects.equals(episodeNumber, programLight.episodeNumber) + && Objects.equals(definition, programLight.definition) + && Objects.equals(links, programLight.links) + && Objects.equals(dayPart, programLight.dayPart) + && Objects.equals(catchupId, programLight.catchupId); + } + + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public String toString() { + return "class ProgramLight {\n" + + " id: " + id + "\n" + + " programType: " + programType + "\n" + + " title: " + title + "\n" + + " channelId: " + channelId + "\n" + + " diffusionDate: " + diffusionDate + "\n" + + " duration: " + duration + "\n" + + " csa: " + csa + "\n" + + " kind: " + kind + "\n" + + " kindDetailed: " + kindDetailed + "\n" + + " synopsis: " + synopsis + "\n" + + " languageVersion: " + languageVersion + "\n" + + " hearingImpaired: " + hearingImpaired + "\n" + + " audioDescription: " + audioDescription + "\n" + + " season: " + season + "\n" + + " episodeNumber: " + episodeNumber + "\n" + + " definition: " + definition + "\n" + + " links: " + links + "\n" + + " dayPart: " + dayPart + "\n" + + " catchupId: " + catchupId + "\n" + + "}\n"; + } + // +} + + diff --git a/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/model/SimilarContent.java b/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/model/SimilarContent.java new file mode 100755 index 0000000..34e85d0 --- /dev/null +++ b/restdoc-helper-plugin/src/test/java/com/pconil/restdoc/model/SimilarContent.java @@ -0,0 +1,123 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc.model; + +import com.pconil.restdoc.annotation.AsciidocAnnotation; +import com.pconil.restdoc.annotation.InspectToDocument; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * SimilarContent describes a similar content as the one it refers to. + * + * @author patrice_conil + */ +@InspectToDocument(description = "SimilarContent describes a similar content as the one it refers to") +class SimilarContent { + + /** + * Title. + **/ + @AsciidocAnnotation(description = "Title") + private String title = null; + + /** + * Content identifier : the content identifier depends on its contentType. For MOVIE contentType, id is a videoId + * (aka externalAssetId in RTV). For SEASON contentType, id is a seasonId (aka serieId in RTV). + **/ + @AsciidocAnnotation(description = "Content identifier") + private String id = null; + + /** + * Offer identifier. + **/ + @AsciidocAnnotation(description = "Offer identifier") + private String offerId = null; + + /** + * Type of service. + */ + @AsciidocAnnotation(description = "Type of service") + private String serviceType = null; + + /** + * Type of content. + */ + @AsciidocAnnotation(description = "Type of content") + private String contentType = null; + + /** + * Type of similarity. + */ + @AsciidocAnnotation(description = "Type of similarity see <>") + private String similarity = null; + + /** + * CSA Level between 1 and 5. + **/ + @AsciidocAnnotation(description = "CSA Level", constraints = "between 1 and 5") + private Integer csaLevel = null; + + /** + * List of available images. + **/ + @AsciidocAnnotation(description = "Available images") + private List images = new ArrayList<>(); + + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SimilarContent similarContent = (SimilarContent) o; + return Objects.equals(title, similarContent.title) + && Objects.equals(id, similarContent.id) + && Objects.equals(offerId, similarContent.offerId) + && Objects.equals(serviceType, similarContent.serviceType) + && Objects.equals(contentType, similarContent.contentType) + && Objects.equals(similarity, similarContent.similarity) + && Objects.equals(csaLevel, similarContent.csaLevel) + && Objects.equals(images, similarContent.images); + } + + @Override + public int hashCode() { + return Objects.hash(title, id, offerId, serviceType, contentType, similarity, csaLevel, images); + } + + @Override + public String toString() { + + return "class SimilarContent {\n" + + " title: " + title + "\n" + + " id: " + id + "\n" + + " offerId: " + offerId + "\n" + + " serviceType: " + serviceType + "\n" + + " contentType: " + contentType + "\n" + + " similarity: " + similarity + "\n" + + " csaLevel: " + csaLevel + "\n" + + " images: " + images + "\n" + + "}\n"; + } + // +} diff --git a/restdoc-helper-plugin/src/test/resources/ChannelFieldDescriptor.java.expected b/restdoc-helper-plugin/src/test/resources/ChannelFieldDescriptor.java.expected new file mode 100644 index 0000000..f5b455f --- /dev/null +++ b/restdoc-helper-plugin/src/test/resources/ChannelFieldDescriptor.java.expected @@ -0,0 +1,31 @@ +package com.pconil.restdoc.otmllive.com.pconil.restdoc.model; + +import org.springframework.restdocs.payload.FieldDescriptor; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +public class ChannelFieldDescriptor { + public static FieldDescriptor[] fdChannel = new FieldDescriptor[] { + fieldWithPath("id").description("Identifier of the channel (Corresponds to the EPG identifier)"), + fieldWithPath("name").description("Name of the channel"), + fieldWithPath("zappingNumber").description("Code used to zap on the channel (on a STB ZE)"), + fieldWithPath("slogan").description("The channel slogan (used in case of no EPG description)"), + fieldWithPath("catchupId").description("The associated catchup channel identifier"), + fieldWithPath("bouquets").description("List of bouquets linked to the channel (PA_xxx)"), + fieldWithPath("links").description("List of links which points to entity in relation with the current channel (For now, points to the list of programs for the whole day)"), + fieldWithPath("logos").description("List of logos available for the given channel"), + fieldWithPath("terminalModels").description("List of terminal models available for the given channel") }; + + + public static FieldDescriptor[] fdChannelList = new FieldDescriptor[] { + fieldWithPath("[].id").description("Identifier of the channel (Corresponds to the EPG identifier)"), + fieldWithPath("[].name").description("Name of the channel"), + fieldWithPath("[].zappingNumber").description("Code used to zap on the channel (on a STB ZE)"), + fieldWithPath("[].slogan").description("The channel slogan (used in case of no EPG description)"), + fieldWithPath("[].catchupId").description("The associated catchup channel identifier"), + fieldWithPath("[].bouquets").description("List of bouquets linked to the channel (PA_xxx)"), + fieldWithPath("[].links").description("List of links which points to entity in relation with the current channel (For now, points to the list of programs for the whole day)"), + fieldWithPath("[].logos").description("List of logos available for the given channel"), + fieldWithPath("[].terminalModels").description("List of terminal models available for the given channel") }; + + +} diff --git a/restdoc-helper-plugin/src/test/resources/Class1DTOExpected.adoc b/restdoc-helper-plugin/src/test/resources/Class1DTOExpected.adoc new file mode 100644 index 0000000..3f83c23 --- /dev/null +++ b/restdoc-helper-plugin/src/test/resources/Class1DTOExpected.adoc @@ -0,0 +1,17 @@ +[Class1DTO] += Class1DTO +This is the Class1 description +|=== +| Field| Type| Description| Constraints + +| field1 +| String +| This field describe name of Class1DTO +| Length must be between 4 and 6 + +| field2 +| String +| field 2 +| +|=== + diff --git a/restdoc-helper-plugin/src/test/resources/Class2DTOExpected.adoc b/restdoc-helper-plugin/src/test/resources/Class2DTOExpected.adoc new file mode 100644 index 0000000..6eecb47 --- /dev/null +++ b/restdoc-helper-plugin/src/test/resources/Class2DTOExpected.adoc @@ -0,0 +1,16 @@ +[Class2DTO] += Class2DTO +|=== +| Field| Type| Description| Constraints + +| field1 +| String +| field 1 +| + +| field2 +| String +| field 2 +| +|=== + diff --git a/restdoc-helper-plugin/src/test/resources/ClassSwaggerDTOExpected.adoc b/restdoc-helper-plugin/src/test/resources/ClassSwaggerDTOExpected.adoc new file mode 100644 index 0000000..9de843c --- /dev/null +++ b/restdoc-helper-plugin/src/test/resources/ClassSwaggerDTOExpected.adoc @@ -0,0 +1,22 @@ +[ClassSwaggerDTO] += ClassSwaggerDTO +This is the Class1 description +|=== +| Field| Type| Description| Constraints + +| field1 +| String +| This is the field field1 +| required + +| field2 +| String +| This is the optional field field2 +| optional + +| field3 +| String +| This is the field field3 +| required +|=== + diff --git a/restdoc-helper-plugin/src/test/resources/ClassSwaggerDTOFieldDescriptor.java.expected b/restdoc-helper-plugin/src/test/resources/ClassSwaggerDTOFieldDescriptor.java.expected new file mode 100644 index 0000000..cbd0eda --- /dev/null +++ b/restdoc-helper-plugin/src/test/resources/ClassSwaggerDTOFieldDescriptor.java.expected @@ -0,0 +1,19 @@ +package com.pconil.restdoc; + +import org.springframework.restdocs.payload.FieldDescriptor; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +public class ClassSwaggerDTOFieldDescriptor { + public static FieldDescriptor[] fdClassSwaggerDTO = new FieldDescriptor[] { + fieldWithPath("field1").description("This is the field field1"), + fieldWithPath("field2").description("This is the optional field field2").optional(), + fieldWithPath("field3").description("This is the field field3") }; + + + public static FieldDescriptor[] fdClassSwaggerDTOList = new FieldDescriptor[] { + fieldWithPath("[].field1").description("This is the field field1"), + fieldWithPath("[].field2").description("This is the optional field field2").optional(), + fieldWithPath("[].field3").description("This is the field field3") }; + + +} diff --git a/restdoc-helper-plugin/src/test/resources/ClassWithoutApiModelPropertyFieldDescriptor.java.expected b/restdoc-helper-plugin/src/test/resources/ClassWithoutApiModelPropertyFieldDescriptor.java.expected new file mode 100644 index 0000000..67ab05a --- /dev/null +++ b/restdoc-helper-plugin/src/test/resources/ClassWithoutApiModelPropertyFieldDescriptor.java.expected @@ -0,0 +1,51 @@ +package com.pconil.restdoc; + +import org.springframework.restdocs.payload.FieldDescriptor; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +public class ClassWithoutApiModelPropertyFieldDescriptor { + public static FieldDescriptor[] fdClassWithoutApiModelProperty = new FieldDescriptor[] { + fieldWithPath("id").description("Program identifier"), + fieldWithPath("programType").description("Program type"), + fieldWithPath("title").description("Program title"), + fieldWithPath("channelId").description("Id of channel broadcasting this program"), + fieldWithPath("diffusionDate").description("Diffusion date in seconds"), + fieldWithPath("duration").description("Program duration in second"), + fieldWithPath("csa").description("Program CSA level"), + fieldWithPath("kind").description("Kind of program (ex: comedy)"), + fieldWithPath("kindDetailed").description("More detailed kind (e.g Comedy-drama)"), + fieldWithPath("synopsis").description("Program sysnopsis"), + fieldWithPath("languageVersion").description("Program language"), + fieldWithPath("hearingImpaired").description("Indicates if the program has some hearing-impaired subtitle"), + fieldWithPath("audioDescription").description("Indicates if the program has an audio description track"), + fieldWithPath("season").description("If the type of the program is EPISODE, gives the season number"), + fieldWithPath("episodeNumber").description("If the type of the program is EPISODE, gives the position of the episode in the season"), + fieldWithPath("definition").description("Program definition"), + fieldWithPath("links").description("List of links which points to entities in relation with the current program"), + fieldWithPath("dayPart").description("Part of day when program start"), + fieldWithPath("catchupId").description("Identifier of the catchup program related to this live program") }; + + + public static FieldDescriptor[] fdClassWithoutApiModelPropertyList = new FieldDescriptor[] { + fieldWithPath("[].id").description("Program identifier"), + fieldWithPath("[].programType").description("Program type"), + fieldWithPath("[].title").description("Program title"), + fieldWithPath("[].channelId").description("Id of channel broadcasting this program"), + fieldWithPath("[].diffusionDate").description("Diffusion date in seconds"), + fieldWithPath("[].duration").description("Program duration in second"), + fieldWithPath("[].csa").description("Program CSA level"), + fieldWithPath("[].kind").description("Kind of program (ex: comedy)"), + fieldWithPath("[].kindDetailed").description("More detailed kind (e.g Comedy-drama)"), + fieldWithPath("[].synopsis").description("Program sysnopsis"), + fieldWithPath("[].languageVersion").description("Program language"), + fieldWithPath("[].hearingImpaired").description("Indicates if the program has some hearing-impaired subtitle"), + fieldWithPath("[].audioDescription").description("Indicates if the program has an audio description track"), + fieldWithPath("[].season").description("If the type of the program is EPISODE, gives the season number"), + fieldWithPath("[].episodeNumber").description("If the type of the program is EPISODE, gives the position of the episode in the season"), + fieldWithPath("[].definition").description("Program definition"), + fieldWithPath("[].links").description("List of links which points to entities in relation with the current program"), + fieldWithPath("[].dayPart").description("Part of day when program start"), + fieldWithPath("[].catchupId").description("Identifier of the catchup program related to this live program") }; + + +} diff --git a/restdoc-helper-plugin/src/test/resources/SimilarContent.adoc b/restdoc-helper-plugin/src/test/resources/SimilarContent.adoc new file mode 100644 index 0000000..d55103b --- /dev/null +++ b/restdoc-helper-plugin/src/test/resources/SimilarContent.adoc @@ -0,0 +1,47 @@ +[SimilarContent] += SimilarContent +SimilarContent describes a similar content as the one it refers to +|=== +| Field| Type| Description| Constraints + +| title +| String +| Title +| + +| id +| String +| Content identifier +| + +| offerId +| String +| Offer identifier +| + +| serviceType +| String +| Type of service +| + +| contentType +| String +| Type of content +| + +| similarity +| String +| Type of similarity see <> +| + +| csaLevel +| Integer +| CSA Level +| between 1 and 5 + +| images +| List +| Available images +| +|=== + diff --git a/restdoc-helper-plugin/src/test/resources/pom.xml b/restdoc-helper-plugin/src/test/resources/pom.xml new file mode 100644 index 0000000..c59dedc --- /dev/null +++ b/restdoc-helper-plugin/src/test/resources/pom.xml @@ -0,0 +1,43 @@ + + + + test-mojo + com.pconil.restdoc + 1.0.0-SNAPSHOT + + 4.0.0 + + + + com.pconil.restdoc + restdoc-helper-plugin + 1.0-SNAPSHOT + + ${basedir}/target/classes/ + com.pconil.restdoc.model + ${basedir}/target/generated-test-sources + ${basedir}/target/generated-snippets + + + + generate + generate-test-sources + + restdoc-helper + + + + + + org.springframework + spring-web + 4.3.3.RELEASE + + + + + + + \ No newline at end of file diff --git a/springboot-sample/pom.xml b/springboot-sample/pom.xml new file mode 100644 index 0000000..c090992 --- /dev/null +++ b/springboot-sample/pom.xml @@ -0,0 +1,335 @@ + + + + 4.0.0 + + com.pconil.restdoc + springboot-sample + 1.0.0-SNAPSHOT + jar + springboot-sample + Demo project for restdoc-helper annotation-parser + + + + UTF-8 + UTF-8 + 1.8 + 1.4.0.RELEASE + 4.3.2.RELEASE + 1.1.3.RELEASE + 2.5.0 + 4.2.5.RELEASE + 1.1.2.RELEASE + 4.12 + 3.3.2 + 1.5.9 + + + + + + org.springframework.boot + spring-boot-dependencies + pom + ${spring-boot.version} + import + + + + org.springframework.cloud + spring-cloud-netflix + ${spring-cloud.version} + pom + import + + + org.springframework.cloud + spring-cloud-dependencies + Brixton.SR5 + pom + import + + + org.springframework.restdocs + spring-restdocs-mockmvc + ${spring-restdoc.version} + + + + io.springfox + springfox-swagger2 + ${springfox.version} + + + io.springfox + springfox-swagger-ui + ${springfox.version} + + + io.springfox + springfox-core + ${springfox.version} + + + io.springfox + springfox-spi + ${springfox.version} + + + io.springfox + springfox-spring-web + ${springfox.version} + + + io.swagger + swagger-models + ${swagger-model.version} + + + io.springfox + springfox-staticdocs + ${springfox.version} + + + junit + junit + ${junit.version} + + + org.hamcrest + hamcrest-all + 1.3 + + + org.mockito + mockito-core + 1.10.19 + + + com.jayway.jsonpath + json-path + 2.0.0 + + + org.codehaus.plexus + plexus-utils + 2.0.5 + + + + + + + org.springframework.boot + spring-boot-starter-data-rest + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + + + + io.springfox + springfox-swagger2 + + + org.slf4j + slf4j-api + + + + + io.springfox + springfox-swagger-ui + + + io.springfox + springfox-staticdocs + test + + + + com.pconil.restdoc + restdoc-annotation + 1.0-SNAPSHOT + + + + org.slf4j + slf4j-api + + + + io.springfox + springfox-core + + + io.springfox + springfox-spi + + + io.springfox + springfox-spring-web + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 1.4.1 + + + enforce + + + + + + + enforce + + + + + + org.springframework.boot + spring-boot-maven-plugin + 1.4.1.RELEASE + + false + + + + + repackage + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + 1.8 + 1.8 + + + + org.codehaus.mojo + build-helper-maven-plugin + 1.12 + + + generate-test-sources + + add-test-source + + + + ${basedir}/target/generated-test-sources + + + + + + + com.pconil.restdoc + restdoc-helper-plugin + 1.0-SNAPSHOT + + ${basedir}/target/classes/ + com.pconil.restdoc.model + ${basedir}/target/generated-test-sources + ${basedir}/target/generated-snippets + + + + generate + generate-test-sources + + restdoc-helper + + + + + + org.springframework + spring-web + 4.3.3.RELEASE + + + + + org.asciidoctor + asciidoctor-maven-plugin + 1.5.3 + + + generate-doc + + prepare-package + + process-asciidoc + + + html + book + ${basedir}/src/main/asciidoc + + + ${basedir}/target/generated-snippets + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.0.1 + + + @ + + false + + + + copy-resources + prepare-package + + copy-resources + + + ${basedir}/target/classes/static/docs + + + ${basedir}/target/generated-docs + + + + + + + + + + \ No newline at end of file diff --git a/springboot-sample/src/main/asciidoc/ApiDocumentation.adoc b/springboot-sample/src/main/asciidoc/ApiDocumentation.adoc new file mode 100644 index 0000000..fbc8d34 --- /dev/null +++ b/springboot-sample/src/main/asciidoc/ApiDocumentation.adoc @@ -0,0 +1,43 @@ += Sample API - Getting Started Guide +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 4 +:sectlinks: + +[Introduction] += Introduction +Sample API documentation for RESTful microservice. + +[Overview] += Overview generated by swagger +include::{snippets}/overview.adoc[] + +[Model] += Models generated by restdoc-helper +:leveloffset: +1 +//insert here your generated classes docs +include::{snippets}/Program.adoc[] +:leveloffset: -1 + +[Definitions] += Model/Definitions generated via swagger +include::{snippets}/definitions.adoc[] + +[Paths] += Path Generated via swagger +include::{snippets}/paths.adoc[] + +[Tests] += Documentation from test + +== GET programs +See swagger +include::{snippets}/get-programs/response-fields.adoc[] + +=== Example request +include::{snippets}/get-programs/curl-request.adoc[] + +=== Example response +include::{snippets}/get-programs/http-response.adoc[] \ No newline at end of file diff --git a/springboot-sample/src/main/java/com/pconil/restdoc/SpringbootSampleApplication.java b/springboot-sample/src/main/java/com/pconil/restdoc/SpringbootSampleApplication.java new file mode 100644 index 0000000..3478d86 --- /dev/null +++ b/springboot-sample/src/main/java/com/pconil/restdoc/SpringbootSampleApplication.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc; + +import org.springframework.boot.Banner; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.annotation.ComponentScan; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +/** + * Spring Boot Application class. + * + * @author patrice_conil + */ +@SpringBootApplication +@ComponentScan(basePackages = "com.pconil.restdoc") +@EnableSwagger2 +public class SpringbootSampleApplication { + + /** + * Main for app. + * + * @param args arguments + */ + public static void main(String[] args) { + + new SpringApplicationBuilder() + .sources(SpringbootSampleApplication.class) + .bannerMode(Banner.Mode.LOG) + .run(args); + } +} diff --git a/springboot-sample/src/main/java/com/pconil/restdoc/configuration/StaticDocServerConfiguration.java b/springboot-sample/src/main/java/com/pconil/restdoc/configuration/StaticDocServerConfiguration.java new file mode 100644 index 0000000..e6ecd09 --- /dev/null +++ b/springboot-sample/src/main/java/com/pconil/restdoc/configuration/StaticDocServerConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.pconil.restdoc.configuration; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +/** + * Adds static/docs as resource location to handle them via servlet. + * + * @author Patrice_Conil + */ +@Configuration +@EnableWebMvc +public class StaticDocServerConfiguration extends WebMvcConfigurerAdapter { + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/docs/**").addResourceLocations("classpath:/static/docs/"); + } +} diff --git a/springboot-sample/src/main/java/com/pconil/restdoc/configuration/SwaggerDocumentationConfig.java b/springboot-sample/src/main/java/com/pconil/restdoc/configuration/SwaggerDocumentationConfig.java new file mode 100755 index 0000000..3493423 --- /dev/null +++ b/springboot-sample/src/main/java/com/pconil/restdoc/configuration/SwaggerDocumentationConfig.java @@ -0,0 +1,65 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.pconil.restdoc.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; + + +/** + * Swagger doc. + * + * @author patrice_conil + */ +@Configuration +public class SwaggerDocumentationConfig { + + /** + * Api info. + * + * @return Api info + */ + ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("Sample API") + .version("1.0.0-SNAPSHOT") + .contact(new Contact("Me", "", "me@myserver.com")) + .build(); + } + + /** + * Custom implementation. + * + * @return custom implementation doclet + */ + @Bean + public Docket customImplementation() { + return new Docket(DocumentationType.SWAGGER_2) + .select() + .apis(RequestHandlerSelectors.basePackage("com.pconil.restdoc")) + .build() + .useDefaultResponseMessages(false) + .apiInfo(apiInfo()); + } + +} diff --git a/springboot-sample/src/main/java/com/pconil/restdoc/controller/ProgramController.java b/springboot-sample/src/main/java/com/pconil/restdoc/controller/ProgramController.java new file mode 100644 index 0000000..0a31f05 --- /dev/null +++ b/springboot-sample/src/main/java/com/pconil/restdoc/controller/ProgramController.java @@ -0,0 +1,130 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.pconil.restdoc.controller; + +import com.pconil.restdoc.model.Constant; +import com.pconil.restdoc.model.Program; +import com.pconil.restdoc.service.ProgramsService; +import com.pconil.restdoc.exception.SampleException; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.ResponseHeader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.List; + +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8_VALUE; + +/** + * Controller that manages TV-EPG resources. + * + * @author patrice_conil + */ +@Controller +@RequestMapping(value = "/v1", produces = {APPLICATION_JSON_UTF8_VALUE}) +@Api(value = "/programs", tags = {"/v1/programs"}, description = "the programs API") +public class ProgramController { + + /** + * Logger. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(ProgramController.class); + + /** + * Programs com.pconil.restdoc.service. + */ + @Autowired + private ProgramsService programsService; + + + /** + * Default constructor. + */ + public ProgramController() { + } + + /** + * Get a list of programs (light information) for a range of channels (one, a list with commas or all) + * and a specific date formatted as 'YYYY-MM-DD'. + * + * @param date date: a specific date formatted as 'YYYY-MM-DD' + * @param application application requesting + * @param channels channels for which you need to retrieve programs + * @param offset offset of first program to retrieve + * @param limit maximum number of items to retrieve + * @return list of program summaries + */ + @RequestMapping(value = "/programs", produces = {"application/json;charset=UTF-8"}, method = RequestMethod.GET) + @ApiOperation(value = "Get a list of programs given a date for a given range of channels possibly", + notes = "Get a list of programs for a range of channels (one, a list with commas or " + + "all) a specific date formatted as 'YYYY-MM-DD'.", + response = Program.class, + responseContainer = "List") + @ApiResponses(value = { + @ApiResponse(code = Constant.OK, message = "OK", response = Program.class, responseContainer = "List", + responseHeaders = { + @ResponseHeader(name = "X-Result-Count", + description = "The actual number of items contained in the response body.", + response = String.class), + @ResponseHeader(name = "X-Total-Count", + description = "The total number of items in the collection.", response = String.class), + @ResponseHeader(name = "Cache-Control[max-age,public]", + description = "Contains max-age in seconds", response = + String.class), + @ResponseHeader(name = "ETag", description = "The Entity Tag", response = String.class)}), + @ApiResponse(code = Constant.BAD_REQUEST, message = "Bad Request", response = Error.class), + @ApiResponse(code = Constant.INTERNAL_EROR, message = "Internal Server Error", response = Error.class)}) + public ResponseEntity> getPrograms( + @RequestParam(value = "date", required = false, defaultValue = "current") String date, + @RequestParam(value = "application", required = false, defaultValue = "PC") String application, + @RequestParam(value = "channels", required = false, defaultValue = "all") List channels, + @RequestParam(value = "offset", required = false, defaultValue = "0") Integer offset, + @RequestParam(value = "limit", required = false, defaultValue = "10") Integer limit) { + + LOGGER.info("[getPrograms] date={}, application={}, channels={}, offset={}, limit={}", + date, application, channels.toString(), offset, limit); + if (date.equals("current")) { + date = LocalDate.now().toString(); + } else { + try { + LocalDate.parse(date); + } catch (DateTimeParseException e) { + throw new SampleException("Bad date format"); + } + } + List lightPaginated = programsService.findAllPrograms(date, application, channels); + ResponseEntity> responseEntity = ResponseEntity.ok() + .body(lightPaginated); + LOGGER.debug("[getPrograms] response = {}", responseEntity.toString()); + return responseEntity; + } + + + + +} diff --git a/springboot-sample/src/main/java/com/pconil/restdoc/exception/SampleException.java b/springboot-sample/src/main/java/com/pconil/restdoc/exception/SampleException.java new file mode 100644 index 0000000..0fb86af --- /dev/null +++ b/springboot-sample/src/main/java/com/pconil/restdoc/exception/SampleException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc.exception; + +/** + * Sample com.pconil.restdoc.exception to show how exceptionHandling works. + * + * @author patrice_conil + */ +public class SampleException extends RuntimeException { + + /** + * The error. + */ + private Error error; + + /** + * Constructor. + * @param message the message to store + */ + public SampleException(String message) { + super(message); + error = new Error(message); + } + + public Error getError() { + return error; + } +} diff --git a/springboot-sample/src/main/java/com/pconil/restdoc/model/Constant.java b/springboot-sample/src/main/java/com/pconil/restdoc/model/Constant.java new file mode 100644 index 0000000..25b91d8 --- /dev/null +++ b/springboot-sample/src/main/java/com/pconil/restdoc/model/Constant.java @@ -0,0 +1,41 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.pconil.restdoc.model; + +/** + * All constants. + * + * @author patrice_conil + */ +public class Constant { + + /** + * 200 OK reply. + */ + public static final int OK = 200; + + /** + * 400 BAD_REQUEST reply. + */ + public static final int BAD_REQUEST = 400; + + /** + * 500 INTERNAL_ERROR 500. + */ + public static final int INTERNAL_EROR = 500; + +} diff --git a/springboot-sample/src/main/java/com/pconil/restdoc/model/ExceptionHandlingControllerAdvice.java b/springboot-sample/src/main/java/com/pconil/restdoc/model/ExceptionHandlingControllerAdvice.java new file mode 100644 index 0000000..9b4891c --- /dev/null +++ b/springboot-sample/src/main/java/com/pconil/restdoc/model/ExceptionHandlingControllerAdvice.java @@ -0,0 +1,50 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc.model; + +import com.pconil.restdoc.exception.SampleException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +/** + * Performs exception handling globally. The exceptions below could be raised by any + * controller and they would be handled here, if not handled in the controller. + * + * @author patrice_conil + */ +@ControllerAdvice +public class ExceptionHandlingControllerAdvice { + + /** + * Logger. + */ + private static Logger logger = LoggerFactory.getLogger(ExceptionHandlingControllerAdvice.class); + + /** + * Resource Not found handler. + * @param exception the com.pconil.restdoc.exception + * @return NotFound Error + */ + @ExceptionHandler(SampleException.class) + public ResponseEntity handleException(SampleException exception) { + logger.warn("OTMLException: ", exception.getMessage()); + return new ResponseEntity<>(new Error(exception.getMessage()), HttpStatus.BAD_REQUEST); + } +} diff --git a/springboot-sample/src/main/java/com/pconil/restdoc/model/Program.java b/springboot-sample/src/main/java/com/pconil/restdoc/model/Program.java new file mode 100755 index 0000000..cfbefe3 --- /dev/null +++ b/springboot-sample/src/main/java/com/pconil/restdoc/model/Program.java @@ -0,0 +1,454 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.pconil.restdoc.annotation.AsciidocAnnotation; +import com.pconil.restdoc.annotation.InspectToDocument; +import io.swagger.annotations.ApiModelProperty; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Object that 'summarize' a Program (light data). + * + * @author patrice_conil + */ +@InspectToDocument(description = "Electronic Program Guide Item") +public class Program { + + /** + * Program identifier. + */ + @AsciidocAnnotation(description = "Program identifier") + @ApiModelProperty(required = true, value = "Program identifier") + @JsonProperty("id") + private String id = null; + + /** + * Program type. + */ + @AsciidocAnnotation(description = "Program type") + @ApiModelProperty(required = true, value = "Program type") + @JsonProperty("programType") + private String programType = null; + + /** + * Program title. + */ + @AsciidocAnnotation(description = "Program title") + @ApiModelProperty(required = true, value = "Program title") + @JsonProperty("title") + private String title = null; + + /** + * Id of channel broadcasting this program. + */ + @AsciidocAnnotation(description = "Id of channel broadcasting this program") + @ApiModelProperty(required = true, value = "Identifier of the channel where the program is broadcasted") + @JsonProperty("channelId") + private String channelId = null; + + /** + * Diffusion date in seconds. + */ + @AsciidocAnnotation(description = "Diffusion date in seconds") + @ApiModelProperty(required = true, value = "Diffusion date in seconds") + @JsonProperty("diffusionDate") + private Long diffusionDate = null; + + /** + * Program duration in second. + */ + @AsciidocAnnotation(description = "Program duration in second") + @ApiModelProperty(required = true, value = "Duration in seconds") + @JsonProperty("duration") + private Long duration = null; + + /** + * Program CSA level. + */ + @AsciidocAnnotation(description = "Program CSA level") + @ApiModelProperty(required = true, value = "Audience maturity level") + @JsonProperty("csa") + private Long csa = null; + + /** + * Kind of program (ex: variety). + */ + @AsciidocAnnotation(description = "Kind of program (ex: comedy)") + @ApiModelProperty(required = true, value = "Program kind (e.g Comedy)") + @JsonProperty("kind") + private String kind = null; + + /** + * More detailed kind (e.g Comedy-drama). + */ + @AsciidocAnnotation(description = "More detailed kind (e.g Comedy-drama)") + @ApiModelProperty(required = true, value = "More detailed kind (e.g Comedy-drama)") + @JsonProperty("kindDetailed") + private String kindDetailed = null; + + /** + * Program sysnopsis. + */ + @AsciidocAnnotation(description = "Program sysnopsis") + @ApiModelProperty(required = true, value = "Program synopsis") + @JsonProperty("synopsis") + private String synopsis = null; + + /** + * Program language. + */ + @AsciidocAnnotation(description = "Program language") + @ApiModelProperty(required = true, value = "Program language") + @JsonProperty("languageVersion") + private String languageVersion = null; + + /** + * Indicates if the program has some hearing-impaired subtitle. + */ + @AsciidocAnnotation(description = "Indicates if the program has some hearing-impaired subtitle") + @ApiModelProperty(required = true, value = "Indicates if the program has some hearing-impaired subtitle") + @JsonProperty("hearingImpaired") + private Boolean hearingImpaired = null; + + /** + * Indicates if the program has an audio description track. + */ + @ApiModelProperty(required = true, value = "Indicates if the program has an audio description track") + @JsonProperty("audioDescription") + @AsciidocAnnotation(description = "Indicates if the program has an audio description track") + private Boolean audioDescription = null; + + /** + * If the type of the program is EPISODE, gives the season number. + */ + @AsciidocAnnotation(description = "If the type of the program is EPISODE, gives the season number") + @ApiModelProperty(required = true, value = "If the type of the program is EPISODE, gives the season number") + @JsonProperty("season") + private Long season = null; + + /** + * If the type of the program is EPISODE, gives the position of the episode in the season. + */ + @AsciidocAnnotation(description = "If the type of the program is EPISODE, gives the position of the episode in " + + "the season") + @ApiModelProperty(required = true, value = "if the type of the program is EPISODE, gives the position of the episode" + + "in the season") + @JsonProperty("episodeNumber") + private String episodeNumber = null; + + /** + * Program definition. + */ + @AsciidocAnnotation(description = "Program definition") + @ApiModelProperty(required = true, value = "Program definition", allowableValues = "SD, HD") + @JsonProperty("definition") + private String definition = null; + + /** + * List of links which points to entity in relation with the current program. + */ + @AsciidocAnnotation(description = "List of <> which points to entity in relation with" + + " the current program") + @ApiModelProperty(required = true, value = "List of links which points to entity in relation with the current program") + @JsonProperty("links") + private List links = new ArrayList<>(); + + /** + * Part of day when program is broadcast(ed). + */ + @AsciidocAnnotation(description = "Part of day when program start") + @ApiModelProperty(required = true, value = "Part of day when program start, PT for prime time", + allowableValues = "PT1, PT2, PT3, OTHER") + @JsonProperty("dayPart") + private String dayPart = null; + + /** + * Identifier of the catchup program related to this live program. + */ + @AsciidocAnnotation(description = "Identifier of the catchup program related to this live program") + @ApiModelProperty(required = true, value = "Identifier of the catchup program related to this live program") + @JsonProperty("catchupId") + private String catchupId = null; + + + // + /** + * Full param constructor. + */ + @SuppressWarnings("all") + @JsonIgnore + public Program(String id, String programType, String title, String channelId, + Long diffusionDate, Long duration, Long csa, String kind, String kindDetailed, + String synopsis, + String languageVersion, Boolean hearingImpaired, Boolean audioDescription, + Long season, + String episodeNumber, String definition, List links, + String dayPart, + String catchupId) { + this.id = id; + this.programType = programType; + this.title = title; + this.channelId = channelId; + this.diffusionDate = diffusionDate; + this.duration = duration; + this.csa = csa; + this.kind = kind; + this.kindDetailed = kindDetailed; + this.synopsis = synopsis; + this.languageVersion = languageVersion; + this.hearingImpaired = hearingImpaired; + this.audioDescription = audioDescription; + this.season = season; + this.episodeNumber = episodeNumber; + this.definition = definition; + this.links = links; + this.dayPart = dayPart; + this.catchupId = catchupId; + } + + /** + * Default constructor. + */ + public Program() { + + } + // + + // + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getProgramType() { + return programType; + } + + public void setProgramType(String programType) { + this.programType = programType; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getChannelId() { + return channelId; + } + + public void setChannelId(String channelId) { + this.channelId = channelId; + } + + public Long getDiffusionDate() { + return diffusionDate; + } + + public void setDiffusionDate(Long diffusionDate) { + this.diffusionDate = diffusionDate; + } + + public Long getDuration() { + return duration; + } + + public void setDuration(Long duration) { + this.duration = duration; + } + + public Long getCsa() { + return csa; + } + + public void setCsa(Long csa) { + this.csa = csa; + } + + public String getKind() { + return kind; + } + + public void setKind(String kind) { + this.kind = kind; + } + + public String getKindDetailed() { + return kindDetailed; + } + + public void setKindDetailed(String kindDetailed) { + this.kindDetailed = kindDetailed; + } + + public String getSynopsis() { + return synopsis; + } + + public void setSynopsis(String synopsis) { + this.synopsis = synopsis; + } + + public String getLanguageVersion() { + return languageVersion; + } + + public void setLanguageVersion(String languageVersion) { + this.languageVersion = languageVersion; + } + + public Boolean getHearingImpaired() { + return hearingImpaired; + } + + public void setHearingImpaired(Boolean hearingImpaired) { + this.hearingImpaired = hearingImpaired; + } + + public Boolean getAudioDescription() { + return audioDescription; + } + + public void setAudioDescription(Boolean audioDescription) { + this.audioDescription = audioDescription; + } + + public Long getSeason() { + return season; + } + + public void setSeason(Long season) { + this.season = season; + } + + public String getEpisodeNumber() { + return episodeNumber; + } + + public void setEpisodeNumber(String episodeNumber) { + this.episodeNumber = episodeNumber; + } + + public String getDefinition() { + return definition; + } + + public void setDefinition(String definition) { + this.definition = definition; + } + + public List getLinks() { + return links; + } + + public void setLinks(List links) { + this.links = links; + } + + public String getDayPart() { + return dayPart; + } + + public void setDayPart(String dayPart) { + this.dayPart = dayPart; + } + + public String getCatchupId() { + return catchupId; + } + + public void setCatchupId(String catchupId) { + this.catchupId = catchupId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Program programLight = (Program) o; + return Objects.equals(id, programLight.id) + && Objects.equals(programType, programLight.programType) + && Objects.equals(title, programLight.title) + && Objects.equals(channelId, programLight.channelId) + && Objects.equals(diffusionDate, programLight.diffusionDate) + && Objects.equals(duration, programLight.duration) + && Objects.equals(csa, programLight.csa) + && Objects.equals(kind, programLight.kind) + && Objects.equals(kindDetailed, programLight.kindDetailed) + && Objects.equals(synopsis, programLight.synopsis) + && Objects.equals(languageVersion, programLight.languageVersion) + && Objects.equals(hearingImpaired, programLight.hearingImpaired) + && Objects.equals(audioDescription, programLight.audioDescription) + && Objects.equals(season, programLight.season) + && Objects.equals(episodeNumber, programLight.episodeNumber) + && Objects.equals(definition, programLight.definition) + && Objects.equals(links, programLight.links) + && Objects.equals(dayPart, programLight.dayPart) + && Objects.equals(catchupId, programLight.catchupId); + } + + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public String toString() { + return "class ProgramLight {\n" + + " id: " + id + "\n" + + " programType: " + programType + "\n" + + " title: " + title + "\n" + + " channelId: " + channelId + "\n" + + " diffusionDate: " + diffusionDate + "\n" + + " duration: " + duration + "\n" + + " csa: " + csa + "\n" + + " kind: " + kind + "\n" + + " kindDetailed: " + kindDetailed + "\n" + + " synopsis: " + synopsis + "\n" + + " languageVersion: " + languageVersion + "\n" + + " hearingImpaired: " + hearingImpaired + "\n" + + " audioDescription: " + audioDescription + "\n" + + " season: " + season + "\n" + + " episodeNumber: " + episodeNumber + "\n" + + " definition: " + definition + "\n" + + " links: " + links + "\n" + + " dayPart: " + dayPart + "\n" + + " catchupId: " + catchupId + "\n" + + "}\n"; + } + // +} + + diff --git a/springboot-sample/src/main/java/com/pconil/restdoc/service/ProgramsService.java b/springboot-sample/src/main/java/com/pconil/restdoc/service/ProgramsService.java new file mode 100644 index 0000000..0054a7d --- /dev/null +++ b/springboot-sample/src/main/java/com/pconil/restdoc/service/ProgramsService.java @@ -0,0 +1,38 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc.service; + +import com.pconil.restdoc.model.Program; + +import java.util.List; + + +/** + * Interface that services must implement to comply with controller. + * + * @author patrice_conil + */ +public interface ProgramsService { + + /** + * Retrieves all the programs from EPG. + * @param date the date to consider + * @param application the application that request the data + * @param channels list of channel to consider + * @return List of programs + */ + List findAllPrograms(String date, String application, List channels); +} diff --git a/springboot-sample/src/main/java/com/pconil/restdoc/service/ProgramsServiceImpl.java b/springboot-sample/src/main/java/com/pconil/restdoc/service/ProgramsServiceImpl.java new file mode 100644 index 0000000..8d9dd15 --- /dev/null +++ b/springboot-sample/src/main/java/com/pconil/restdoc/service/ProgramsServiceImpl.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.pconil.restdoc.service; + + +import com.pconil.restdoc.model.Program; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +/** + * Service that retrieves program resources. + * + * @author patrice_conil + */ +@Service("programsService") +public class ProgramsServiceImpl implements ProgramsService { + + + /** + * Constructor. + */ + public ProgramsServiceImpl() { + } + + @Override + public List findAllPrograms(String date, String application, List channels) { + + Program programLight = new Program("0", "EPISODE", "House of cards", + "Canal+", 0L, 1L, 1L, "comedy", "romantic comedy", "a beautiful synopsis", + "VF", false, false, 2L, "1", "HD", null, "PT1", + "catchupId"); + List programLights = new ArrayList<>(); + programLights.add(programLight); + programLight = new Program("0", "EPISODE", "House of cards", "Canal+", 1L, + 1L, 1L, "comedy", "romantic comedy", "a beautiful synopsis", "VF", false, false, + 2L, "2", "HD", null, "PT1", "catchupId"); + programLights.add(programLight); + return programLights; + } +} diff --git a/springboot-sample/src/main/resources/application.properties b/springboot-sample/src/main/resources/application.properties new file mode 100644 index 0000000..d623bb4 --- /dev/null +++ b/springboot-sample/src/main/resources/application.properties @@ -0,0 +1 @@ +logging.level=debug \ No newline at end of file diff --git a/springboot-sample/src/test/java/SpringbootSampleApplicationTests.java b/springboot-sample/src/test/java/SpringbootSampleApplicationTests.java new file mode 100644 index 0000000..b61e962 --- /dev/null +++ b/springboot-sample/src/test/java/SpringbootSampleApplicationTests.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import com.pconil.restdoc.SpringbootSampleApplication; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.web.WebAppConfiguration; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = SpringbootSampleApplication.class) +@WebAppConfiguration +public class SpringbootSampleApplicationTests { + + @Test + public void contextLoads() { + } + +} diff --git a/springboot-sample/src/test/java/SwaggerStaticDocGeneratorTest.java b/springboot-sample/src/test/java/SwaggerStaticDocGeneratorTest.java new file mode 100644 index 0000000..fc8dba6 --- /dev/null +++ b/springboot-sample/src/test/java/SwaggerStaticDocGeneratorTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import com.pconil.restdoc.SpringbootSampleApplication; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.context.WebApplicationContext; +import springfox.documentation.staticdocs.Swagger2MarkupResultHandler; + +import java.io.File; +import java.util.Arrays; +import java.util.List; + +import static junit.framework.TestCase.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; + +/** + * Adoc generation from swagger /v2/api-docs. + * This test produces 3 docs : definitions.adoc, overview.adoc, paths.adoc. + * These docs can be referenced by other adoc files to produce more precise documentation on your API + * (for example sequence diagram, or other additional information). + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = SpringbootSampleApplication.class) +@ActiveProfiles("test") +@WebAppConfiguration +public class SwaggerStaticDocGeneratorTest { + + private MockMvc mockMvc; + + @Autowired + private WebApplicationContext webApplicationContext; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mockMvc = webAppContextSetup(webApplicationContext).build(); + } + + + /** + * Calls Swagger2MarkupResultHandler to generate adoc files from swagger.json/swagger.yaml. + * @throws Exception if something goes wrong + */ + @Test + public void generateAsciiDoc() throws Exception { + String outDir = System.getProperty("asciiDocOutputDir", "target/generated-snippets"); + Swagger2MarkupResultHandler resultHandler = Swagger2MarkupResultHandler + .outputDirectory(outDir) + .build(); + this.mockMvc.perform(get("/v2/api-docs").accept(MediaType.APPLICATION_JSON)) + .andDo(resultHandler) + .andExpect(status().isOk()); + + assertTrue(allDocsAreGenerated(outDir)); + } + + /** + * Verify that the three expected docs are generated. + * definitions.adoc, overview.adoc, paths.adoc + * @param outDir the directory where the doc must be generated + * @return true if the three docs are present + */ + private boolean allDocsAreGenerated(String outDir) { + boolean result = true; + String[] names = {"overview.adoc", "paths.adoc", "definitions.adoc"}; + List fileNames = Arrays.asList(names); + for (String file : fileNames) { + if (!new File(outDir + '/' + file).exists()) { + result = false; + } + } + return result; + } +} \ No newline at end of file diff --git a/springboot-sample/src/test/java/com/pconil/restdoc/ProgramControllerTest.java b/springboot-sample/src/test/java/com/pconil/restdoc/ProgramControllerTest.java new file mode 100644 index 0000000..a5532ba --- /dev/null +++ b/springboot-sample/src/test/java/com/pconil/restdoc/ProgramControllerTest.java @@ -0,0 +1,181 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.pconil.restdoc; + + +import com.pconil.restdoc.controller.ProgramController; +import com.pconil.restdoc.model.Program; +import com.pconil.restdoc.model.ProgramFieldDescriptor; +import com.pconil.restdoc.service.ProgramsService; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.context.WebApplicationContext; + +import java.io.File; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; + +/** + * Unit test that document your API. + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = SpringbootSampleApplication.class) +@WebAppConfiguration +public class ProgramControllerTest { + private MockMvc mockMvc; + + @Mock + ProgramsService programsService; + + @Autowired + private WebApplicationContext webApplicationContext; + + @InjectMocks + @Autowired + private ProgramController programController; + + + @Rule + //Need to generate in parent project dir to collect all needed data to produce html + public JUnitRestDocumentation restDocumentation = + new JUnitRestDocumentation("target/generated-snippets"); + + private RestDocumentationResultHandler restDocumentationResultHandler; + + + @Before + public void setUp() throws Exception { + // Creates snippets named using method-name expansion + restDocumentationResultHandler = document("{method-name}", preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint())); + + MockitoAnnotations.initMocks(this); + this.mockMvc = webAppContextSetup(webApplicationContext).apply(documentationConfiguration(restDocumentation)) + .alwaysDo(restDocumentationResultHandler).build(); + + } + + @After + public void tearDown() throws Exception { + //Reset Mock + + } + + + @Test + public void getPrograms() throws Exception { + + when(programsService.findAllPrograms(any(), any(), any())) + .thenReturn(findAllPrograms(LocalDate.now().toString(), "PC", Collections.singletonList("19"))); + + mockMvc.perform(get("/v1/programs").contentType(MediaType.APPLICATION_JSON_UTF8).header("Host", "localhost") + .accept(MediaType.APPLICATION_JSON_UTF8)) + /* + * Here's the magic doc + */ + .andDo(document("{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), + responseFields( + /* + * Here we use the java code generated accordingly to swagger annotations + */ + ProgramFieldDescriptor.fdProgramList + )) + ) + /* + * Expect your response is valid + */ + .andExpect(MockMvcResultMatchers.jsonPath("$", hasSize(2))) + .andExpect(MockMvcResultMatchers.jsonPath("$[0].id").value("0")) + .andExpect(MockMvcResultMatchers.jsonPath("$[0].programType").value("EPISODE")) + .andExpect(status().isOk()); + + //Checks that snippet exists + assertEquals(true, new File("target/generated-snippets/get-programs").exists()); + } + + @Test + public void getProgramsWithBadDate() throws Exception { + + MultiValueMap params = new LinkedMultiValueMap(); + params.set("date", "badDate"); + + mockMvc.perform(get("/v1/programs").contentType(MediaType.APPLICATION_JSON_UTF8).header("Host", "localhost") + .params(params) + .accept(MediaType.APPLICATION_JSON_UTF8)) + /* + * Here's the magic doc + */ + .andDo(document("{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()) + ) + ) + // Expect your response is an error + .andExpect(status().is4xxClientError()); + + //Checks that snippet exists + assertEquals(true, new File("target/generated-snippets/get-programs-with-bad-date").exists()); + } + + + private List findAllPrograms(String date, String application, List channels) { + + Program programLight = new Program("0", "EPISODE", "House of cards", + "Canal+", 0L, 2400L, 1L, "comedy", "romantic comedy", "a beautiful synopsis", + "VF", false, false, 2L, "1", "HD", null, "PT1", + "catchupId"); + List programLights = new ArrayList<>(); + programLights.add(programLight); + programLight = new Program("0", "EPISODE", "House of cards", "Canal+", 1L, + 2400L, 1L, "comedy", "romantic comedy", "a beautiful synopsis", "VF", false, false, + 2L, "2", "HD", null, "PT1", "catchupId"); + programLights.add(programLight); + return programLights; + } + +} \ No newline at end of file