├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.sbt ├── hystrix.png ├── project ├── build.properties └── plugins.sbt ├── src ├── main │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── takezoe │ │ │ └── resty │ │ │ ├── Action.java │ │ │ ├── Controller.java │ │ │ └── Param.java │ ├── resources │ │ ├── logback.xml │ │ └── public │ │ │ └── vendors │ │ │ └── swagger-ui │ │ │ ├── css │ │ │ ├── print.css │ │ │ ├── reset.css │ │ │ ├── screen.css │ │ │ ├── style.css │ │ │ └── typography.css │ │ │ ├── fonts │ │ │ ├── DroidSans-Bold.ttf │ │ │ └── DroidSans.ttf │ │ │ ├── images │ │ │ ├── collapse.gif │ │ │ ├── expand.gif │ │ │ ├── explorer_icons.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── logo_small.png │ │ │ ├── pet_store_api.png │ │ │ ├── throbber.gif │ │ │ └── wordnik_api.png │ │ │ ├── index.html │ │ │ ├── lang │ │ │ ├── ca.js │ │ │ ├── el.js │ │ │ ├── en.js │ │ │ ├── es.js │ │ │ ├── fr.js │ │ │ ├── geo.js │ │ │ ├── it.js │ │ │ ├── ja.js │ │ │ ├── ko-kr.js │ │ │ ├── pl.js │ │ │ ├── pt.js │ │ │ ├── ru.js │ │ │ ├── tr.js │ │ │ ├── translator.js │ │ │ └── zh-cn.js │ │ │ ├── lib │ │ │ ├── backbone-min.js │ │ │ ├── es5-shim.js │ │ │ ├── handlebars-4.0.5.js │ │ │ ├── highlight.9.1.0.pack.js │ │ │ ├── highlight.9.1.0.pack_extended.js │ │ │ ├── jquery-1.8.0.min.js │ │ │ ├── jquery.ba-bbq.min.js │ │ │ ├── jquery.slideto.min.js │ │ │ ├── jquery.wiggle.min.js │ │ │ ├── js-yaml.min.js │ │ │ ├── jsoneditor.min.js │ │ │ ├── lodash.min.js │ │ │ ├── marked.js │ │ │ ├── object-assign-pollyfill.js │ │ │ ├── sanitize-html.min.js │ │ │ └── swagger-oauth.js │ │ │ ├── o2c.html │ │ │ ├── swagger-ui.js │ │ │ └── swagger-ui.min.js │ └── scala │ │ └── com │ │ └── github │ │ └── takezoe │ │ └── resty │ │ ├── ActionResult.scala │ │ ├── CORSSupport.scala │ │ ├── HttpClientSupport.scala │ │ ├── HystrixSupport.scala │ │ ├── ParamInjector.scala │ │ ├── Resty.scala │ │ ├── RestyKernel.scala │ │ ├── SwaggerController.scala │ │ ├── model │ │ ├── ActionDef.scala │ │ ├── AppInfo.scala │ │ ├── ControllerDef.scala │ │ ├── ParamConverter.scala │ │ └── ParamDef.scala │ │ ├── servlet │ │ ├── FileResourceServlet.scala │ │ ├── InitializeListener.scala │ │ ├── ResourceServlet.scala │ │ ├── RestyServlet.scala │ │ ├── SwaggerUIServlet.scala │ │ ├── WebJarsServlet.scala │ │ └── ZipkinBraveFilter.scala │ │ └── util │ │ ├── JsonUtils.scala │ │ ├── ReflectionUtils.scala │ │ ├── ScaladocUtils.scala │ │ └── StringUtils.scala └── test │ └── scala │ └── com │ └── github │ └── takezoe │ └── resty │ ├── RandomRequestTargetSpec.scala │ └── util │ └── ReflectionUtilsSpec.scala └── swagger.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | .ensime 4 | .ensime_cache 5 | 6 | # sbt specific 7 | dist/* 8 | target/ 9 | lib_managed/ 10 | src_managed/ 11 | project/boot/ 12 | project/plugins/project/ 13 | 14 | # Scala-IDE specific 15 | .scala_dependencies 16 | .classpath 17 | .project 18 | .cache 19 | .settings 20 | 21 | # IntelliJ specific 22 | .idea/ 23 | .idea_modules/ 24 | 25 | # Metals specific 26 | .metals 27 | .bloop 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | script: 3 | - sbt test 4 | jdk: 5 | - openjdk8 6 | cache: 7 | directories: 8 | - $HOME/.ivy2/cache 9 | - $HOME/.sbt/boot 10 | - $HOME/.sbt/launchers 11 | - $HOME/.coursier 12 | 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Resty [![Build Status](https://travis-ci.org/takezoe/resty.svg?branch=master)](https://travis-ci.org/takezoe/resty) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.takezoe/resty_2.12/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.takezoe/resty_2.12) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/takezoe/resty/blob/master/LICENSE) 2 | ======== 3 | 4 | Super easy REST API framework for Scala 5 | 6 | You can run the sample project by hitting following commands: 7 | 8 | ``` 9 | $ git clone https://github.com/takezoe/resty-sample.git 10 | $ cd resty-sample/ 11 | $ sbt ~jetty:start 12 | ``` 13 | 14 | Check APIs via Swagger UI at: `http://localhost:8080/swagger-ui/`. 15 | 16 | ## Getting started 17 | 18 | This is a simplest controller example: 19 | 20 | ```scala 21 | import com.github.takezoe.resty._ 22 | 23 | class HelloController { 24 | @Action(method = "GET", path = "/hello/{name}") 25 | def hello(name: String): Message = { 26 | Message(s"Hello ${name}!") 27 | } 28 | } 29 | 30 | case class Message(message: String) 31 | ``` 32 | 33 | Define a web listener that registers your controller. 34 | 35 | ```scala 36 | @WebListener 37 | class InitializeListener extends ServletContextListener { 38 | override def contextDestroyed(sce: ServletContextEvent): Unit = { 39 | } 40 | override def contextInitialized(sce: ServletContextEvent): Unit = { 41 | Resty.register(new HelloController()) 42 | } 43 | } 44 | ``` 45 | 46 | Let's test this controller. 47 | 48 | ``` 49 | $ curl -XGET http://localhost:8080/hello/resty 50 | {"message": "Hello resty!" } 51 | ``` 52 | 53 | ## Annotations 54 | 55 | Resty provides some annotations including `@Action`. 56 | 57 | ### @Controller 58 | 59 | You can add `@Controller` to the controller class to define the controller name and description. They are applied to Swagger JSON. 60 | 61 | |parameter |required |description | 62 | |------------|---------|------------------------------| 63 | |name |optional |name of the controller | 64 | |description |optional |description of the controller | 65 | 66 | ```scala 67 | @Controller(name = "hello", description = "HelloWorld API") 68 | class HelloController { 69 | ... 70 | } 71 | ``` 72 | 73 | ### @Action 74 | 75 | We already looked `@Action` to annotate the action method. It has some more parameters to add more information about the action. 76 | 77 | |parameter |required |description | 78 | |------------|---------|-----------------------------------------------------| 79 | |method |required |GET, POST, PUT or DELETE | 80 | |path |required |path of the action (`{name}` defines path parameter) | 81 | |description |optional |description of the method | 82 | |deprecated |optional |if true then deprecated (default is false) | 83 | 84 | ```scala 85 | class HelloController { 86 | @Action(method = "GET", path = "/v1/hello", 87 | description = "Old version of HelloWorld API", deprecated = true) 88 | def hello() = { 89 | ... 90 | } 91 | } 92 | ``` 93 | 94 | ### @Param 95 | 96 | `@Param` is added to the arguments of the action method to define advanced parameter binding. 97 | 98 | |parameter |required |description | 99 | |------------|---------|-----------------------------------------------------| 100 | |from |optional |query, path, header or body | 101 | |name |optional |parameter or header name (default is arg name) | 102 | |description |optional |description of the parameter | 103 | 104 | ```scala 105 | class HelloController { 106 | @Action(method = "GET", path = "/hello") 107 | def hello( 108 | @Param(from = "query", name = "user-name") userName: String, 109 | @Param(from = "header", name = "User-Agent") userAgent: String 110 | ) = { 111 | ... 112 | } 113 | } 114 | ``` 115 | 116 | ## Types 117 | 118 | Resty supports following types as the parameter argument: 119 | 120 | - `Unit` 121 | - `String` 122 | - `Int` 123 | - `Long` 124 | - `Boolean` 125 | - `Option[T]` 126 | - `Seq[T]` 127 | - `Array[T]` 128 | - `Array[Byte]` (for Base64 encoded string) 129 | - `AnyRef` (for JSON in the request body) 130 | 131 | Also following types are supported as the return value of the action method: 132 | 133 | - `String` is responded as `text/plain; charset=UTF-8` 134 | - `Array[Byte]`, `InputStream`, `java.io.File` are responded as `application/octet-stream` 135 | - `AnyRef` is responded as `application/json` 136 | - `ActionResult[_]` is responded as specified status, headers and body 137 | - `Future[_]` is processed asynchronously using `AsyncContext` 138 | 139 | ## Servlet API 140 | 141 | You can access Servlet API by defining method arguments with following types: 142 | 143 | - `HttpServletRequest` 144 | - `HttpServletResponse` 145 | - `HttpSession` 146 | - `ServletContext` 147 | 148 | ```scala 149 | class HelloController { 150 | @Action(method = "GET", path = "/hello") 151 | def hello(request: HttpServletRequest): Message = { 152 | val name = request.getParameter("name") 153 | Message(s"Hello ${name}!") 154 | } 155 | } 156 | ``` 157 | 158 | ## Validation 159 | 160 | It's possible to validate JSON properties by asserting properties in the constructor of the mapped case class. 161 | 162 | ```scala 163 | case class Message(message: String){ 164 | assert(message.length < 10, "message must be less than 10 charactors.") 165 | } 166 | ``` 167 | 168 | When the parameter value is invalid, Resty responds the following response with the `400 BadRequest` status: 169 | 170 | ```javascript 171 | { 172 | "errors": [ 173 | "message must be less than 10 charactors." 174 | ] 175 | } 176 | ``` 177 | 178 | ## HTTP client 179 | 180 | `HttpClientSupport` trait offers methods to send HTTP request. You can call other Web APIs easily using these methods. 181 | 182 | ```scala 183 | class HelloController extends HttpClientSupport { 184 | @Action(method = "GET", path = "/hello/{id}") 185 | def hello(id: Int): Message = { 186 | // Call other API using methods provided by HttpClientSupport 187 | val user: User = httpGet[User](s"http://localhost:8080/user/${id}") 188 | Message(s"Hello ${user.name}!") 189 | } 190 | 191 | @Action(method = "GET", path = "/hello-async/{id}") 192 | def helloAsync(id: Int): Future[Message] = { 193 | // HttpClientSupport also supports asynchronous communication 194 | val future: Future[Either[ErrorModel, User]] = httpGetAsync[User](s"http://localhost:8080/user/${id}") 195 | future.map { 196 | case Right(user) => Message(s"Hello ${user.name}!") 197 | case Left(error) => throw new ActionResultException(InternalServerError(error)) 198 | } 199 | } 200 | } 201 | ``` 202 | 203 | These methods have retrying ability and circuit breaker. You can configure these behavior by defining `HttpClientConfig` as an implicit value. 204 | 205 | ```scala 206 | class HelloController extends HttpClientSupport { 207 | 208 | implicit override val httpClientConfig = HttpClientConfig( 209 | maxRetry = 5, // max number of retry. default is 0 (no retry) 210 | retryInterval = 500, // interval of retry (msec). default is 0 (retry immediately) 211 | maxFailure = 3, // max number until open circuit breaker. default is 0 (disabling circuit breaker) 212 | resetInterval = 60000 // interval to reset closed circuit breaker (msec). default is 60000 213 | ) 214 | 215 | ... 216 | } 217 | ``` 218 | 219 | ## Swagger integration 220 | 221 | Resty provides [Swagger](http://swagger.io/) integration in default. Swagger JSON is provided at `http://localhost:8080/swagger.json` and also Swagger UI is available at `http://localhost:8080/swagger-ui/`. 222 | 223 | ![Swagger integration](swagger.png) 224 | 225 | Add following parameter to `web.xml` to enable Swagger integration: 226 | 227 | ```xml 228 | 229 | resty.swagger 230 | enable 231 | 232 | ``` 233 | 234 | By enabling [runtime-scaladoc-reader](https://github.com/takezoe/runtime-scaladoc-reader) plugin in your project, Scaladoc of controller classes is reflected to Swagger JSON. For example, Scaladoc of the controller class is used as the tag description, and Scaladoc of the method is used as the operation description, the parameter description and the response description. 235 | 236 | ```scala 237 | addCompilerPlugin("com.github.takezoe" %% "runtime-scaladoc-reader" % "1.0.1") 238 | ``` 239 | 240 | ## Hystrix integration 241 | 242 | Resty also provides [Hystrix](https://github.com/Netflix/Hystrix) integration in default. Metrics are published for each operations. The stream endpoint is available at `http://localhost:8080/hystrix.stream`. Register this endpoint to the Hystrix dashboard. 243 | 244 | ![Hystrix integration](hystrix.png) 245 | 246 | Add following parameter to `web.xml` to enable Hystrix integration: 247 | 248 | ```xml 249 | 250 | resty.hystrix 251 | enable 252 | 253 | ``` 254 | 255 | ## Zipkin integration 256 | 257 | Furthermore, Resty supports [Zipkin](http://zipkin.io/) as well. You can send execution results to the Zipkin server by enabling Zipkin support and using `HttpClientSupport` for calling other APIs. 258 | 259 | Add following parameters to `web.xml` to enable Zipkin integration: 260 | 261 | ```xml 262 | 263 | resty.zipkin 264 | enable 265 | 266 | 267 | resty.zipkin.service.name 268 | resty-sample 269 | 270 | 271 | resty.zipkin.sample.rate 272 | 1.0 273 | 274 | 275 | resty.zipkin.server.url 276 | http://127.0.0.1:9411/api/v1/spans 277 | 278 | ``` 279 | 280 | ## WebJars support 281 | 282 | [WebJars](http://www.webjars.org/) is a cool stuff to integrate frontend libraries with JVM based applications. Resty can host static files that provided by WebJars for frontend applications. 283 | 284 | Add a following parameter to `web.xml` to enable WebJars hosting: 285 | 286 | ```xml 287 | 288 | resty.wabjars 289 | enable 290 | 291 | 292 | resty.wabjars.path 293 | /public/assets/* 294 | 295 | ``` 296 | 297 | You can add WebJars dependencies in your application as following: 298 | 299 | ```scala 300 | libraryDependencies += "org.webjars" % "jquery" % "3.1.1-1" 301 | ``` 302 | 303 | Then import JavaScript library as following: 304 | 305 | ```html 306 | 307 | ``` 308 | 309 | ## Static files hosting 310 | 311 | Resty is including some base servlets to host static files. You can provide a frontend application through Resty application from the classpath or the file system by defining following servlet based on these classes. 312 | 313 | ```scala 314 | // Host static files on the file system 315 | @WebServlet(name="FileResourceServlet", urlPatterns=Array("/public/*")) 316 | class MyFileResourceServlet extends FileResourceServlet("src/main/webapp") 317 | 318 | // Host static files in the classpath 319 | @WebServlet(name="ClasspathResourceServlet", urlPatterns=Array("/public/*")) 320 | class MyClasspathResourceServlet extends ResourceServlet("com/github/resty/sample/public") 321 | ``` 322 | 323 | ## CORS support 324 | 325 | CORS support can be enabled by adding following parameters to `web.xml`: 326 | 327 | ```xml 328 | 329 | resty.cors 330 | enable 331 | 332 | 333 | resty.cors.allowedOrigins 334 | http://localhost:8080 335 | 336 | 337 | resty.cors.allowedMethods 338 | GET, POST 339 | 340 | 341 | resty.cors.allowedHeaders 342 | Content-Type 343 | 344 | 345 | resty.cors.allowCredentials 346 | true 347 | 348 | 349 | resty.cors.preflightMaxAge 350 | 1800 351 | 352 | ``` 353 | 354 | Description about optional parameters: 355 | 356 | - `resty.cors.allowedOrigins`: Comma separated list of hosts and ports which will be allowed to make cross-origin requests (default is `*`). 357 | - `resty.cors.allowedMethods`: Comma separated list of HTTP methods will be allowed (default is `GET, POST, PUT, DELETE`). 358 | - `resty.cors.allowedHeaders`: Comma separated list of allowed HTTP headers (most headers are allowed in default). 359 | - `resty.cors.allowCredentials`: Set this parameter to true to allow cookies in CORS requests (default is `false`). 360 | - `resty.cors.preflightMaxAge`: Number of seconds that preflight request can be cached in the client (default is `0`). 361 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "resty" 2 | 3 | organization := "com.github.takezoe" 4 | 5 | version := "0.0.19-SNAPSHOT" 6 | 7 | scalaVersion := "2.12.8" 8 | 9 | libraryDependencies ++= Seq( 10 | "com.github.takezoe" %% "runtime-scaladoc-reader" % "1.0.1", 11 | "org.json4s" %% "json4s-scalap" % "3.5.4", 12 | "com.netflix.hystrix" % "hystrix-core" % "1.5.12", 13 | "com.netflix.hystrix" % "hystrix-metrics-event-stream" % "1.5.12", 14 | "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.9.5", 15 | "com.squareup.okhttp3" % "okhttp" % "3.10.0", 16 | "io.swagger" % "swagger-models" % "1.5.19", 17 | "io.zipkin.brave" % "brave" % "4.19.2", 18 | "io.zipkin.brave" % "brave-instrumentation-okhttp3" % "4.19.2", 19 | "io.zipkin.brave" % "brave-instrumentation-servlet" % "4.19.2", 20 | "io.zipkin.reporter2" % "zipkin-sender-okhttp3" % "2.6.0", 21 | "commons-io" % "commons-io" % "2.6", 22 | "org.webjars" % "webjars-locator" % "0.34", 23 | "ch.qos.logback" % "logback-classic" % "1.2.3", 24 | "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided", 25 | "org.scalatest" %% "scalatest" % "3.0.5" % "test" 26 | ) 27 | 28 | scalacOptions := Seq("-deprecation") 29 | 30 | publishMavenStyle := true 31 | 32 | publishArtifact in Test := false 33 | 34 | publishTo := { 35 | val nexus = "https://oss.sonatype.org/" 36 | if (version.value.trim.endsWith("SNAPSHOT")) 37 | Some("snapshots" at nexus + "content/repositories/snapshots") 38 | else 39 | Some("releases" at nexus + "service/local/staging/deploy/maven2") 40 | } 41 | 42 | pomIncludeRepository := { _ => false } 43 | 44 | pomExtra := ( 45 | https://github.com/takezoe/resty 46 | 47 | 48 | The Apache Software License, Version 2.0 49 | http://www.apache.org/licenses/LICENSE-2.0.txt 50 | 51 | 52 | 53 | https://github.com/takezoe/resty 54 | scm:git:https://github.com/takezoe/resty.git 55 | 56 | 57 | 58 | takezoe 59 | Naoki Takezoe 60 | takezoe_at_gmail.com 61 | +9 62 | 63 | 64 | ) 65 | -------------------------------------------------------------------------------- /hystrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takezoe/resty/a96025f5742a78fca2ed63ca92019af0b27945c8/hystrix.png -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.2.8 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.1") 2 | -------------------------------------------------------------------------------- /src/main/java/com/github/takezoe/resty/Action.java: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Target({ElementType.METHOD}) 6 | @Retention(RetentionPolicy.RUNTIME) 7 | @Documented 8 | public @interface Action { 9 | 10 | String path(); 11 | 12 | /** 13 | * GET, POST, PUT or DELETE 14 | */ 15 | String method(); 16 | 17 | /** 18 | * Optional 19 | */ 20 | String description() default ""; 21 | 22 | /** 23 | * Optional (default is false) 24 | */ 25 | boolean deprecated() default false; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/takezoe/resty/Controller.java: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Target({ElementType.METHOD}) 6 | @Retention(RetentionPolicy.RUNTIME) 7 | @Documented 8 | public @interface Controller { 9 | 10 | /** 11 | * 12 | */ 13 | String name(); 14 | 15 | /** 16 | * Optional 17 | */ 18 | String description() default ""; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/takezoe/resty/Param.java: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Target({ElementType.PARAMETER}) 6 | @Retention(RetentionPolicy.RUNTIME) 7 | @Documented 8 | public @interface Param { 9 | 10 | /** 11 | * Optional (default is a parameter name) 12 | */ 13 | String name() default ""; 14 | 15 | /** 16 | * query, path, header or body 17 | */ 18 | String from() default ""; 19 | 20 | /** 21 | * Optional 22 | */ 23 | String description() default ""; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | UTF-8 9 | logs/application.log 10 | 11 | logs/application.%d{yyyy-MM-dd}.log 12 | 7 13 | 14 | 15 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/css/reset.css: -------------------------------------------------------------------------------- 1 | a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,output,p,pre,q,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,u,ul,var,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:after,blockquote:before,q:after,q:before{content:'';content:none}table{border-collapse:collapse;border-spacing:0} -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/css/style.css: -------------------------------------------------------------------------------- 1 | .swagger-section #header a#logo{font-size:1.5em;font-weight:700;text-decoration:none;background:transparent url(../images/logo.png) no-repeat 0;padding:20px 0 20px 40px}#text-head{font-size:80px;font-family:Roboto,sans-serif;color:#fff;float:right;margin-right:20%}.navbar-fixed-top .navbar-brand,.navbar-fixed-top .navbar-nav,.navbar-header{height:auto}.navbar-inverse{background-color:#000;border-color:#000}#navbar-brand{margin-left:20%}.navtext{font-size:10px}.h1,h1{font-size:60px}.navbar-default .navbar-header .navbar-brand{color:#a2dfee}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading h2 a{color:#393939;font-family:Arvo,serif;font-size:1.5em}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading h2 a:hover{color:#000}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading h2{color:#525252;padding-left:0;display:block;clear:none;float:left;font-family:Arvo,serif;font-weight:700}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#0a0a0a}.container1{width:1500px;margin:auto;margin-top:0;background-image:url(../images/shield.png);background-repeat:no-repeat;background-position:-40px -20px;margin-bottom:210px}.container-inner{width:1200px;margin:auto;background-color:hsla(192,8%,88%,.75);padding-bottom:40px;padding-top:40px;border-radius:15px}.header-content{padding:0;width:1000px}.title1{font-size:80px;font-family:Vollkorn,serif;color:#404040;text-align:center;padding-top:40px;padding-bottom:100px}#icon{margin-top:-18px}.subtext{font-size:25px;font-style:italic;color:#08b;text-align:right;padding-right:250px}.bg-primary{background-color:#00468b}.navbar-default .nav>li>a,.navbar-default .nav>li>a:focus,.navbar-default .nav>li>a:focus:hover,.navbar-default .nav>li>a:hover{color:#08b}.text-faded{font-size:25px;font-family:Vollkorn,serif}.section-heading{font-family:Vollkorn,serif;font-size:45px;padding-bottom:10px}hr{border-color:#00468b;padding-bottom:10px}.description{margin-top:20px;padding-bottom:200px}.description li{font-family:Vollkorn,serif;font-size:25px;color:#525252;margin-left:28%;padding-top:5px}.gap{margin-top:200px}.troubleshootingtext{color:hsla(0,0%,100%,.7);padding-left:30%}.troubleshootingtext li{list-style-type:circle;font-size:25px;padding-bottom:5px}.overlay{position:absolute;top:0;left:0;width:100%;height:100%;z-index:1}.block.response_body.json:hover{cursor:pointer}.backdrop{color:blue}#myModal{height:100%}.modal-backdrop{bottom:0;position:fixed}.curl{padding:10px;font-family:Anonymous Pro,Menlo,Consolas,Bitstream Vera Sans Mono,Courier New,monospace;font-size:.9em;max-height:400px;margin-top:5px;overflow-y:auto;background-color:#fcf6db;border:1px solid #e5e0c6;border-radius:4px}.curl_title{font-size:1.1em;margin:0;padding:15px 0 5px;font-family:Open Sans,Helvetica Neue,Arial,sans-serif;font-weight:500;line-height:1.1}.footer{display:none}.swagger-section .swagger-ui-wrap h2{padding:0}h2{margin:0;margin-bottom:5px}.markdown p,.swagger-section .swagger-ui-wrap .code{font-size:15px;font-family:Arvo,serif}.swagger-section .swagger-ui-wrap b{font-family:Arvo,serif}#signin:hover{cursor:pointer}.dropdown-menu{padding:15px}.navbar-right .dropdown-menu{left:0;right:auto}#signinbutton{width:100%;height:32px;font-size:13px;font-weight:700;color:#08b}.navbar-default .nav>li .details{color:#000;text-transform:none;font-size:15px;font-weight:400;font-family:Open Sans,sans-serif;font-style:italic;line-height:20px;top:-2px}.navbar-default .nav>li .details:hover{color:#000}#signout{width:100%;height:32px;font-size:13px;font-weight:700;color:#08b} -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/css/typography.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takezoe/resty/a96025f5742a78fca2ed63ca92019af0b27945c8/src/main/resources/public/vendors/swagger-ui/css/typography.css -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/fonts/DroidSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takezoe/resty/a96025f5742a78fca2ed63ca92019af0b27945c8/src/main/resources/public/vendors/swagger-ui/fonts/DroidSans-Bold.ttf -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/fonts/DroidSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takezoe/resty/a96025f5742a78fca2ed63ca92019af0b27945c8/src/main/resources/public/vendors/swagger-ui/fonts/DroidSans.ttf -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/images/collapse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takezoe/resty/a96025f5742a78fca2ed63ca92019af0b27945c8/src/main/resources/public/vendors/swagger-ui/images/collapse.gif -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/images/expand.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takezoe/resty/a96025f5742a78fca2ed63ca92019af0b27945c8/src/main/resources/public/vendors/swagger-ui/images/expand.gif -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/images/explorer_icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takezoe/resty/a96025f5742a78fca2ed63ca92019af0b27945c8/src/main/resources/public/vendors/swagger-ui/images/explorer_icons.png -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takezoe/resty/a96025f5742a78fca2ed63ca92019af0b27945c8/src/main/resources/public/vendors/swagger-ui/images/favicon-16x16.png -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takezoe/resty/a96025f5742a78fca2ed63ca92019af0b27945c8/src/main/resources/public/vendors/swagger-ui/images/favicon-32x32.png -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takezoe/resty/a96025f5742a78fca2ed63ca92019af0b27945c8/src/main/resources/public/vendors/swagger-ui/images/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/images/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takezoe/resty/a96025f5742a78fca2ed63ca92019af0b27945c8/src/main/resources/public/vendors/swagger-ui/images/logo_small.png -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/images/pet_store_api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takezoe/resty/a96025f5742a78fca2ed63ca92019af0b27945c8/src/main/resources/public/vendors/swagger-ui/images/pet_store_api.png -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/images/throbber.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takezoe/resty/a96025f5742a78fca2ed63ca92019af0b27945c8/src/main/resources/public/vendors/swagger-ui/images/throbber.gif -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/images/wordnik_api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takezoe/resty/a96025f5742a78fca2ed63ca92019af0b27945c8/src/main/resources/public/vendors/swagger-ui/images/wordnik_api.png -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Swagger UI 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 89 | 90 | 91 | 92 | 102 | 103 |
 
104 |
105 | 106 | 107 | -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/lang/ca.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Advertència: Obsolet", 6 | "Implementation Notes":"Notes d'implementació", 7 | "Response Class":"Classe de la Resposta", 8 | "Status":"Estatus", 9 | "Parameters":"Paràmetres", 10 | "Parameter":"Paràmetre", 11 | "Value":"Valor", 12 | "Description":"Descripció", 13 | "Parameter Type":"Tipus del Paràmetre", 14 | "Data Type":"Tipus de la Dada", 15 | "Response Messages":"Missatges de la Resposta", 16 | "HTTP Status Code":"Codi d'Estatus HTTP", 17 | "Reason":"Raó", 18 | "Response Model":"Model de la Resposta", 19 | "Request URL":"URL de la Sol·licitud", 20 | "Response Body":"Cos de la Resposta", 21 | "Response Code":"Codi de la Resposta", 22 | "Response Headers":"Capçaleres de la Resposta", 23 | "Hide Response":"Amagar Resposta", 24 | "Try it out!":"Prova-ho!", 25 | "Show/Hide":"Mostrar/Amagar", 26 | "List Operations":"Llista Operacions", 27 | "Expand Operations":"Expandir Operacions", 28 | "Raw":"Cru", 29 | "can't parse JSON. Raw result":"no puc analitzar el JSON. Resultat cru", 30 | "Example Value":"Valor d'Exemple", 31 | "Model Schema":"Esquema del Model", 32 | "Model":"Model", 33 | "apply":"aplicar", 34 | "Username":"Nom d'usuari", 35 | "Password":"Contrasenya", 36 | "Terms of service":"Termes del servei", 37 | "Created by":"Creat per", 38 | "See more at":"Veure més en", 39 | "Contact the developer":"Contactar amb el desenvolupador", 40 | "api version":"versió de la api", 41 | "Response Content Type":"Tipus de Contingut de la Resposta", 42 | "fetching resource":"recollint recurs", 43 | "fetching resource list":"recollins llista de recursos", 44 | "Explore":"Explorant", 45 | "Show Swagger Petstore Example Apis":"Mostrar API d'Exemple Swagger Petstore", 46 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"No es pot llegir del servidor. Potser no teniu la configuració de control d'accés apropiada.", 47 | "Please specify the protocol for":"Si us plau, especifiqueu el protocol per a", 48 | "Can't read swagger JSON from":"No es pot llegir el JSON de swagger des de", 49 | "Finished Loading Resource Information. Rendering Swagger UI":"Finalitzada la càrrega del recurs informatiu. Renderitzant Swagger UI", 50 | "Unable to read api":"No es pot llegir l'api", 51 | "from path":"des de la ruta", 52 | "server returned":"el servidor ha retornat" 53 | }); 54 | -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/lang/el.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Προειδοποίηση: Έχει αποσυρθεί", 6 | "Implementation Notes":"Σημειώσεις Υλοποίησης", 7 | "Response Class":"Απόκριση", 8 | "Status":"Κατάσταση", 9 | "Parameters":"Παράμετροι", 10 | "Parameter":"Παράμετρος", 11 | "Value":"Τιμή", 12 | "Description":"Περιγραφή", 13 | "Parameter Type":"Τύπος Παραμέτρου", 14 | "Data Type":"Τύπος Δεδομένων", 15 | "Response Messages":"Μηνύματα Απόκρισης", 16 | "HTTP Status Code":"Κωδικός Κατάστασης HTTP", 17 | "Reason":"Αιτιολογία", 18 | "Response Model":"Μοντέλο Απόκρισης", 19 | "Request URL":"URL Αιτήματος", 20 | "Response Body":"Σώμα Απόκρισης", 21 | "Response Code":"Κωδικός Απόκρισης", 22 | "Response Headers":"Επικεφαλίδες Απόκρισης", 23 | "Hide Response":"Απόκρυψη Απόκρισης", 24 | "Headers":"Επικεφαλίδες", 25 | "Try it out!":"Δοκιμάστε το!", 26 | "Show/Hide":"Εμφάνιση/Απόκρυψη", 27 | "List Operations":"Λίστα Λειτουργιών", 28 | "Expand Operations":"Ανάπτυξη Λειτουργιών", 29 | "Raw":"Ακατέργαστο", 30 | "can't parse JSON. Raw result":"αδυναμία ανάλυσης JSON. Ακατέργαστο αποτέλεσμα", 31 | "Example Value":"Παράδειγμα Τιμής", 32 | "Model Schema":"Σχήμα Μοντέλου", 33 | "Model":"Μοντέλο", 34 | "Click to set as parameter value":"Πατήστε για να θέσετε τιμή παραμέτρου", 35 | "apply":"εφαρμογή", 36 | "Username":"Όνομα χρήση", 37 | "Password":"Κωδικός πρόσβασης", 38 | "Terms of service":"Όροι χρήσης", 39 | "Created by":"Δημιουργήθηκε από", 40 | "See more at":"Δείτε περισσότερα στο", 41 | "Contact the developer":"Επικοινωνήστε με τον προγραμματιστή", 42 | "api version":"έκδοση api", 43 | "Response Content Type":"Τύπος Περιεχομένου Απόκρισης", 44 | "Parameter content type:":"Τύπος περιεχομένου παραμέτρου:", 45 | "fetching resource":"παραλαβή πόρου", 46 | "fetching resource list":"παραλαβή λίστας πόρων", 47 | "Explore":"Εξερεύνηση", 48 | "Show Swagger Petstore Example Apis":"Εμφάνιση Api Δειγμάτων Petstore του Swagger", 49 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Αδυναμία ανάγνωσης από τον εξυπηρετητή. Μπορεί να μην έχει κατάλληλες ρυθμίσεις για access-control-origin.", 50 | "Please specify the protocol for":"Παρακαλώ προσδιορίστε το πρωτόκολλο για", 51 | "Can't read swagger JSON from":"Αδυναμία ανάγνωσης swagger JSON από", 52 | "Finished Loading Resource Information. Rendering Swagger UI":"Ολοκλήρωση Φόρτωσης Πληροφορικών Πόρου. Παρουσίαση Swagger UI", 53 | "Unable to read api":"Αδυναμία ανάγνωσης api", 54 | "from path":"από το μονοπάτι", 55 | "server returned":"ο εξυπηρετηρής επέστρεψε" 56 | }); 57 | -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/lang/en.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Warning: Deprecated", 6 | "Implementation Notes":"Implementation Notes", 7 | "Response Class":"Response Class", 8 | "Status":"Status", 9 | "Parameters":"Parameters", 10 | "Parameter":"Parameter", 11 | "Value":"Value", 12 | "Description":"Description", 13 | "Parameter Type":"Parameter Type", 14 | "Data Type":"Data Type", 15 | "Response Messages":"Response Messages", 16 | "HTTP Status Code":"HTTP Status Code", 17 | "Reason":"Reason", 18 | "Response Model":"Response Model", 19 | "Request URL":"Request URL", 20 | "Response Body":"Response Body", 21 | "Response Code":"Response Code", 22 | "Response Headers":"Response Headers", 23 | "Hide Response":"Hide Response", 24 | "Headers":"Headers", 25 | "Try it out!":"Try it out!", 26 | "Show/Hide":"Show/Hide", 27 | "List Operations":"List Operations", 28 | "Expand Operations":"Expand Operations", 29 | "Raw":"Raw", 30 | "can't parse JSON. Raw result":"can't parse JSON. Raw result", 31 | "Example Value":"Example Value", 32 | "Model Schema":"Model Schema", 33 | "Model":"Model", 34 | "Click to set as parameter value":"Click to set as parameter value", 35 | "apply":"apply", 36 | "Username":"Username", 37 | "Password":"Password", 38 | "Terms of service":"Terms of service", 39 | "Created by":"Created by", 40 | "See more at":"See more at", 41 | "Contact the developer":"Contact the developer", 42 | "api version":"api version", 43 | "Response Content Type":"Response Content Type", 44 | "Parameter content type:":"Parameter content type:", 45 | "fetching resource":"fetching resource", 46 | "fetching resource list":"fetching resource list", 47 | "Explore":"Explore", 48 | "Show Swagger Petstore Example Apis":"Show Swagger Petstore Example Apis", 49 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Can't read from server. It may not have the appropriate access-control-origin settings.", 50 | "Please specify the protocol for":"Please specify the protocol for", 51 | "Can't read swagger JSON from":"Can't read swagger JSON from", 52 | "Finished Loading Resource Information. Rendering Swagger UI":"Finished Loading Resource Information. Rendering Swagger UI", 53 | "Unable to read api":"Unable to read api", 54 | "from path":"from path", 55 | "server returned":"server returned" 56 | }); 57 | -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/lang/es.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Advertencia: Obsoleto", 6 | "Implementation Notes":"Notas de implementación", 7 | "Response Class":"Clase de la Respuesta", 8 | "Status":"Status", 9 | "Parameters":"Parámetros", 10 | "Parameter":"Parámetro", 11 | "Value":"Valor", 12 | "Description":"Descripción", 13 | "Parameter Type":"Tipo del Parámetro", 14 | "Data Type":"Tipo del Dato", 15 | "Response Messages":"Mensajes de la Respuesta", 16 | "HTTP Status Code":"Código de Status HTTP", 17 | "Reason":"Razón", 18 | "Response Model":"Modelo de la Respuesta", 19 | "Request URL":"URL de la Solicitud", 20 | "Response Body":"Cuerpo de la Respuesta", 21 | "Response Code":"Código de la Respuesta", 22 | "Response Headers":"Encabezados de la Respuesta", 23 | "Hide Response":"Ocultar Respuesta", 24 | "Try it out!":"Pruébalo!", 25 | "Show/Hide":"Mostrar/Ocultar", 26 | "List Operations":"Listar Operaciones", 27 | "Expand Operations":"Expandir Operaciones", 28 | "Raw":"Crudo", 29 | "can't parse JSON. Raw result":"no puede parsear el JSON. Resultado crudo", 30 | "Example Value":"Valor de Ejemplo", 31 | "Model Schema":"Esquema del Modelo", 32 | "Model":"Modelo", 33 | "apply":"aplicar", 34 | "Username":"Nombre de usuario", 35 | "Password":"Contraseña", 36 | "Terms of service":"Términos de Servicio", 37 | "Created by":"Creado por", 38 | "See more at":"Ver más en", 39 | "Contact the developer":"Contactar al desarrollador", 40 | "api version":"versión de la api", 41 | "Response Content Type":"Tipo de Contenido (Content Type) de la Respuesta", 42 | "fetching resource":"buscando recurso", 43 | "fetching resource list":"buscando lista del recurso", 44 | "Explore":"Explorar", 45 | "Show Swagger Petstore Example Apis":"Mostrar Api Ejemplo de Swagger Petstore", 46 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"No se puede leer del servidor. Tal vez no tiene la configuración de control de acceso de origen (access-control-origin) apropiado.", 47 | "Please specify the protocol for":"Por favor, especificar el protocola para", 48 | "Can't read swagger JSON from":"No se puede leer el JSON de swagger desde", 49 | "Finished Loading Resource Information. Rendering Swagger UI":"Finalizada la carga del recurso de Información. Mostrando Swagger UI", 50 | "Unable to read api":"No se puede leer la api", 51 | "from path":"desde ruta", 52 | "server returned":"el servidor retornó" 53 | }); 54 | -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/lang/fr.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Avertissement : Obsolète", 6 | "Implementation Notes":"Notes d'implémentation", 7 | "Response Class":"Classe de la réponse", 8 | "Status":"Statut", 9 | "Parameters":"Paramètres", 10 | "Parameter":"Paramètre", 11 | "Value":"Valeur", 12 | "Description":"Description", 13 | "Parameter Type":"Type du paramètre", 14 | "Data Type":"Type de données", 15 | "Response Messages":"Messages de la réponse", 16 | "HTTP Status Code":"Code de statut HTTP", 17 | "Reason":"Raison", 18 | "Response Model":"Modèle de réponse", 19 | "Request URL":"URL appelée", 20 | "Response Body":"Corps de la réponse", 21 | "Response Code":"Code de la réponse", 22 | "Response Headers":"En-têtes de la réponse", 23 | "Hide Response":"Cacher la réponse", 24 | "Headers":"En-têtes", 25 | "Try it out!":"Testez !", 26 | "Show/Hide":"Afficher/Masquer", 27 | "List Operations":"Liste des opérations", 28 | "Expand Operations":"Développer les opérations", 29 | "Raw":"Brut", 30 | "can't parse JSON. Raw result":"impossible de décoder le JSON. Résultat brut", 31 | "Example Value":"Exemple la valeur", 32 | "Model Schema":"Définition du modèle", 33 | "Model":"Modèle", 34 | "apply":"appliquer", 35 | "Username":"Nom d'utilisateur", 36 | "Password":"Mot de passe", 37 | "Terms of service":"Conditions de service", 38 | "Created by":"Créé par", 39 | "See more at":"Voir plus sur", 40 | "Contact the developer":"Contacter le développeur", 41 | "api version":"version de l'api", 42 | "Response Content Type":"Content Type de la réponse", 43 | "fetching resource":"récupération de la ressource", 44 | "fetching resource list":"récupération de la liste de ressources", 45 | "Explore":"Explorer", 46 | "Show Swagger Petstore Example Apis":"Montrer les Apis de l'exemple Petstore de Swagger", 47 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Impossible de lire à partir du serveur. Il se peut que les réglages access-control-origin ne soient pas appropriés.", 48 | "Please specify the protocol for":"Veuillez spécifier un protocole pour", 49 | "Can't read swagger JSON from":"Impossible de lire le JSON swagger à partir de", 50 | "Finished Loading Resource Information. Rendering Swagger UI":"Chargement des informations terminé. Affichage de Swagger UI", 51 | "Unable to read api":"Impossible de lire l'api", 52 | "from path":"à partir du chemin", 53 | "server returned":"réponse du serveur" 54 | }); 55 | -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/lang/geo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"ყურადღება: აღარ გამოიყენება", 6 | "Implementation Notes":"იმპლემენტაციის აღწერა", 7 | "Response Class":"რესპონს კლასი", 8 | "Status":"სტატუსი", 9 | "Parameters":"პარამეტრები", 10 | "Parameter":"პარამეტრი", 11 | "Value":"მნიშვნელობა", 12 | "Description":"აღწერა", 13 | "Parameter Type":"პარამეტრის ტიპი", 14 | "Data Type":"მონაცემის ტიპი", 15 | "Response Messages":"პასუხი", 16 | "HTTP Status Code":"HTTP სტატუსი", 17 | "Reason":"მიზეზი", 18 | "Response Model":"რესპონს მოდელი", 19 | "Request URL":"მოთხოვნის URL", 20 | "Response Body":"პასუხის სხეული", 21 | "Response Code":"პასუხის კოდი", 22 | "Response Headers":"პასუხის ჰედერები", 23 | "Hide Response":"დამალე პასუხი", 24 | "Headers":"ჰედერები", 25 | "Try it out!":"ცადე !", 26 | "Show/Hide":"გამოჩენა/დამალვა", 27 | "List Operations":"ოპერაციების სია", 28 | "Expand Operations":"ოპერაციები ვრცლად", 29 | "Raw":"ნედლი", 30 | "can't parse JSON. Raw result":"JSON-ის დამუშავება ვერ მოხერხდა. ნედლი პასუხი", 31 | "Example Value":"მაგალითი", 32 | "Model Schema":"მოდელის სტრუქტურა", 33 | "Model":"მოდელი", 34 | "Click to set as parameter value":"პარამეტრისთვის მნიშვნელობის მისანიჭებლად, დააკლიკე", 35 | "apply":"გამოყენება", 36 | "Username":"მოხმარებელი", 37 | "Password":"პაროლი", 38 | "Terms of service":"მომსახურების პირობები", 39 | "Created by":"შექმნა", 40 | "See more at":"ნახე ვრცლად", 41 | "Contact the developer":"დაუკავშირდი დეველოპერს", 42 | "api version":"api ვერსია", 43 | "Response Content Type":"პასუხის კონტენტის ტიპი", 44 | "Parameter content type:":"პარამეტრის კონტენტის ტიპი:", 45 | "fetching resource":"რესურსების მიღება", 46 | "fetching resource list":"რესურსების სიის მიღება", 47 | "Explore":"ნახვა", 48 | "Show Swagger Petstore Example Apis":"ნახე Swagger Petstore სამაგალითო Api", 49 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"სერვერთან დაკავშირება ვერ ხერხდება. შეამოწმეთ access-control-origin.", 50 | "Please specify the protocol for":"მიუთითეთ პროტოკოლი", 51 | "Can't read swagger JSON from":"swagger JSON წაკითხვა ვერ მოხერხდა", 52 | "Finished Loading Resource Information. Rendering Swagger UI":"რესურსების ჩატვირთვა სრულდება. Swagger UI რენდერდება", 53 | "Unable to read api":"api წაკითხვა ვერ მოხერხდა", 54 | "from path":"მისამართიდან", 55 | "server returned":"სერვერმა დააბრუნა" 56 | }); 57 | -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/lang/it.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Attenzione: Deprecato", 6 | "Implementation Notes":"Note di implementazione", 7 | "Response Class":"Classe della risposta", 8 | "Status":"Stato", 9 | "Parameters":"Parametri", 10 | "Parameter":"Parametro", 11 | "Value":"Valore", 12 | "Description":"Descrizione", 13 | "Parameter Type":"Tipo di parametro", 14 | "Data Type":"Tipo di dato", 15 | "Response Messages":"Messaggi della risposta", 16 | "HTTP Status Code":"Codice stato HTTP", 17 | "Reason":"Motivo", 18 | "Response Model":"Modello di risposta", 19 | "Request URL":"URL della richiesta", 20 | "Response Body":"Corpo della risposta", 21 | "Response Code":"Oggetto della risposta", 22 | "Response Headers":"Intestazioni della risposta", 23 | "Hide Response":"Nascondi risposta", 24 | "Try it out!":"Provalo!", 25 | "Show/Hide":"Mostra/Nascondi", 26 | "List Operations":"Mostra operazioni", 27 | "Expand Operations":"Espandi operazioni", 28 | "Raw":"Grezzo (raw)", 29 | "can't parse JSON. Raw result":"non è possibile parsare il JSON. Risultato grezzo (raw).", 30 | "Model Schema":"Schema del modello", 31 | "Model":"Modello", 32 | "apply":"applica", 33 | "Username":"Nome utente", 34 | "Password":"Password", 35 | "Terms of service":"Condizioni del servizio", 36 | "Created by":"Creato da", 37 | "See more at":"Informazioni aggiuntive:", 38 | "Contact the developer":"Contatta lo sviluppatore", 39 | "api version":"versione api", 40 | "Response Content Type":"Tipo di contenuto (content type) della risposta", 41 | "fetching resource":"recuperando la risorsa", 42 | "fetching resource list":"recuperando lista risorse", 43 | "Explore":"Esplora", 44 | "Show Swagger Petstore Example Apis":"Mostra le api di esempio di Swagger Petstore", 45 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Non è possibile leggere dal server. Potrebbe non avere le impostazioni di controllo accesso origine (access-control-origin) appropriate.", 46 | "Please specify the protocol for":"Si prega di specificare il protocollo per", 47 | "Can't read swagger JSON from":"Impossibile leggere JSON swagger da:", 48 | "Finished Loading Resource Information. Rendering Swagger UI":"Lettura informazioni risorse termianta. Swagger UI viene mostrata", 49 | "Unable to read api":"Impossibile leggere la api", 50 | "from path":"da cartella", 51 | "server returned":"il server ha restituito" 52 | }); 53 | -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/lang/ja.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"警告: 廃止予定", 6 | "Implementation Notes":"実装メモ", 7 | "Response Class":"レスポンスクラス", 8 | "Status":"ステータス", 9 | "Parameters":"パラメータ群", 10 | "Parameter":"パラメータ", 11 | "Value":"値", 12 | "Description":"説明", 13 | "Parameter Type":"パラメータタイプ", 14 | "Data Type":"データタイプ", 15 | "Response Messages":"レスポンスメッセージ", 16 | "HTTP Status Code":"HTTPステータスコード", 17 | "Reason":"理由", 18 | "Response Model":"レスポンスモデル", 19 | "Request URL":"リクエストURL", 20 | "Response Body":"レスポンスボディ", 21 | "Response Code":"レスポンスコード", 22 | "Response Headers":"レスポンスヘッダ", 23 | "Hide Response":"レスポンスを隠す", 24 | "Headers":"ヘッダ", 25 | "Try it out!":"実際に実行!", 26 | "Show/Hide":"表示/非表示", 27 | "List Operations":"操作一覧", 28 | "Expand Operations":"操作の展開", 29 | "Raw":"未加工", 30 | "can't parse JSON. Raw result":"JSONへ解釈できません. 未加工の結果", 31 | "Example Value":"値の例", 32 | "Model Schema":"モデルスキーマ", 33 | "Model":"モデル", 34 | "Click to set as parameter value":"パラメータ値と設定するにはクリック", 35 | "apply":"実行", 36 | "Username":"ユーザ名", 37 | "Password":"パスワード", 38 | "Terms of service":"サービス利用規約", 39 | "Created by":"Created by", 40 | "See more at":"詳細を見る", 41 | "Contact the developer":"開発者に連絡", 42 | "api version":"APIバージョン", 43 | "Response Content Type":"レスポンス コンテンツタイプ", 44 | "Parameter content type:":"パラメータコンテンツタイプ:", 45 | "fetching resource":"リソースの取得", 46 | "fetching resource list":"リソース一覧の取得", 47 | "Explore":"調査", 48 | "Show Swagger Petstore Example Apis":"SwaggerペットストアAPIの表示", 49 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"サーバから読み込めません. 適切なaccess-control-origin設定を持っていない可能性があります.", 50 | "Please specify the protocol for":"プロトコルを指定してください", 51 | "Can't read swagger JSON from":"次からswagger JSONを読み込めません", 52 | "Finished Loading Resource Information. Rendering Swagger UI":"リソース情報の読み込みが完了しました. Swagger UIを描画しています", 53 | "Unable to read api":"APIを読み込めません", 54 | "from path":"次のパスから", 55 | "server returned":"サーバからの返答" 56 | }); 57 | -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/lang/ko-kr.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"경고:폐기예정됨", 6 | "Implementation Notes":"구현 노트", 7 | "Response Class":"응답 클래스", 8 | "Status":"상태", 9 | "Parameters":"매개변수들", 10 | "Parameter":"매개변수", 11 | "Value":"값", 12 | "Description":"설명", 13 | "Parameter Type":"매개변수 타입", 14 | "Data Type":"데이터 타입", 15 | "Response Messages":"응답 메세지", 16 | "HTTP Status Code":"HTTP 상태 코드", 17 | "Reason":"원인", 18 | "Response Model":"응답 모델", 19 | "Request URL":"요청 URL", 20 | "Response Body":"응답 본문", 21 | "Response Code":"응답 코드", 22 | "Response Headers":"응답 헤더", 23 | "Hide Response":"응답 숨기기", 24 | "Headers":"헤더", 25 | "Try it out!":"써보기!", 26 | "Show/Hide":"보이기/숨기기", 27 | "List Operations":"목록 작업", 28 | "Expand Operations":"전개 작업", 29 | "Raw":"원본", 30 | "can't parse JSON. Raw result":"JSON을 파싱할수 없음. 원본결과:", 31 | "Model Schema":"모델 스키마", 32 | "Model":"모델", 33 | "apply":"적용", 34 | "Username":"사용자 이름", 35 | "Password":"암호", 36 | "Terms of service":"이용약관", 37 | "Created by":"작성자", 38 | "See more at":"추가정보:", 39 | "Contact the developer":"개발자에게 문의", 40 | "api version":"api버전", 41 | "Response Content Type":"응답Content Type", 42 | "fetching resource":"리소스 가져오기", 43 | "fetching resource list":"리소스 목록 가져오기", 44 | "Explore":"탐색", 45 | "Show Swagger Petstore Example Apis":"Swagger Petstore 예제 보기", 46 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"서버로부터 읽어들일수 없습니다. access-control-origin 설정이 올바르지 않을수 있습니다.", 47 | "Please specify the protocol for":"다음을 위한 프로토콜을 정하세요", 48 | "Can't read swagger JSON from":"swagger JSON 을 다음으로 부터 읽을수 없습니다", 49 | "Finished Loading Resource Information. Rendering Swagger UI":"리소스 정보 불러오기 완료. Swagger UI 랜더링", 50 | "Unable to read api":"api를 읽을 수 없습니다.", 51 | "from path":"다음 경로로 부터", 52 | "server returned":"서버 응답함." 53 | }); 54 | -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/lang/pl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Uwaga: Wycofane", 6 | "Implementation Notes":"Uwagi Implementacji", 7 | "Response Class":"Klasa Odpowiedzi", 8 | "Status":"Status", 9 | "Parameters":"Parametry", 10 | "Parameter":"Parametr", 11 | "Value":"Wartość", 12 | "Description":"Opis", 13 | "Parameter Type":"Typ Parametru", 14 | "Data Type":"Typ Danych", 15 | "Response Messages":"Wiadomości Odpowiedzi", 16 | "HTTP Status Code":"Kod Statusu HTTP", 17 | "Reason":"Przyczyna", 18 | "Response Model":"Model Odpowiedzi", 19 | "Request URL":"URL Wywołania", 20 | "Response Body":"Treść Odpowiedzi", 21 | "Response Code":"Kod Odpowiedzi", 22 | "Response Headers":"Nagłówki Odpowiedzi", 23 | "Hide Response":"Ukryj Odpowiedź", 24 | "Headers":"Nagłówki", 25 | "Try it out!":"Wypróbuj!", 26 | "Show/Hide":"Pokaż/Ukryj", 27 | "List Operations":"Lista Operacji", 28 | "Expand Operations":"Rozwiń Operacje", 29 | "Raw":"Nieprzetworzone", 30 | "can't parse JSON. Raw result":"nie można przetworzyć pliku JSON. Nieprzetworzone dane", 31 | "Model Schema":"Schemat Modelu", 32 | "Model":"Model", 33 | "apply":"użyj", 34 | "Username":"Nazwa użytkownika", 35 | "Password":"Hasło", 36 | "Terms of service":"Warunki używania", 37 | "Created by":"Utworzone przez", 38 | "See more at":"Zobacz więcej na", 39 | "Contact the developer":"Kontakt z deweloperem", 40 | "api version":"wersja api", 41 | "Response Content Type":"Typ Zasobu Odpowiedzi", 42 | "fetching resource":"ładowanie zasobu", 43 | "fetching resource list":"ładowanie listy zasobów", 44 | "Explore":"Eksploruj", 45 | "Show Swagger Petstore Example Apis":"Pokaż Przykładowe Api Swagger Petstore", 46 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Brak połączenia z serwerem. Może on nie mieć odpowiednich ustawień access-control-origin.", 47 | "Please specify the protocol for":"Proszę podać protokół dla", 48 | "Can't read swagger JSON from":"Nie można odczytać swagger JSON z", 49 | "Finished Loading Resource Information. Rendering Swagger UI":"Ukończono Ładowanie Informacji o Zasobie. Renderowanie Swagger UI", 50 | "Unable to read api":"Nie można odczytać api", 51 | "from path":"ze ścieżki", 52 | "server returned":"serwer zwrócił" 53 | }); 54 | -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/lang/pt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Aviso: Depreciado", 6 | "Implementation Notes":"Notas de Implementação", 7 | "Response Class":"Classe de resposta", 8 | "Status":"Status", 9 | "Parameters":"Parâmetros", 10 | "Parameter":"Parâmetro", 11 | "Value":"Valor", 12 | "Description":"Descrição", 13 | "Parameter Type":"Tipo de parâmetro", 14 | "Data Type":"Tipo de dados", 15 | "Response Messages":"Mensagens de resposta", 16 | "HTTP Status Code":"Código de status HTTP", 17 | "Reason":"Razão", 18 | "Response Model":"Modelo resposta", 19 | "Request URL":"URL requisição", 20 | "Response Body":"Corpo da resposta", 21 | "Response Code":"Código da resposta", 22 | "Response Headers":"Cabeçalho da resposta", 23 | "Headers":"Cabeçalhos", 24 | "Hide Response":"Esconder resposta", 25 | "Try it out!":"Tente agora!", 26 | "Show/Hide":"Mostrar/Esconder", 27 | "List Operations":"Listar operações", 28 | "Expand Operations":"Expandir operações", 29 | "Raw":"Cru", 30 | "can't parse JSON. Raw result":"Falha ao analisar JSON. Resulto cru", 31 | "Model Schema":"Modelo esquema", 32 | "Model":"Modelo", 33 | "apply":"Aplicar", 34 | "Username":"Usuário", 35 | "Password":"Senha", 36 | "Terms of service":"Termos do serviço", 37 | "Created by":"Criado por", 38 | "See more at":"Veja mais em", 39 | "Contact the developer":"Contate o desenvolvedor", 40 | "api version":"Versão api", 41 | "Response Content Type":"Tipo de conteúdo da resposta", 42 | "fetching resource":"busca recurso", 43 | "fetching resource list":"buscando lista de recursos", 44 | "Explore":"Explorar", 45 | "Show Swagger Petstore Example Apis":"Show Swagger Petstore Example Apis", 46 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Não é possível ler do servidor. Pode não ter as apropriadas configurações access-control-origin", 47 | "Please specify the protocol for":"Por favor especifique o protocolo", 48 | "Can't read swagger JSON from":"Não é possível ler o JSON Swagger de", 49 | "Finished Loading Resource Information. Rendering Swagger UI":"Carregar informação de recurso finalizada. Renderizando Swagger UI", 50 | "Unable to read api":"Não foi possível ler api", 51 | "from path":"do caminho", 52 | "server returned":"servidor retornou" 53 | }); 54 | -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/lang/ru.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Предупреждение: Устарело", 6 | "Implementation Notes":"Заметки", 7 | "Response Class":"Пример ответа", 8 | "Status":"Статус", 9 | "Parameters":"Параметры", 10 | "Parameter":"Параметр", 11 | "Value":"Значение", 12 | "Description":"Описание", 13 | "Parameter Type":"Тип параметра", 14 | "Data Type":"Тип данных", 15 | "HTTP Status Code":"HTTP код", 16 | "Reason":"Причина", 17 | "Response Model":"Структура ответа", 18 | "Request URL":"URL запроса", 19 | "Response Body":"Тело ответа", 20 | "Response Code":"HTTP код ответа", 21 | "Response Headers":"Заголовки ответа", 22 | "Hide Response":"Спрятать ответ", 23 | "Headers":"Заголовки", 24 | "Response Messages":"Что может прийти в ответ", 25 | "Try it out!":"Попробовать!", 26 | "Show/Hide":"Показать/Скрыть", 27 | "List Operations":"Операции кратко", 28 | "Expand Operations":"Операции подробно", 29 | "Raw":"В сыром виде", 30 | "can't parse JSON. Raw result":"Не удается распарсить ответ:", 31 | "Example Value":"Пример", 32 | "Model Schema":"Структура", 33 | "Model":"Описание", 34 | "Click to set as parameter value":"Нажмите, чтобы испльзовать в качестве значения параметра", 35 | "apply":"применить", 36 | "Username":"Имя пользователя", 37 | "Password":"Пароль", 38 | "Terms of service":"Условия использования", 39 | "Created by":"Разработано", 40 | "See more at":"Еще тут", 41 | "Contact the developer":"Связаться с разработчиком", 42 | "api version":"Версия API", 43 | "Response Content Type":"Content Type ответа", 44 | "Parameter content type:":"Content Type параметра:", 45 | "fetching resource":"Получение ресурса", 46 | "fetching resource list":"Получение ресурсов", 47 | "Explore":"Показать", 48 | "Show Swagger Petstore Example Apis":"Показать примеры АПИ", 49 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Не удается получить ответ от сервера. Возможно, проблема с настройками доступа", 50 | "Please specify the protocol for":"Пожалуйста, укажите протокол для", 51 | "Can't read swagger JSON from":"Не получается прочитать swagger json из", 52 | "Finished Loading Resource Information. Rendering Swagger UI":"Загрузка информации о ресурсах завершена. Рендерим", 53 | "Unable to read api":"Не удалось прочитать api", 54 | "from path":"по адресу", 55 | "server returned":"сервер сказал" 56 | }); 57 | -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/lang/tr.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Uyarı: Deprecated", 6 | "Implementation Notes":"Gerçekleştirim Notları", 7 | "Response Class":"Dönen Sınıf", 8 | "Status":"Statü", 9 | "Parameters":"Parametreler", 10 | "Parameter":"Parametre", 11 | "Value":"Değer", 12 | "Description":"Açıklama", 13 | "Parameter Type":"Parametre Tipi", 14 | "Data Type":"Veri Tipi", 15 | "Response Messages":"Dönüş Mesajı", 16 | "HTTP Status Code":"HTTP Statü Kodu", 17 | "Reason":"Gerekçe", 18 | "Response Model":"Dönüş Modeli", 19 | "Request URL":"İstek URL", 20 | "Response Body":"Dönüş İçeriği", 21 | "Response Code":"Dönüş Kodu", 22 | "Response Headers":"Dönüş Üst Bilgileri", 23 | "Hide Response":"Dönüşü Gizle", 24 | "Headers":"Üst Bilgiler", 25 | "Try it out!":"Dene!", 26 | "Show/Hide":"Göster/Gizle", 27 | "List Operations":"Operasyonları Listele", 28 | "Expand Operations":"Operasyonları Aç", 29 | "Raw":"Ham", 30 | "can't parse JSON. Raw result":"JSON çözümlenemiyor. Ham sonuç", 31 | "Model Schema":"Model Şema", 32 | "Model":"Model", 33 | "apply":"uygula", 34 | "Username":"Kullanıcı Adı", 35 | "Password":"Parola", 36 | "Terms of service":"Servis şartları", 37 | "Created by":"Oluşturan", 38 | "See more at":"Daha fazlası için", 39 | "Contact the developer":"Geliştirici ile İletişime Geçin", 40 | "api version":"api versiyon", 41 | "Response Content Type":"Dönüş İçerik Tipi", 42 | "fetching resource":"kaynak getiriliyor", 43 | "fetching resource list":"kaynak listesi getiriliyor", 44 | "Explore":"Keşfet", 45 | "Show Swagger Petstore Example Apis":"Swagger Petstore Örnek Api'yi Gör", 46 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Sunucudan okuma yapılamıyor. Sunucu access-control-origin ayarlarınızı kontrol edin.", 47 | "Please specify the protocol for":"Lütfen istenen adres için protokol belirtiniz", 48 | "Can't read swagger JSON from":"Swagger JSON bu kaynaktan okunamıyor", 49 | "Finished Loading Resource Information. Rendering Swagger UI":"Kaynak baglantısı tamamlandı. Swagger UI gösterime hazırlanıyor", 50 | "Unable to read api":"api okunamadı", 51 | "from path":"yoldan", 52 | "server returned":"sunucuya dönüldü" 53 | }); 54 | -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/lang/translator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Translator for documentation pages. 5 | * 6 | * To enable translation you should include one of language-files in your index.html 7 | * after . 8 | * For example - 9 | * 10 | * If you wish to translate some new texts you should do two things: 11 | * 1. Add a new phrase pair ("New Phrase": "New Translation") into your language file (for example lang/ru.js). It will be great if you add it in other language files too. 12 | * 2. Mark that text it templates this way New Phrase or . 13 | * The main thing here is attribute data-sw-translate. Only inner html, title-attribute and value-attribute are going to translate. 14 | * 15 | */ 16 | window.SwaggerTranslator = { 17 | 18 | _words:[], 19 | 20 | translate: function(sel) { 21 | var $this = this; 22 | sel = sel || '[data-sw-translate]'; 23 | 24 | $(sel).each(function() { 25 | $(this).html($this._tryTranslate($(this).html())); 26 | 27 | $(this).val($this._tryTranslate($(this).val())); 28 | $(this).attr('title', $this._tryTranslate($(this).attr('title'))); 29 | }); 30 | }, 31 | 32 | _tryTranslate: function(word) { 33 | return this._words[$.trim(word)] !== undefined ? this._words[$.trim(word)] : word; 34 | }, 35 | 36 | learn: function(wordsMap) { 37 | this._words = wordsMap; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/lang/zh-cn.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"警告:已过时", 6 | "Implementation Notes":"实现备注", 7 | "Response Class":"响应类", 8 | "Status":"状态", 9 | "Parameters":"参数", 10 | "Parameter":"参数", 11 | "Value":"值", 12 | "Description":"描述", 13 | "Parameter Type":"参数类型", 14 | "Data Type":"数据类型", 15 | "Response Messages":"响应消息", 16 | "HTTP Status Code":"HTTP状态码", 17 | "Reason":"原因", 18 | "Response Model":"响应模型", 19 | "Request URL":"请求URL", 20 | "Response Body":"响应体", 21 | "Response Code":"响应码", 22 | "Response Headers":"响应头", 23 | "Hide Response":"隐藏响应", 24 | "Headers":"头", 25 | "Try it out!":"试一下!", 26 | "Show/Hide":"显示/隐藏", 27 | "List Operations":"显示操作", 28 | "Expand Operations":"展开操作", 29 | "Raw":"原始", 30 | "can't parse JSON. Raw result":"无法解析JSON. 原始结果", 31 | "Example Value":"示例", 32 | "Click to set as parameter value":"点击设置参数", 33 | "Model Schema":"模型架构", 34 | "Model":"模型", 35 | "apply":"应用", 36 | "Username":"用户名", 37 | "Password":"密码", 38 | "Terms of service":"服务条款", 39 | "Created by":"创建者", 40 | "See more at":"查看更多:", 41 | "Contact the developer":"联系开发者", 42 | "api version":"api版本", 43 | "Response Content Type":"响应Content Type", 44 | "Parameter content type:":"参数类型:", 45 | "fetching resource":"正在获取资源", 46 | "fetching resource list":"正在获取资源列表", 47 | "Explore":"浏览", 48 | "Show Swagger Petstore Example Apis":"显示 Swagger Petstore 示例 Apis", 49 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"无法从服务器读取。可能没有正确设置access-control-origin。", 50 | "Please specify the protocol for":"请指定协议:", 51 | "Can't read swagger JSON from":"无法读取swagger JSON于", 52 | "Finished Loading Resource Information. Rendering Swagger UI":"已加载资源信息。正在渲染Swagger UI", 53 | "Unable to read api":"无法读取api", 54 | "from path":"从路径", 55 | "server returned":"服务器返回" 56 | }); 57 | -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/lib/highlight.9.1.0.pack.js: -------------------------------------------------------------------------------- 1 | !function(e){"undefined"!=typeof exports?e(exports):(self.hljs=e({}),"function"==typeof define&&define.amd&&define("hljs",[],function(){return self.hljs}))}(function(e){function r(e){return e.replace(/&/gm,"&").replace(//gm,">")}function t(e){return e.nodeName.toLowerCase()}function n(e,r){var t=e&&e.exec(r);return t&&0==t.index}function a(e){return/^(no-?highlight|plain|text)$/i.test(e)}function c(e){var r,t,n,c=e.className+" ";if(c+=e.parentNode?e.parentNode.className:"",t=/\blang(?:uage)?-([\w-]+)\b/i.exec(c))return E(t[1])?t[1]:"no-highlight";for(c=c.split(/\s+/),r=0,n=c.length;n>r;r++)if(E(c[r])||a(c[r]))return c[r]}function i(e,r){var t,n={};for(t in e)n[t]=e[t];if(r)for(t in r)n[t]=r[t];return n}function o(e){var r=[];return function n(e,a){for(var c=e.firstChild;c;c=c.nextSibling)3==c.nodeType?a+=c.nodeValue.length:1==c.nodeType&&(r.push({event:"start",offset:a,node:c}),a=n(c,a),t(c).match(/br|hr|img|input/)||r.push({event:"stop",offset:a,node:c}));return a}(e,0),r}function s(e,n,a){function c(){return e.length&&n.length?e[0].offset!=n[0].offset?e[0].offset"}function o(e){l+=""}function s(e){("start"==e.event?i:o)(e.node)}for(var u=0,l="",f=[];e.length||n.length;){var b=c();if(l+=r(a.substr(u,b[0].offset-u)),u=b[0].offset,b==e){f.reverse().forEach(o);do s(b.splice(0,1)[0]),b=c();while(b==e&&b.length&&b[0].offset==u);f.reverse().forEach(i)}else"start"==b[0].event?f.push(b[0].node):f.pop(),s(b.splice(0,1)[0])}return l+r(a.substr(u))}function u(e){function r(e){return e&&e.source||e}function t(t,n){return new RegExp(r(t),"m"+(e.cI?"i":"")+(n?"g":""))}function n(a,c){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var o={},s=function(r,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");o[t[0]]=[r,t[1]?Number(t[1]):1]})};"string"==typeof a.k?s("keyword",a.k):Object.keys(a.k).forEach(function(e){s(e,a.k[e])}),a.k=o}a.lR=t(a.l||/\b\w+\b/,!0),c&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=r(a.e)||"",a.eW&&c.tE&&(a.tE+=(a.e?"|":"")+c.tE)),a.i&&(a.iR=t(a.i)),void 0===a.r&&(a.r=1),a.c||(a.c=[]);var u=[];a.c.forEach(function(e){e.v?e.v.forEach(function(r){u.push(i(e,r))}):u.push("self"==e?a:e)}),a.c=u,a.c.forEach(function(e){n(e,a)}),a.starts&&n(a.starts,c);var l=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(r).filter(Boolean);a.t=l.length?t(l.join("|"),!0):{exec:function(){return null}}}}n(e)}function l(e,t,a,c){function i(e,r){for(var t=0;t";return c+=e+'">',c+r+i}function p(){if(!M.k)return r(B);var e="",t=0;M.lR.lastIndex=0;for(var n=M.lR.exec(B);n;){e+=r(B.substr(t,n.index-t));var a=b(M,n);a?(L+=a[1],e+=g(a[0],r(n[0]))):e+=r(n[0]),t=M.lR.lastIndex,n=M.lR.exec(B)}return e+r(B.substr(t))}function h(){var e="string"==typeof M.sL;if(e&&!y[M.sL])return r(B);var t=e?l(M.sL,B,!0,R[M.sL]):f(B,M.sL.length?M.sL:void 0);return M.r>0&&(L+=t.r),e&&(R[M.sL]=t.top),g(t.language,t.value,!1,!0)}function d(){return void 0!==M.sL?h():p()}function m(e,t){var n=e.cN?g(e.cN,"",!0):"";e.rB?(x+=n,B=""):e.eB?(x+=r(t)+n,B=""):(x+=n,B=t),M=Object.create(e,{parent:{value:M}})}function v(e,t){if(B+=e,void 0===t)return x+=d(),0;var n=i(t,M);if(n)return x+=d(),m(n,t),n.rB?0:t.length;var a=o(M,t);if(a){var c=M;c.rE||c.eE||(B+=t),x+=d();do M.cN&&(x+=""),L+=M.r,M=M.parent;while(M!=a.parent);return c.eE&&(x+=r(t)),B="",a.starts&&m(a.starts,""),c.rE?0:t.length}if(s(t,M))throw new Error('Illegal lexeme "'+t+'" for mode "'+(M.cN||"")+'"');return B+=t,t.length||1}var N=E(e);if(!N)throw new Error('Unknown language: "'+e+'"');u(N);var C,M=c||N,R={},x="";for(C=M;C!=N;C=C.parent)C.cN&&(x=g(C.cN,"",!0)+x);var B="",L=0;try{for(var S,A,k=0;M.t.lastIndex=k,S=M.t.exec(t),S;)A=v(t.substr(k,S.index-k),S[0]),k=S.index+A;for(v(t.substr(k)),C=M;C.parent;C=C.parent)C.cN&&(x+="");return{r:L,value:x,language:e,top:M}}catch(I){if(-1!=I.message.indexOf("Illegal"))return{r:0,value:r(t)};throw I}}function f(e,t){t=t||w.languages||Object.keys(y);var n={r:0,value:r(e)},a=n;return t.forEach(function(r){if(E(r)){var t=l(r,e,!1);t.language=r,t.r>a.r&&(a=t),t.r>n.r&&(a=n,n=t)}}),a.language&&(n.second_best=a),n}function b(e){return w.tabReplace&&(e=e.replace(/^((<[^>]+>|\t)+)/gm,function(e,r){return r.replace(/\t/g,w.tabReplace)})),w.useBR&&(e=e.replace(/\n/g,"
")),e}function g(e,r,t){var n=r?C[r]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(n)&&a.push(n),a.join(" ").trim()}function p(e){var r=c(e);if(!a(r)){var t;w.useBR?(t=document.createElementNS("http://www.w3.org/1999/xhtml","div"),t.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):t=e;var n=t.textContent,i=r?l(r,n,!0):f(n),u=o(t);if(u.length){var p=document.createElementNS("http://www.w3.org/1999/xhtml","div");p.innerHTML=i.value,i.value=s(u,o(p),n)}i.value=b(i.value),e.innerHTML=i.value,e.className=g(e.className,r,i.language),e.result={language:i.language,re:i.r},i.second_best&&(e.second_best={language:i.second_best.language,re:i.second_best.r})}}function h(e){w=i(w,e)}function d(){if(!d.called){d.called=!0;var e=document.querySelectorAll("pre code");Array.prototype.forEach.call(e,p)}}function m(){addEventListener("DOMContentLoaded",d,!1),addEventListener("load",d,!1)}function v(r,t){var n=y[r]=t(e);n.aliases&&n.aliases.forEach(function(e){C[e]=r})}function N(){return Object.keys(y)}function E(e){return e=(e||"").toLowerCase(),y[e]||y[C[e]]}var w={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},y={},C={};return e.highlight=l,e.highlightAuto=f,e.fixMarkup=b,e.highlightBlock=p,e.configure=h,e.initHighlighting=d,e.initHighlightingOnLoad=m,e.registerLanguage=v,e.listLanguages=N,e.getLanguage=E,e.inherit=i,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/},e.C=function(r,t,n){var a=e.inherit({cN:"comment",b:r,e:t,c:[]},n||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e}),hljs.registerLanguage("json",function(e){var r={literal:"true false null"},t=[e.QSM,e.CNM],n={e:",",eW:!0,eE:!0,c:t,k:r},a={b:"{",e:"}",c:[{cN:"attr",b:'\\s*"',e:'"\\s*:\\s*',eB:!0,eE:!0,c:[e.BE],i:"\\n",starts:n}],i:"\\S"},c={b:"\\[",e:"\\]",c:[e.inherit(n)],i:"\\S"};return t.splice(t.length,0,a,c),{c:t,k:r,i:"\\S"}}),hljs.registerLanguage("xml",function(e){var r="[A-Za-z0-9\\._:-]+",t={b:/<\?(php)?(?!\w)/,e:/\?>/,sL:"php"},n={eW:!0,i:/]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xsl","plist"],cI:!0,c:[{cN:"meta",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},e.C("",{r:10}),{b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"tag",b:"|$)",e:">",k:{name:"style"},c:[n],starts:{e:"",rE:!0,sL:["css","xml"]}},{cN:"tag",b:"|$)",e:">",k:{name:"script"},c:[n],starts:{e:"",rE:!0,sL:["actionscript","javascript","handlebars","xml"]}},t,{cN:"meta",b:/<\?\w+/,e:/\?>/,r:10},{cN:"tag",b:"",c:[{cN:"name",b:/[^\/><\s]+/,r:0},n]}]}}),hljs.registerLanguage("javascript",function(e){return{aliases:["js"],k:{keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},c:[{cN:"meta",r:10,b:/^\s*['"]use (strict|asm)['"]/},{cN:"meta",b:/^#!/,e:/$/},e.ASM,e.QSM,{cN:"string",b:"`",e:"`",c:[e.BE,{cN:"subst",b:"\\$\\{",e:"\\}"}]},e.CLCM,e.CBCM,{cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{b:/\s*[);\]]/,r:0,sL:"xml"}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:[e.CLCM,e.CBCM]}],i:/\[|%/},{b:/\$[(.]/},{b:"\\."+e.IR,r:0},{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]},{bK:"constructor",e:/\{/,eE:!0}],i:/#(?!!)/}}),hljs.registerLanguage("css",function(e){var r="[a-zA-Z-][a-zA-Z0-9_-]*",t={b:/[A-Z\_\.\-]+\s*:/,rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:/\S/,e:":",eE:!0,starts:{eW:!0,eE:!0,c:[{b:/[\w-]+\s*\(/,rB:!0,c:[{cN:"built_in",b:/[\w-]+/}]},e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"number",b:"#[0-9A-Fa-f]+"},{cN:"meta",b:"!important"}]}}]};return{cI:!0,i:/[=\/|'\$]/,c:[e.CBCM,{cN:"selector-id",b:/#[A-Za-z0-9_-]+/},{cN:"selector-class",b:/\.[A-Za-z0-9_-]+/},{cN:"selector-attr",b:/\[/,e:/\]/,i:"$"},{cN:"selector-pseudo",b:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{b:"@",e:"[{;]",c:[{cN:"keyword",b:/\S+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[e.ASM,e.QSM,e.CSSNM]}]},{cN:"selector-tag",b:r,r:0},{b:"{",e:"}",i:/\S/,c:[e.CBCM,t]}]}}); -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/lib/highlight.9.1.0.pack_extended.js: -------------------------------------------------------------------------------- 1 | "use strict";!function(){var h,l;h=hljs.configure,hljs.configure=function(l){var i=l.highlightSizeThreshold;hljs.highlightSizeThreshold=i===+i?i:null,h.call(this,l)},l=hljs.highlightBlock,hljs.highlightBlock=function(h){var i=h.innerHTML,g=hljs.highlightSizeThreshold;(null==g||g>i.length)&&l.call(hljs,h)}}(); -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/lib/jquery.ba-bbq.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){function n(e){return"string"==typeof e}function r(e){var t=g.call(arguments,1);return function(){return e.apply(this,t.concat(g.call(arguments)))}}function o(e){return e.replace(/^[^#]*#?(.*)$/,"$1")}function a(e){return e.replace(/(?:^[^?#]*\?([^#]*).*$)?.*/,"$1")}function i(r,o,a,i,c){var u,s,p,h,d;return i!==f?(p=a.match(r?/^([^#]*)\#?(.*)$/:/^([^#?]*)\??([^#]*)(#?.*)/),d=p[3]||"",2===c&&n(i)?s=i.replace(r?R:E,""):(h=l(p[2]),i=n(i)?l[r?A:w](i):i,s=2===c?i:1===c?e.extend({},i,h):e.extend({},h,i),s=b(s),r&&(s=s.replace(m,y))),u=p[1]+(r?"#":s||!p[1]?"?":"")+s+d):u=o(a!==f?a:t[S][q]),u}function c(e,t,r){return t===f||"boolean"==typeof t?(r=t,t=b[e?A:w]()):t=n(t)?t.replace(e?R:E,""):t,l(t,r)}function u(t,r,o,a){return n(o)||"object"==typeof o||(a=o,o=r,r=f),this.each(function(){var n=e(this),i=r||v()[(this.nodeName||"").toLowerCase()]||"",c=i&&n.attr(i)||"";n.attr(i,b[t](c,o,a))})}var f,s,l,p,h,d,v,m,g=Array.prototype.slice,y=decodeURIComponent,b=e.param,$=e.bbq=e.bbq||{},x=e.event.special,j="hashchange",w="querystring",A="fragment",N="elemUrlAttr",S="location",q="href",C="src",E=/^.*\?|#.*$/g,R=/^.*\#/,U={};b[w]=r(i,0,a),b[A]=s=r(i,1,o),s.noEscape=function(t){t=t||"";var n=e.map(t.split(""),encodeURIComponent);m=new RegExp(n.join("|"),"g")},s.noEscape(",/"),e.deparam=l=function(t,n){var r={},o={"true":!0,"false":!1,"null":null};return e.each(t.replace(/\+/g," ").split("&"),function(t,a){var i,c=a.split("="),u=y(c[0]),s=r,l=0,p=u.split("]["),h=p.length-1;if(/\[/.test(p[0])&&/\]$/.test(p[h])?(p[h]=p[h].replace(/\]$/,""),p=p.shift().split("[").concat(p),h=p.length-1):h=0,2===c.length)if(i=y(c[1]),n&&(i=i&&!isNaN(i)?+i:"undefined"===i?f:o[i]!==f?o[i]:i),h)for(;l<=h;l++)u=""===p[l]?s.length:p[l],s=s[u]=l').hide().insertAfter("body")[0].contentWindow,s=function(){return r(a.document[i][u])},(f=function(e,t){if(e!==t){var n=a.document;n.open().close(),n[i].hash="#"+e}})(r()))}var o,a,f,s,p={};return p.start=function(){if(!o){var a=r();f||n(),function l(){var n=r(),p=s(a);n!==a?(f(a=n,p),e(t).trigger(c)):p!==a&&(t[i][u]=t[i][u].replace(/#.*/,"")+"#"+p),o=setTimeout(l,e[c+"Delay"])}()}},p.stop=function(){a||(o&&clearTimeout(o),o=0)},p}()}(jQuery,this); -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/lib/jquery.slideto.min.js: -------------------------------------------------------------------------------- 1 | !function(i){i.fn.slideto=function(o){return o=i.extend({slide_duration:"slow",highlight_duration:3e3,highlight:!0,highlight_color:"#FFFF99"},o),this.each(function(){obj=i(this),i("body").animate({scrollTop:obj.offset().top},o.slide_duration,function(){o.highlight&&i.ui.version&&obj.effect("highlight",{color:o.highlight_color},o.highlight_duration)})})}}(jQuery); -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/lib/jquery.wiggle.min.js: -------------------------------------------------------------------------------- 1 | jQuery.fn.wiggle=function(e){var a={speed:50,wiggles:3,travel:5,callback:null},e=jQuery.extend(a,e);return this.each(function(){var a=this,l=(jQuery(this).wrap('
').css("position","relative"),0);for(i=1;i<=e.wiggles;i++)jQuery(this).animate({left:"-="+e.travel},e.speed).animate({left:"+="+2*e.travel},2*e.speed).animate({left:"-="+e.travel},e.speed,function(){l++,jQuery(a).parent().hasClass("wiggle-wrap")&&jQuery(a).parent().replaceWith(a),l==e.wiggles&&jQuery.isFunction(e.callback)&&e.callback()})})}; -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/lib/marked.js: -------------------------------------------------------------------------------- 1 | (function(){function e(e){this.tokens=[],this.tokens.links={},this.options=e||a.defaults,this.rules=p.normal,this.options.gfm&&(this.options.tables?this.rules=p.tables:this.rules=p.gfm)}function t(e,t){if(this.options=t||a.defaults,this.links=e,this.rules=u.normal,this.renderer=this.options.renderer||new n,this.renderer.options=this.options,!this.links)throw new Error("Tokens array requires a `links` property.");this.options.gfm?this.options.breaks?this.rules=u.breaks:this.rules=u.gfm:this.options.pedantic&&(this.rules=u.pedantic)}function n(e){this.options=e||{}}function r(e){this.tokens=[],this.token=null,this.options=e||a.defaults,this.options.renderer=this.options.renderer||new n,this.renderer=this.options.renderer,this.renderer.options=this.options}function s(e,t){return e.replace(t?/&/g:/&(?!#?\w+;)/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function i(e){return e.replace(/&([#\w]+);/g,function(e,t){return t=t.toLowerCase(),"colon"===t?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""})}function l(e,t){return e=e.source,t=t||"",function n(r,s){return r?(s=s.source||s,s=s.replace(/(^|[^\[])\^/g,"$1"),e=e.replace(r,s),n):new RegExp(e,t)}}function o(){}function h(e){for(var t,n,r=1;rAn error occured:

"+s(c.message+"",!0)+"
";throw c}}var p={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:o,hr:/^( *[-*_]){3,} *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,nptable:o,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,blockquote:/^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,list:/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:/^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,def:/^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,table:o,paragraph:/^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,text:/^[^\n]+/};p.bullet=/(?:[*+-]|\d+\.)/,p.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/,p.item=l(p.item,"gm")(/bull/g,p.bullet)(),p.list=l(p.list)(/bull/g,p.bullet)("hr","\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))")("def","\\n+(?="+p.def.source+")")(),p.blockquote=l(p.blockquote)("def",p.def)(),p._tag="(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b",p.html=l(p.html)("comment",//)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/])*?>/)(/tag/g,p._tag)(),p.paragraph=l(p.paragraph)("hr",p.hr)("heading",p.heading)("lheading",p.lheading)("blockquote",p.blockquote)("tag","<"+p._tag)("def",p.def)(),p.normal=h({},p),p.gfm=h({},p.normal,{fences:/^ *(`{3,}|~{3,}) *(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/,paragraph:/^/}),p.gfm.paragraph=l(p.paragraph)("(?!","(?!"+p.gfm.fences.source.replace("\\1","\\2")+"|"+p.list.source.replace("\\1","\\3")+"|")(),p.tables=h({},p.gfm,{nptable:/^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,table:/^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/}),e.rules=p,e.lex=function(t,n){var r=new e(n);return r.lex(t)},e.prototype.lex=function(e){return e=e.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n"),this.token(e,!0)},e.prototype.token=function(e,t,n){for(var r,s,i,l,o,h,a,u,c,e=e.replace(/^ +$/gm,"");e;)if((i=this.rules.newline.exec(e))&&(e=e.substring(i[0].length),i[0].length>1&&this.tokens.push({type:"space"})),i=this.rules.code.exec(e))e=e.substring(i[0].length),i=i[0].replace(/^ {4}/gm,""),this.tokens.push({type:"code",text:this.options.pedantic?i:i.replace(/\n+$/,"")});else if(i=this.rules.fences.exec(e))e=e.substring(i[0].length),this.tokens.push({type:"code",lang:i[2],text:i[3]});else if(i=this.rules.heading.exec(e))e=e.substring(i[0].length),this.tokens.push({type:"heading",depth:i[1].length,text:i[2]});else if(t&&(i=this.rules.nptable.exec(e))){for(e=e.substring(i[0].length),h={type:"table",header:i[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:i[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:i[3].replace(/\n$/,"").split("\n")},u=0;u ?/gm,""),this.token(i,t,!0),this.tokens.push({type:"blockquote_end"});else if(i=this.rules.list.exec(e)){for(e=e.substring(i[0].length),l=i[2],this.tokens.push({type:"list_start",ordered:l.length>1}),i=i[0].match(this.rules.item),r=!1,c=i.length,u=0;u1&&o.length>1||(e=i.slice(u+1).join("\n")+e,u=c-1)),s=r||/\n\n(?!\s*$)/.test(h),u!==c-1&&(r="\n"===h.charAt(h.length-1),s||(s=r)),this.tokens.push({type:s?"loose_item_start":"list_item_start"}),this.token(h,!1,n),this.tokens.push({type:"list_item_end"});this.tokens.push({type:"list_end"})}else if(i=this.rules.html.exec(e))e=e.substring(i[0].length),this.tokens.push({type:this.options.sanitize?"paragraph":"html",pre:"pre"===i[1]||"script"===i[1]||"style"===i[1],text:i[0]});else if(!n&&t&&(i=this.rules.def.exec(e)))e=e.substring(i[0].length),this.tokens.links[i[1].toLowerCase()]={href:i[2],title:i[3]};else if(t&&(i=this.rules.table.exec(e))){for(e=e.substring(i[0].length),h={type:"table",header:i[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:i[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:i[3].replace(/(?: *\| *)?\n$/,"").split("\n")},u=0;u])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:o,tag:/^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,link:/^!?\[(inside)\]\(href\)/,reflink:/^!?\[(inside)\]\s*\[([^\]]*)\]/,nolink:/^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,strong:/^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,em:/^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,code:/^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,br:/^ {2,}\n(?!\s*$)/,del:o,text:/^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/,u.link=l(u.link)("inside",u._inside)("href",u._href)(),u.reflink=l(u.reflink)("inside",u._inside)(),u.normal=h({},u),u.pedantic=h({},u.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/}),u.gfm=h({},u.normal,{escape:l(u.escape)("])","~|])")(),url:/^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,del:/^~~(?=\S)([\s\S]*?\S)~~/,text:l(u.text)("]|","~]|")("|","|https?://|")()}),u.breaks=h({},u.gfm,{br:l(u.br)("{2,}","*")(),text:l(u.gfm.text)("{2,}","*")()}),t.rules=u,t.output=function(e,n,r){var s=new t(n,r);return s.output(e)},t.prototype.output=function(e){for(var t,n,r,i,l="";e;)if(i=this.rules.escape.exec(e))e=e.substring(i[0].length),l+=i[1];else if(i=this.rules.autolink.exec(e))e=e.substring(i[0].length),"@"===i[2]?(n=":"===i[1].charAt(6)?this.mangle(i[1].substring(7)):this.mangle(i[1]),r=this.mangle("mailto:")+n):(n=s(i[1]),r=n),l+=this.renderer.link(r,null,n);else if(this.inLink||!(i=this.rules.url.exec(e))){if(i=this.rules.tag.exec(e))!this.inLink&&/^/i.test(i[0])&&(this.inLink=!1),e=e.substring(i[0].length),l+=this.options.sanitize?s(i[0]):i[0];else if(i=this.rules.link.exec(e))e=e.substring(i[0].length),this.inLink=!0,l+=this.outputLink(i,{href:i[2],title:i[3]}),this.inLink=!1;else if((i=this.rules.reflink.exec(e))||(i=this.rules.nolink.exec(e))){if(e=e.substring(i[0].length),t=(i[2]||i[1]).replace(/\s+/g," "),t=this.links[t.toLowerCase()],!t||!t.href){l+=i[0].charAt(0),e=i[0].substring(1)+e;continue}this.inLink=!0,l+=this.outputLink(i,t),this.inLink=!1}else if(i=this.rules.strong.exec(e))e=e.substring(i[0].length),l+=this.renderer.strong(this.output(i[2]||i[1]));else if(i=this.rules.em.exec(e))e=e.substring(i[0].length),l+=this.renderer.em(this.output(i[2]||i[1]));else if(i=this.rules.code.exec(e))e=e.substring(i[0].length),l+=this.renderer.codespan(s(i[2],!0));else if(i=this.rules.br.exec(e))e=e.substring(i[0].length),l+=this.renderer.br();else if(i=this.rules.del.exec(e))e=e.substring(i[0].length),l+=this.renderer.del(this.output(i[1]));else if(i=this.rules.text.exec(e))e=e.substring(i[0].length),l+=s(this.smartypants(i[0]));else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0))}else e=e.substring(i[0].length),n=s(i[1]),r=n,l+=this.renderer.link(r,null,n);return l},t.prototype.outputLink=function(e,t){var n=s(t.href),r=t.title?s(t.title):null;return"!"!==e[0].charAt(0)?this.renderer.link(n,r,this.output(e[1])):this.renderer.image(n,r,s(e[1]))},t.prototype.smartypants=function(e){return this.options.smartypants?e.replace(/--/g,"—").replace(/(^|[-\u2014\/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014\/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…"):e},t.prototype.mangle=function(e){for(var t,n="",r=e.length,s=0;s.5&&(t="x"+t.toString(16)),n+="&#"+t+";";return n},n.prototype.code=function(e,t,n){if(this.options.highlight){var r=this.options.highlight(e,t);null!=r&&r!==e&&(n=!0,e=r)}return t?'
'+(n?e:s(e,!0))+"\n
\n":"
"+(n?e:s(e,!0))+"\n
"},n.prototype.blockquote=function(e){return"
\n"+e+"
\n"},n.prototype.html=function(e){return e},n.prototype.heading=function(e,t,n){return"'+e+"\n"},n.prototype.hr=function(){return this.options.xhtml?"
\n":"
\n"},n.prototype.list=function(e,t){var n=t?"ol":"ul";return"<"+n+">\n"+e+"\n"},n.prototype.listitem=function(e){return"
  • "+e+"
  • \n"},n.prototype.paragraph=function(e){return"

    "+e+"

    \n"},n.prototype.table=function(e,t){return"\n\n"+e+"\n\n"+t+"\n
    \n"},n.prototype.tablerow=function(e){return"\n"+e+"\n"},n.prototype.tablecell=function(e,t){var n=t.header?"th":"td",r=t.align?"<"+n+' style="text-align:'+t.align+'">':"<"+n+">";return r+e+"\n"},n.prototype.strong=function(e){return""+e+""},n.prototype.em=function(e){return""+e+""},n.prototype.codespan=function(e){return""+e+""},n.prototype.br=function(){return this.options.xhtml?"
    ":"
    "},n.prototype.del=function(e){return""+e+""},n.prototype.link=function(e,t,n){if(this.options.sanitize){try{var r=decodeURIComponent(i(e)).replace(/[^\w:]/g,"").toLowerCase()}catch(s){return""}if(0===r.indexOf("javascript:"))return""}var l='
    "},n.prototype.image=function(e,t,n){var r=''+n+'":">"},r.parse=function(e,t,n){var s=new r(t,n);return s.parse(e)},r.prototype.parse=function(e){this.inline=new t(e.links,this.options,this.renderer),this.tokens=e.reverse();for(var n="";this.next();)n+=this.tok();return n},r.prototype.next=function(){return this.token=this.tokens.pop()},r.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0},r.prototype.parseText=function(){for(var e=this.token.text;"text"===this.peek().type;)e+="\n"+this.next().text;return this.inline.output(e)},r.prototype.tok=function(){switch(this.token.type){case"space":return"";case"hr":return this.renderer.hr();case"heading":return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,this.token.text);case"code":return this.renderer.code(this.token.text,this.token.lang,this.token.escaped);case"table":var e,t,n,r,s,i="",l="";for(n="",e=0;e','
    Select OAuth2.0 Scopes
    ','
    ',"

    Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.",'Learn how to use',"

    ","

    "+appName+" API requires the following scopes. Select which ones you want to grant to Swagger UI.

    ",'
      ',"
    ",'

    ','
    ',"
    ",""].join("")),$(document.body).append(popupDialog),popup=popupDialog.find("ul.api-popup-scopes").empty(),p=0;p",popup.append(str);var r=$(window),c=r.width(),s=r.height(),l=r.scrollTop(),d=popupDialog.outerWidth(),u=popupDialog.outerHeight(),h=(s-u)/2+l,g=(c-d)/2;popupDialog.css({top:(h<0?0:h)+"px",left:(g<0?0:g)+"px"}),popupDialog.find("button.api-popup-cancel").click(function(){popupMask.hide(),popupDialog.hide(),popupDialog.empty(),popupDialog=[]}),$("button.api-popup-authbtn").unbind(),popupDialog.find("button.api-popup-authbtn").click(function(){popupMask.hide(),popupDialog.hide();var e,o=window.swaggerUi.api.authSchemes,i=window.location,a=location.pathname.substring(0,location.pathname.lastIndexOf("/")),n=i.protocol+"//"+i.host+a+"/o2c.html",t=window.oAuthRedirectUrl||n,p=null,r=[],c=popup.find("input:checked"),s=[];for(k=0;k0?void log("auth unable initialize oauth: "+i):($("pre code").each(function(e,o){hljs.highlightBlock(o)}),$(".api-ic").unbind(),void $(".api-ic").click(function(e){$(e.target).hasClass("ic-off")?handleLogin():handleLogout()}))}function clientCredentialsFlow(e,o,i){var a={client_id:clientId,client_secret:clientSecret,scope:e.join(" "),grant_type:"client_credentials"};$.ajax({url:o,type:"POST",data:a,success:function(e,o,a){onOAuthComplete(e,i)},error:function(e,o,i){onOAuthComplete("")}})}var appName,popupMask,popupDialog,clientId,realm,redirect_uri,clientSecret,scopeSeparator,additionalQueryStringParams;window.processOAuthCode=function(e){var o=e.state,i=window.location,a=location.pathname.substring(0,location.pathname.lastIndexOf("/")),n=i.protocol+"//"+i.host+a+"/o2c.html",t=window.oAuthRedirectUrl||n,p={client_id:clientId,code:e.code,grant_type:"authorization_code",redirect_uri:t};clientSecret&&(p.client_secret=clientSecret),$.ajax({url:window.swaggerUiAuth.tokenUrl,type:"POST",data:p,success:function(e,i,a){onOAuthComplete(e,o)},error:function(e,o,i){onOAuthComplete("")}})},window.onOAuthComplete=function(e,o){if(e)if(e.error){var i=$("input[type=checkbox],.secured");i.each(function(e){i[e].checked=!1}),alert(e.error)}else{var a=e[window.swaggerUiAuth.tokenName];if(o||(o=e.state),a){var n=null;$.each($(".auth .api-ic .api_information_panel"),function(e,o){var i=o;if(i&&i.childNodes){var a=[];$.each(i.childNodes,function(e,o){var i=o.innerHTML;i&&a.push(i)});for(var t=[],p=0;p0?(n=o.parentNode.parentNode,$(n.parentNode).find(".api-ic.ic-on").addClass("ic-off"),$(n.parentNode).find(".api-ic.ic-on").removeClass("ic-on"),$(n).find(".api-ic").addClass("ic-warning"),$(n).find(".api-ic").removeClass("ic-error")):(n=o.parentNode.parentNode,$(n.parentNode).find(".api-ic.ic-off").addClass("ic-on"),$(n.parentNode).find(".api-ic.ic-off").removeClass("ic-off"),$(n).find(".api-ic").addClass("ic-info"),$(n).find(".api-ic").removeClass("ic-warning"),$(n).find(".api-ic").removeClass("ic-error"))}}),"undefined"!=typeof window.swaggerUi&&(window.swaggerUi.api.clientAuthorizations.add(window.swaggerUiAuth.OAuthSchemeKey,new SwaggerClient.ApiKeyAuthorization("Authorization","Bearer "+a,"header")),window.swaggerUi.load())}}}; -------------------------------------------------------------------------------- /src/main/resources/public/vendors/swagger-ui/o2c.html: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/ActionResult.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty 2 | 3 | case class ErrorModel(errors: Seq[String]) 4 | 5 | case class ActionResult[T](status: Int, body: T, headers: Map[String, String] = Map.empty){ 6 | def withHeaders(headers: (String, String)*): ActionResult[T] = { 7 | copy(headers = headers.toMap) 8 | } 9 | } 10 | 11 | object Ok { 12 | def apply(): ActionResult[Unit] = ActionResult[Unit](200, ()) 13 | def apply[T](body: T): ActionResult[T] = ActionResult[T](200, body) 14 | } 15 | 16 | object BadRequest { 17 | def apply[T](): T = throw new ActionResultException(ActionResult(400, ())) 18 | def apply[T](body: AnyRef): T = throw new ActionResultException(ActionResult(400, body)) 19 | } 20 | 21 | 22 | object NotFound { 23 | def apply[T](): T = throw new ActionResultException(ActionResult(404, ())) 24 | def apply[T](body: AnyRef): T = throw new ActionResultException(ActionResult(404, body)) 25 | } 26 | 27 | object InternalServerError { 28 | def apply[T](): T = throw new ActionResultException(ActionResult(500, ())) 29 | def apply[T](body: AnyRef): T = throw new ActionResultException(ActionResult(500, body)) 30 | } 31 | 32 | class ActionResultException(val result: ActionResult[_]) extends RuntimeException(result.body.toString) -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/CORSSupport.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty 2 | 3 | import java.util.concurrent.atomic.{AtomicBoolean, AtomicLong, AtomicReference} 4 | import javax.servlet.ServletContextEvent 5 | import javax.servlet.http.{HttpServletRequest, HttpServletResponse} 6 | 7 | import com.github.takezoe.resty.servlet.ConfigKeys 8 | import com.github.takezoe.resty.util.StringUtils 9 | import scala.collection.JavaConverters._ 10 | 11 | object CORSSupport { 12 | 13 | private val DefaultHeaders = Seq( 14 | "Cookie", 15 | "Host", 16 | "X-Forwarded-For", 17 | "Accept-Charset", 18 | "If-Modified-Since", 19 | "Accept-Language", 20 | "X-Forwarded-Port", 21 | "Connection", 22 | "X-Forwarded-Proto", 23 | "User-Agent", 24 | "Referer", 25 | "Accept-Encoding", 26 | "X-Requested-With", 27 | "Authorization", 28 | "Accept", 29 | "Content-Type" 30 | ).map(_.toUpperCase) 31 | 32 | private val SimpleHeaders: Seq[String] = Seq( 33 | "Origin", 34 | "Accept", 35 | "Accept-Language", 36 | "Content-Language" 37 | ).map(_.toUpperCase) 38 | 39 | private val SimpleContentTypes: Seq[String] = Seq( 40 | "application/x-www-form-urlencoded", 41 | "multipart/form-data", 42 | "text/plain" 43 | ) 44 | 45 | private val enable = new AtomicBoolean(false) 46 | private val allowedOrigins = new AtomicReference[Seq[String]](Seq("*")) 47 | private val allowedMethods = new AtomicReference[Seq[String]](Seq("GET", "POST", "PUT", "DELETE")) 48 | private val allowedHeaders = new AtomicReference[Seq[String]](DefaultHeaders) 49 | private val preflightMaxAge = new AtomicLong(0) 50 | private val allowCredentials = new AtomicBoolean(false) 51 | 52 | def initialize(sce: ServletContextEvent): Unit = { 53 | if("enable" == StringUtils.trim(sce.getServletContext.getInitParameter(ConfigKeys.CORSSupport))){ 54 | enable.set(true) 55 | 56 | Option(sce.getServletContext.getInitParameter(ConfigKeys.CORSAllowedOrigins)).foreach { value => 57 | allowedOrigins.set(value.split(",").map(_.trim)) 58 | } 59 | Option(sce.getServletContext.getInitParameter(ConfigKeys.CORSAllowedMethods)).foreach { value => 60 | allowedMethods.set(value.split(",").map(_.trim.toUpperCase)) 61 | } 62 | Option(sce.getServletContext.getInitParameter(ConfigKeys.CORSAllowedHeaders)).foreach { value => 63 | allowedHeaders.set(value.split(",").map(_.trim.toUpperCase)) 64 | } 65 | Option(sce.getServletContext.getInitParameter(ConfigKeys.CORSPreflightMaxAge)).foreach { value => 66 | preflightMaxAge.set(StringUtils.trim(value).toLong) 67 | } 68 | Option(sce.getServletContext.getInitParameter(ConfigKeys.CORSAllowCredentials)).foreach { value => 69 | allowCredentials.set(StringUtils.trim(value).toBoolean) 70 | } 71 | } 72 | } 73 | 74 | def processCORSRequest(request: HttpServletRequest): Option[CORSInfo] = { 75 | val isPreflight = request.getMethod == "OPTION" && request.getHeader("Access-Control-Request-Method") != null 76 | 77 | def _getAllowedOrigin: Option[String] = 78 | if(allowCredentials.get() == false && allowedOrigins.get.contains("*")){ 79 | Some("*") 80 | } else if (allowedOrigins.get().contains(request.getHeader("Origin"))){ 81 | Some(request.getHeader("Origin")) 82 | } else None 83 | 84 | def _getAllowedMethods: Option[Seq[String]] = { 85 | val method = if(isPreflight){ 86 | request.getHeader("Access-Control-Request-Method") 87 | } else { 88 | request.getMethod 89 | }.toUpperCase 90 | 91 | if(allowedMethods.get().contains(method)){ 92 | Some(allowedMethods.get()) 93 | } else None 94 | } 95 | 96 | def _getAllowedHeaders: Option[Seq[String]] = { 97 | val headers = if(isPreflight){ 98 | request.getHeader("Access-Control-Request-Headers").split(",").map(_.trim).toSeq 99 | } else { 100 | request.getHeaderNames.asScala 101 | }.map(_.toUpperCase) 102 | 103 | if(headers.forall { header => 104 | SimpleHeaders.contains(header) || 105 | (header == "CONTENT-TYPE" && SimpleContentTypes.contains(request.getContentType)) || 106 | allowedHeaders.get().contains(header) 107 | }){ 108 | Some(Option(request.getHeader("Access-Control-Request-Headers")).map(_.split(", ").map(_.trim).toSeq).getOrElse(Nil)) 109 | } else None 110 | } 111 | 112 | if(enable.get() && request.getHeader("Origin") != null){ 113 | for { 114 | origin <- _getAllowedOrigin 115 | methods <- _getAllowedMethods 116 | headers <- _getAllowedHeaders 117 | } yield CORSInfo(origin, methods, headers, isPreflight) 118 | } else None 119 | } 120 | 121 | def setCORSResponseHeaders(response: HttpServletResponse, corsInfo: CORSInfo): Unit = { 122 | if(enable.get()){ 123 | response.setHeader("Access-Control-Allow-Origin", corsInfo.allowedOrigin) 124 | if(allowCredentials.get()){ 125 | response.setHeader("Access-Control-Allow-Credentials", "true") 126 | } 127 | if(corsInfo.isPreflight && preflightMaxAge.get() > 0){ 128 | response.setHeader("Access-Control-Max-Age", preflightMaxAge.get().toString) 129 | if(corsInfo.allowedMethods.nonEmpty){ 130 | response.setHeader("Access-Control-Allow-Methods", corsInfo.allowedMethods.mkString(", ")) 131 | } 132 | if(corsInfo.allowedHeaders.nonEmpty){ 133 | response.setHeader("Access-Control-Allow-Headers", corsInfo.allowedHeaders.mkString(", ")) 134 | } 135 | } 136 | } 137 | } 138 | 139 | case class CORSInfo( 140 | allowedOrigin: String, 141 | allowedMethods: Seq[String], 142 | allowedHeaders: Seq[String], 143 | isPreflight: Boolean 144 | ) 145 | 146 | } 147 | -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/HttpClientSupport.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty 2 | 3 | import java.io.IOException 4 | import java.net.InetAddress 5 | import java.nio.charset.StandardCharsets 6 | import java.util.concurrent.atomic.{AtomicInteger, AtomicReference} 7 | import javax.servlet.ServletContextEvent 8 | 9 | import com.github.takezoe.resty.servlet.ConfigKeys 10 | import com.github.takezoe.resty.util.{JsonUtils, StringUtils} 11 | import zipkin2.reporter.AsyncReporter 12 | import brave._ 13 | import brave.sampler._ 14 | import _root_.okhttp3._ 15 | import brave.okhttp3.TracingInterceptor 16 | import zipkin2.reporter.okhttp3.OkHttpSender 17 | 18 | import scala.concurrent.{Future, Promise} 19 | import scala.reflect.ClassTag 20 | 21 | object HttpClientSupport { 22 | 23 | val ContentType_JSON = MediaType.parse("application/json; charset=utf-8") 24 | 25 | private val _tracing = new AtomicReference[Tracing](null) 26 | private val _httpClient = new AtomicReference[OkHttpClient](null) 27 | 28 | def tracing: Tracing = { 29 | val instance = _tracing.get() 30 | if(instance == null){ 31 | throw new IllegalStateException("HttpClientSupport has not been initialized or Zipkin support is disabled.") 32 | } 33 | instance 34 | } 35 | 36 | def httpClient = { 37 | val instance = _httpClient.get() 38 | if(instance == null){ 39 | throw new IllegalStateException("HttpClientSupport has not been initialized yet.") 40 | } 41 | instance 42 | } 43 | 44 | def initialize(sce: ServletContextEvent): Unit = { 45 | if(_httpClient.get() != null){ 46 | throw new IllegalArgumentException("HttpClientSupport has been already initialized.") 47 | } 48 | 49 | if("enable" == StringUtils.trim(sce.getServletContext.getInitParameter(ConfigKeys.ZipkinSupport))){ 50 | val name = StringUtils.trim(sce.getServletContext.getInitParameter(ConfigKeys.ZipkinServiceName)) 51 | val url = StringUtils.trim(sce.getServletContext.getInitParameter(ConfigKeys.ZipkinServerUrl)) 52 | val rate = StringUtils.trim(sce.getServletContext.getInitParameter(ConfigKeys.ZipkinSampleRate)) 53 | val builder = Tracing.newBuilder().localServiceName(if(name.nonEmpty) name else InetAddress.getLocalHost.getHostAddress) 54 | 55 | if(url.nonEmpty){ 56 | val reporter = AsyncReporter.builder(OkHttpSender.create(url.trim)).build() 57 | builder.spanReporter(reporter) 58 | } 59 | 60 | if(rate.nonEmpty){ 61 | val sampler = Sampler.create(rate.toFloat) 62 | builder.sampler(sampler) 63 | } 64 | 65 | val httpTracing = builder.build() 66 | 67 | _tracing.set(httpTracing) 68 | 69 | val client = new OkHttpClient.Builder().dispatcher(new Dispatcher( 70 | tracing.currentTraceContext().executorService(new Dispatcher().executorService()) 71 | )) 72 | .addNetworkInterceptor(TracingInterceptor.create(httpTracing)) 73 | .build() 74 | 75 | _httpClient.set(client) 76 | 77 | } else { 78 | _httpClient.set(new OkHttpClient()) 79 | } 80 | } 81 | 82 | def shutdown(sce: ServletContextEvent): Unit = { 83 | if(_httpClient.get() == null){ 84 | throw new IllegalArgumentException("HttpClientSupport is inactive now.") 85 | } 86 | _tracing.set(null) 87 | _httpClient.get().dispatcher().executorService().shutdown() 88 | _httpClient.set(null) 89 | } 90 | 91 | } 92 | 93 | /** 94 | * Base trait for the HTTP request target. 95 | */ 96 | trait RequestTarget { 97 | 98 | def execute[T](httpClient: OkHttpClient, configurer: (String, Request.Builder) => Unit, clazz: Class[_]): Either[ErrorModel, T] 99 | 100 | def executeAsync[T](httpClient: OkHttpClient, configurer: (String, Request.Builder) => Unit, clazz: Class[_]): Future[Either[ErrorModel, T]] 101 | 102 | protected def handleResponse[T](request: Request, response: Response, clazz: Class[_]): Either[ErrorModel, T] = { 103 | try { 104 | response.code match { 105 | case 200 => { 106 | val result = if (clazz == classOf[String]) { 107 | new String(response.body.bytes, StandardCharsets.UTF_8) 108 | } else { 109 | JsonUtils.deserialize(new String(response.body.bytes, StandardCharsets.UTF_8), clazz) 110 | } 111 | Right(result.asInstanceOf[T]) 112 | } 113 | case code => 114 | Left(ErrorModel(Seq(s"${request.url.toString} responded status ${code}"))) 115 | } 116 | } finally { 117 | response.close() 118 | } 119 | } 120 | 121 | } 122 | 123 | /** 124 | * Implementation of [[RequestTarget]] for single url. 125 | */ 126 | class SimpleRequestTarget(val url: String, config: HttpClientConfig) extends RequestTarget { 127 | 128 | private val disabledTime = new AtomicReference[Option[Long]](None) 129 | private val failureCount = new AtomicInteger(0) 130 | 131 | def isAvailable: Boolean = { 132 | (config.maxFailure <= 0 || failureCount.get() <= config.maxFailure) 133 | } 134 | 135 | def execute[T](httpClient: OkHttpClient, configurer: (String, Request.Builder) => Unit, clazz: Class[_]): Either[ErrorModel, T] = { 136 | try { 137 | checkWhetherEnabled() 138 | 139 | val builder = new Request.Builder() 140 | configurer(url, builder) 141 | 142 | val request = builder.build() 143 | val response = withRetry(httpClient, request, config) 144 | 145 | // reset failure counter 146 | disabledTime.set(None) 147 | failureCount.set(0) 148 | 149 | handleResponse(request, response, clazz) 150 | 151 | } catch { 152 | case e: Exception => { 153 | if(config.maxFailure > 0 && failureCount.incrementAndGet() > config.maxFailure){ 154 | disabledTime.set(Some(System.currentTimeMillis)) 155 | } 156 | Left(ErrorModel(Seq(e.toString))) 157 | } 158 | } 159 | } 160 | 161 | protected def checkWhetherEnabled(): Unit = { 162 | if(config.maxFailure <= 0 || failureCount.get() < config.maxFailure){ 163 | () 164 | } else { 165 | disabledTime.get() match { 166 | case None => () 167 | case Some(time) if time <= System.currentTimeMillis - config.resetInterval => { 168 | failureCount.set(config.maxFailure - 1) 169 | disabledTime.set(None) 170 | } 171 | case Some(_) => throw new RuntimeException(s"${url} is not available now.") 172 | } 173 | } 174 | } 175 | 176 | protected def withRetry(httpClient: OkHttpClient, request: Request, config: HttpClientConfig): Response = { 177 | var count = 0 178 | while(true){ 179 | try { 180 | return httpClient.newCall(request).execute() 181 | } catch { 182 | case _: Exception if count < config.maxRetry => { 183 | count = count + 1 184 | Thread.sleep(config.retryInterval) 185 | } 186 | } 187 | } 188 | ??? 189 | } 190 | 191 | def executeAsync[T](httpClient: OkHttpClient, configurer: (String, Request.Builder) => Unit, clazz: Class[_]): Future[Either[ErrorModel, T]] = { 192 | try { 193 | checkWhetherEnabled() 194 | 195 | val builder = new Request.Builder() 196 | configurer(url, builder) 197 | 198 | val request = builder.build() 199 | val promise = Promise[Either[ErrorModel, T]]() 200 | 201 | httpClient.newCall(request).enqueue(new Callback { 202 | var retryCount = 0 203 | override def onFailure(call: Call, e: IOException): Unit = { 204 | if(retryCount < config.maxRetry){ 205 | retryCount = retryCount + 1 206 | Thread.sleep(config.retryInterval) // TODO Don't brock a thread here! 207 | httpClient.newCall(request) 208 | } else { 209 | if(config.maxFailure > 0 && failureCount.incrementAndGet() > config.maxFailure){ 210 | disabledTime.set(Some(System.currentTimeMillis)) 211 | } 212 | promise.failure(e) 213 | } 214 | } 215 | override def onResponse(call: Call, response: Response): Unit = { 216 | // reset failure counter 217 | disabledTime.set(None) 218 | failureCount.set(0) 219 | 220 | promise.success(handleResponse(request, response, clazz)) 221 | } 222 | }) 223 | 224 | promise.future 225 | 226 | } catch { 227 | case e: Exception => Future.successful(Left(ErrorModel(Seq(e.toString)))) 228 | } 229 | } 230 | } 231 | 232 | /** 233 | * Implementation of [[RequestTarget]] for multiple urls. This implementation chooses a url from given urls randomly. 234 | */ 235 | class RandomRequestTarget(val urls: Seq[String], config: HttpClientConfig) extends RequestTarget { 236 | 237 | private val targets = urls.map(url => new SimpleRequestTarget(url, config)) 238 | 239 | protected def nextTarget: Option[RequestTarget] = { 240 | val availableTargets = targets.filter((_.isAvailable)) 241 | if(availableTargets.isEmpty){ 242 | None 243 | } else { 244 | Some(availableTargets((scala.math.random * availableTargets.length).toInt)) 245 | } 246 | } 247 | 248 | override def execute[T](httpClient: OkHttpClient, configurer: (String, Request.Builder) => Unit, clazz: Class[_]): Either[ErrorModel, T] = { 249 | nextTarget match { 250 | case Some(target) => target.execute(httpClient, configurer, clazz) 251 | case None => Left(ErrorModel(Seq("No available url!"))) 252 | } 253 | } 254 | 255 | override def executeAsync[T](httpClient: OkHttpClient, configurer: (String, Request.Builder) => Unit, clazz: Class[_]): Future[Either[ErrorModel, T]] = { 256 | nextTarget match { 257 | case Some(target) => target.executeAsync(httpClient, configurer, clazz) 258 | case None => Future.successful(Left(ErrorModel(Seq("No available url!")))) 259 | } 260 | } 261 | 262 | } 263 | 264 | /** 265 | * Configuration of behavior of HttpClient. 266 | * 267 | * @param maxRetry default is 0 means no retry 268 | * @param retryInterval msec. default is 0 means retry immediately 269 | * @param maxFailure default is 0 means disabling circuit breaker 270 | * @param resetInterval msec. default is 60000 271 | */ 272 | case class HttpClientConfig(maxRetry: Int = 0, retryInterval: Int = 0, maxFailure: Int = 0, resetInterval: Int = 60000) 273 | 274 | /** 275 | * HTTP client with Zipkin support. 276 | */ 277 | trait HttpClientSupport { 278 | 279 | implicit def httpClientConfig: HttpClientConfig = HttpClientConfig() 280 | implicit def string2target(url: String)(implicit config: HttpClientConfig): SimpleRequestTarget = new SimpleRequestTarget(url, config) 281 | implicit def stringSeq2target(urls: Seq[String])(implicit config: HttpClientConfig): RandomRequestTarget = new RandomRequestTarget(urls, config) 282 | 283 | protected def httpClient = HttpClientSupport.httpClient 284 | 285 | def httpGet[T](target: RequestTarget, configurer: Request.Builder => Unit = (builder) => ())(implicit c: ClassTag[T]): Either[ErrorModel, T] = { 286 | target.execute(httpClient, (url, builder) => { 287 | builder.url(url).get() 288 | configurer(builder) 289 | }, c.runtimeClass) 290 | } 291 | 292 | def httpGetAsync[T](target: RequestTarget, configurer: Request.Builder => Unit = (builder) => ())(implicit c: ClassTag[T]): Future[Either[ErrorModel, T]] = { 293 | target.executeAsync(httpClient, (url, builder) => { 294 | builder.url(url).get() 295 | configurer(builder) 296 | }, c.runtimeClass) 297 | } 298 | 299 | def httpPost[T](target: RequestTarget, params: Map[String, String], configurer: Request.Builder => Unit = (builder) => ())(implicit c: ClassTag[T]): Either[ErrorModel, T] = { 300 | target.execute(httpClient, (url, builder) => { 301 | val formBuilder = new FormBody.Builder() 302 | params.foreach { case (key, value) => formBuilder.add(key, value) } 303 | 304 | builder.url(url).post(formBuilder.build()) 305 | 306 | configurer(builder) 307 | }, c.runtimeClass) 308 | } 309 | 310 | def httpPostAsync[T](target: RequestTarget, params: Map[String, String], configurer: Request.Builder => Unit = (builder) => ())(implicit c: ClassTag[T]): Future[Either[ErrorModel, T]] = { 311 | target.executeAsync(httpClient, (url, builder) => { 312 | val formBuilder = new FormBody.Builder() 313 | params.foreach { case (key, value) => formBuilder.add(key, value) } 314 | 315 | builder.url(url).post(formBuilder.build()) 316 | 317 | configurer(builder) 318 | }, c.runtimeClass) 319 | } 320 | 321 | def httpPostJson[T](target: RequestTarget, doc: AnyRef, configurer: Request.Builder => Unit = (builder) => ())(implicit c: ClassTag[T]): Either[ErrorModel, T] = { 322 | target.execute(httpClient, (url, builder) => { 323 | builder.url(url).post(RequestBody.create(HttpClientSupport.ContentType_JSON, JsonUtils.serialize(doc))) 324 | configurer(builder) 325 | }, c.runtimeClass) 326 | } 327 | 328 | def httpPostJsonAsync[T](target: RequestTarget, doc: AnyRef, configurer: Request.Builder => Unit = (builder) => ())(implicit c: ClassTag[T]): Future[Either[ErrorModel, T]] = { 329 | target.executeAsync(httpClient, (url, builder) => { 330 | builder.url(url).post(RequestBody.create(HttpClientSupport.ContentType_JSON, JsonUtils.serialize(doc))) 331 | configurer(builder) 332 | }, c.runtimeClass) 333 | } 334 | 335 | def httpPut[T](target: RequestTarget, params: Map[String, String], configurer: Request.Builder => Unit = (builder) => ())(implicit c: ClassTag[T]): Either[ErrorModel, T] = { 336 | target.execute(httpClient, (url, builder) => { 337 | val formBuilder = new FormBody.Builder() 338 | params.foreach { case (key, value) => formBuilder.add(key, value) } 339 | 340 | builder.url(url).put(formBuilder.build()) 341 | 342 | configurer(builder) 343 | }, c.runtimeClass) 344 | } 345 | 346 | def httpPutAsync[T](target: RequestTarget, params: Map[String, String], configurer: Request.Builder => Unit = (builder) => ())(implicit c: ClassTag[T]): Future[Either[ErrorModel, T]] = { 347 | target.executeAsync(httpClient, (url, builder) => { 348 | val formBuilder = new FormBody.Builder() 349 | params.foreach { case (key, value) => formBuilder.add(key, value) } 350 | 351 | builder.url(url).put(formBuilder.build()) 352 | 353 | configurer(builder) 354 | }, c.runtimeClass) 355 | } 356 | 357 | def httpPutJson[T](target: RequestTarget, doc: AnyRef, configurer: Request.Builder => Unit = (builder) => ())(implicit c: ClassTag[T]): Either[ErrorModel, T] = { 358 | target.execute(httpClient, (url, builder) => { 359 | builder.url(url).put(RequestBody.create(HttpClientSupport.ContentType_JSON, JsonUtils.serialize(doc))) 360 | configurer(builder) 361 | }, c.runtimeClass) 362 | } 363 | 364 | def httpPutJsonAsync[T](target: RequestTarget, doc: AnyRef, configurer: Request.Builder => Unit = (builder) => ())(implicit c: ClassTag[T]): Future[Either[ErrorModel, T]] = { 365 | target.executeAsync(httpClient, (url, builder) => { 366 | builder.url(url).put(RequestBody.create(HttpClientSupport.ContentType_JSON, JsonUtils.serialize(doc))) 367 | configurer(builder) 368 | }, c.runtimeClass) 369 | } 370 | 371 | def httpDelete[T](target: RequestTarget, configurer: Request.Builder => Unit = (builder) => ())(implicit c: ClassTag[T]): Either[ErrorModel, T] = { 372 | target.execute(httpClient, (url, builder) => { 373 | builder.url(url).delete() 374 | configurer(builder) 375 | }, c.runtimeClass) 376 | } 377 | 378 | def httpDeleteAsync[T](target: RequestTarget, configurer: Request.Builder => Unit = (builder) => ())(implicit c: ClassTag[T]): Future[Either[ErrorModel, T]] = { 379 | target.executeAsync(httpClient, (url, builder) => { 380 | builder.url(url).delete() 381 | configurer(builder) 382 | }, c.runtimeClass) 383 | } 384 | 385 | } 386 | 387 | 388 | 389 | -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/HystrixSupport.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty 2 | 3 | import java.util.concurrent.atomic.AtomicBoolean 4 | import javax.servlet.ServletContextEvent 5 | 6 | import com.github.takezoe.resty.servlet.ConfigKeys 7 | import com.github.takezoe.resty.util.StringUtils 8 | import com.netflix.hystrix.HystrixCommandProperties.ExecutionIsolationStrategy 9 | import com.netflix.hystrix._ 10 | import rx.Observable 11 | import rx.subjects.ReplaySubject 12 | 13 | import scala.concurrent.{ExecutionContext, Future} 14 | import scala.util.{Failure, Success} 15 | 16 | object HystrixSupport { 17 | 18 | private val enable = new AtomicBoolean(false) 19 | 20 | /** 21 | * HistrixCommand implementation for a synchronous action. 22 | */ 23 | class RestyActionCommand(key: String, f: => AnyRef) extends HystrixCommand[AnyRef]( 24 | HystrixCommand.Setter 25 | .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RestyAction")) 26 | .andCommandKey(HystrixCommandKey.Factory.asKey(key)) 27 | .andCommandPropertiesDefaults( 28 | HystrixCommandProperties.Setter() 29 | .withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE) 30 | .withExecutionIsolationSemaphoreMaxConcurrentRequests(1000)) 31 | ) { 32 | override def run(): AnyRef = f 33 | } 34 | 35 | /** 36 | * HistrixCommand implementation for a asynchronous action. 37 | */ 38 | class RestyAsyncActionCommand(key: String, future: Future[AnyRef], ec: ExecutionContext) extends HystrixObservableCommand[AnyRef]( 39 | HystrixObservableCommand.Setter 40 | .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RestyAction")) 41 | .andCommandKey(HystrixCommandKey.Factory.asKey(key)) 42 | .andCommandPropertiesDefaults( 43 | HystrixCommandProperties.Setter() 44 | .withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE) 45 | .withExecutionIsolationSemaphoreMaxConcurrentRequests(1000)) 46 | ) { 47 | 48 | override def construct(): Observable[AnyRef] = { 49 | val channel = ReplaySubject.create[AnyRef]() 50 | 51 | future.onComplete { 52 | case Success(result) => { 53 | channel.onNext(result) 54 | channel.onCompleted() 55 | } 56 | case Failure(error) => { 57 | channel.onError(error) 58 | } 59 | }(ec) 60 | 61 | channel.asObservable() 62 | } 63 | 64 | } 65 | 66 | 67 | def initialize(sce: ServletContextEvent): Unit = { 68 | if("enable" == StringUtils.trim(sce.getServletContext.getInitParameter(ConfigKeys.HystrixSupport))){ 69 | enable.set(true) 70 | } 71 | } 72 | 73 | def shutdown(sce: ServletContextEvent): Unit = { 74 | } 75 | 76 | def isEnabled = enable.get() 77 | 78 | } -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/ParamInjector.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty 2 | 3 | import javax.servlet.ServletContext 4 | import javax.servlet.http.{HttpServletRequest, HttpServletResponse, HttpSession} 5 | 6 | class ParamInjector { 7 | 8 | private val requestHolder = new ThreadLocal[HttpServletRequest] 9 | private val responseHolder = new ThreadLocal[HttpServletResponse] 10 | 11 | def withValues[T](request: HttpServletRequest, response: HttpServletResponse)(f: => T): T = { 12 | requestHolder.set(request) 13 | responseHolder.set(response) 14 | try { 15 | f 16 | } finally { 17 | requestHolder.remove() 18 | responseHolder.remove() 19 | } 20 | } 21 | 22 | def get(clazz: Class[_]): AnyRef = { 23 | if (clazz == classOf[HttpServletRequest] ) requestHolder.get 24 | else if(clazz == classOf[HttpServletResponse]) responseHolder.get 25 | else if(clazz == classOf[HttpSession] ) requestHolder.get.getSession 26 | else if(clazz == classOf[ServletContext] ) requestHolder.get.getServletContext 27 | else throw new MatchError(s"${clazz.getName} isn't injectable.") 28 | } 29 | 30 | } 31 | 32 | object ParamInjector { 33 | 34 | def isInjectable(clazz: Class[_]): Boolean = { 35 | clazz == classOf[HttpServletRequest] || 36 | clazz == classOf[HttpServletResponse] || 37 | clazz == classOf[HttpSession] || 38 | clazz == classOf[ServletContext] 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/Resty.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty 2 | 3 | import java.lang.reflect.Method 4 | import java.util.concurrent.{CopyOnWriteArrayList, Executors} 5 | import java.util.concurrent.atomic.AtomicReference 6 | 7 | import com.github.takezoe.resty.model.{ActionDef, AppInfo, ControllerDef, ParamDef} 8 | 9 | import scala.collection.mutable 10 | import scala.collection.JavaConverters._ 11 | import scala.concurrent.{ExecutionContext, Future} 12 | 13 | /** 14 | * Manages all actions information of the application. 15 | */ 16 | object Resty { 17 | 18 | val ioExecutionContext = ExecutionContext.fromExecutorService(Executors.newCachedThreadPool()) 19 | 20 | private val _appInfo = new AtomicReference[AppInfo](AppInfo()) 21 | private val _actions = new CopyOnWriteArrayList[(ControllerDef, ActionDef)]() 22 | 23 | def register(appInfo: AppInfo): Unit = { 24 | _appInfo.set(appInfo) 25 | } 26 | 27 | def appInfo = _appInfo.get() 28 | 29 | def register(controller: AnyRef): Unit = { 30 | val annotation = controller.getClass.getAnnotation(classOf[Controller]) 31 | 32 | val controllerDef = ControllerDef( 33 | name = if(annotation != null) annotation.name() else "", 34 | description = if(annotation != null) annotation.description() else "", 35 | instance = controller 36 | ) 37 | 38 | val controllerClass = controller.getClass 39 | 40 | controllerClass.getMethods.foreach { method => 41 | val annotation = method.getAnnotation(classOf[Action]) 42 | if(annotation != null){ 43 | _actions.add((controllerDef, ActionDef( 44 | annotation.method().toLowerCase(), 45 | annotation.path(), 46 | annotation.description(), 47 | annotation.deprecated(), 48 | getParamDefs(method, controllerClass, annotation), 49 | method, 50 | method.getReturnType == classOf[Future[_]] 51 | ))) 52 | } 53 | } 54 | } 55 | 56 | protected def getParamDefs(actionMethod: Method, controllerClass: Class[_], action: Action): Seq[ParamDef] = { 57 | actionMethod.getParameters.zipWithIndex.map { case (param, i) => 58 | actionMethod.getParameterAnnotations()(i).find(_.annotationType() == classOf[Param]).map { case x: Param => 59 | // @Param is specified 60 | val paramName = if(x.name().nonEmpty) x.name else param.getName 61 | val paramType = param.getType 62 | ParamDef( 63 | from = paramFrom(x.from(), action.path(), paramName, actionMethod, i, paramType), 64 | name = paramName, 65 | description = x.description(), 66 | method = actionMethod, 67 | index = i, 68 | clazz = paramType 69 | ) 70 | }.getOrElse { 71 | // @Param is not specified 72 | val paramName = param.getName 73 | val paramType = param.getType 74 | ParamDef( 75 | from = paramFrom("", action.path(), paramName, actionMethod, i, paramType), 76 | name = paramName, 77 | description = "", 78 | method = actionMethod, 79 | index = i, 80 | clazz = paramType 81 | ) 82 | } 83 | } 84 | } 85 | 86 | protected def paramFrom(from: String, path: String, name: String, 87 | actionMethod: Method, index: Int, clazz: Class[_]): String = { 88 | if(from.nonEmpty) from else { 89 | if(ParamDef.isSimpleType(clazz) || ParamDef.isSimpleContainerType(actionMethod, index, clazz)) { 90 | if (path.contains(s"{${name}}")) { 91 | "path" 92 | } else { 93 | "query" 94 | } 95 | } else if(ParamInjector.isInjectable(clazz)){ 96 | "inject" 97 | } else { 98 | "body" 99 | } 100 | } 101 | } 102 | 103 | def findAction(path: String, method: String): Option[(ControllerDef, ActionDef, Map[String, Seq[String]])] = { 104 | val pathParams = new mutable.HashMap[String, Seq[String]]() 105 | 106 | _actions.asScala.filter(_._2.method == method).find { case (controller, action) => 107 | val requestPath = path.split("/") 108 | val actionPath = action.path.split("/") 109 | if(requestPath.length == actionPath.length){ 110 | (requestPath zip actionPath).forall { case (requestPathFragment, actionPathFragment) => 111 | if(actionPathFragment.startsWith("{") && actionPathFragment.endsWith("}")){ 112 | pathParams += (actionPathFragment.substring(1, actionPathFragment.length - 1) -> Seq(requestPathFragment)) 113 | true 114 | } else { 115 | requestPathFragment == actionPathFragment 116 | } 117 | } 118 | } else false 119 | }.map { case (controller, action) => (controller, action, pathParams.toMap) } 120 | } 121 | 122 | def allActions: Seq[(ControllerDef, ActionDef)] = _actions.asScala 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/RestyKernel.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty 2 | 3 | import java.io.{File, FileInputStream, InputStream} 4 | import java.lang.reflect.InvocationTargetException 5 | import javax.servlet.AsyncContext 6 | import javax.servlet.http.{HttpServletRequest, HttpServletResponse} 7 | 8 | import com.github.takezoe.resty.model.{ActionDef, ControllerDef, ParamDef} 9 | import com.github.takezoe.resty.util.JsonUtils 10 | import com.netflix.hystrix.exception.HystrixRuntimeException 11 | import org.apache.commons.io.IOUtils 12 | import org.slf4j.LoggerFactory 13 | 14 | import scala.util.{Failure, Success} 15 | import scala.concurrent.Future 16 | 17 | trait RestyKernel { 18 | 19 | private val logger = LoggerFactory.getLogger(classOf[RestyKernel]) 20 | private val injector = new ParamInjector() 21 | 22 | protected def processAction(request: HttpServletRequest, response: HttpServletResponse, method: String): Unit = { 23 | injector.withValues(request, response){ 24 | val path = request.getRequestURI.substring(request.getContextPath.length) 25 | 26 | Resty.findAction(path, method) match { 27 | case Some((controller, action, pathParams)) => { 28 | if(action.async){ 29 | processAsyncAction(request, response, method, controller, action, pathParams) 30 | } else { 31 | processSyncAction(request, response, method, controller, action, pathParams) 32 | } 33 | } 34 | case None => { 35 | processResponse(response, ActionResult(404, ())) 36 | } 37 | } 38 | } 39 | } 40 | 41 | protected def processSyncAction(request: HttpServletRequest, response: HttpServletResponse, method: String, 42 | controller: ControllerDef, action: ActionDef, pathParams: Map[String, Seq[String]]): Unit = { 43 | try { 44 | val result = if(HystrixSupport.isEnabled) { 45 | new HystrixSupport.RestyActionCommand( 46 | action.method + " " + action.path, 47 | invokeAction(controller, action, pathParams, request, response) 48 | ).execute() 49 | } else { 50 | invokeAction(controller, action, pathParams, request, response) 51 | } 52 | 53 | processResponse(response, result) 54 | 55 | } catch { 56 | case e: HystrixRuntimeException => 57 | val cause = e.getCause match { 58 | case e: InvocationTargetException => e.getCause 59 | case e => e 60 | } 61 | logger.error("Error during processing action", cause) 62 | processResponse(response, ActionResult(500, ErrorModel(Seq(cause.toString)))) 63 | case e: InvocationTargetException => 64 | logger.error("Error during processing action", e.getCause) 65 | processResponse(response, ActionResult(500, ErrorModel(Seq(e.getCause.toString)))) 66 | case e: Exception => 67 | logger.error("Error during processing action", e) 68 | processResponse(response, ActionResult(500, ErrorModel(Seq(e.toString)))) 69 | } 70 | } 71 | 72 | protected def processAsyncAction(request: HttpServletRequest, response: HttpServletResponse, method: String, 73 | controller: ControllerDef, action: ActionDef, pathParams: Map[String, Seq[String]]): Unit = { 74 | val asyncContext = request.startAsync(request, response) 75 | val future = invokeAsyncAction(controller, action, pathParams, request, response, asyncContext) 76 | if(HystrixSupport.isEnabled){ 77 | new HystrixSupport.RestyAsyncActionCommand(action.method + " " + action.path, future, Resty.ioExecutionContext) 78 | .toObservable.subscribe( 79 | (result: AnyRef) => { 80 | processResponse(response, result) 81 | asyncContext.complete() 82 | }, 83 | (error: Throwable) => { 84 | val cause = error match { 85 | case e: HystrixRuntimeException => e.getCause 86 | case e => e 87 | } 88 | logger.error("Error during processing action", cause) 89 | processResponse(response, ActionResult(500, ErrorModel(Seq(cause.toString)))) 90 | asyncContext.complete() 91 | } 92 | ) 93 | } else { 94 | future.onComplete { 95 | case Success(result) => { 96 | processResponse(response, result) 97 | asyncContext.complete() 98 | } 99 | case Failure(error) => { 100 | processResponse(response, ActionResult(500, ErrorModel(Seq(error.toString)))) 101 | asyncContext.complete() 102 | } 103 | }(Resty.ioExecutionContext) 104 | } 105 | } 106 | 107 | protected def invokeAsyncAction(controller: ControllerDef, action: ActionDef, pathParams: Map[String, Seq[String]], 108 | request: HttpServletRequest, response: HttpServletResponse, context: AsyncContext): Future[AnyRef] = { 109 | try { 110 | prepareParams(request, pathParams, action.params) match { 111 | case Left(errors) => Future.successful(BadRequest(ErrorModel(errors))) 112 | case Right(params) => action.function.invoke(controller.instance, params: _*).asInstanceOf[Future[AnyRef]] 113 | } 114 | } catch { 115 | case e: InvocationTargetException => e.getCause match { 116 | case e: ActionResultException => Future.successful(e.result) 117 | case e => Future.failed(e) 118 | } 119 | case e: ActionResultException => Future.successful(e.result) 120 | } 121 | } 122 | 123 | protected def invokeAction(controller: ControllerDef, action: ActionDef, pathParams: Map[String, Seq[String]], 124 | request: HttpServletRequest, response: HttpServletResponse): AnyRef = { 125 | prepareParams(request, pathParams, action.params) match { 126 | case Left(errors) => BadRequest(ErrorModel(errors)) 127 | case Right(params) => action.function.invoke(controller.instance, params: _*) 128 | } 129 | } 130 | 131 | protected def prepareParams(request: HttpServletRequest, 132 | pathParams: Map[String, Seq[String]], 133 | paramDefs: Seq[ParamDef]): Either[Seq[String], Seq[AnyRef]] = { 134 | val converted = paramDefs.map { paramDef => 135 | paramDef match { 136 | case ParamDef.PathParam(name, _, converter) => 137 | converter.convert(pathParams.get(name).getOrElse(request.getParameterValues(name))) 138 | case ParamDef.QueryParam(name, _, converter) => 139 | converter.convert(pathParams.get(name).getOrElse(request.getParameterValues(name))) 140 | case ParamDef.HeaderParam(name, _, converter) => 141 | converter.convert(Seq(request.getHeader(name))) 142 | case ParamDef.BodyParam(_, _, _, converter) => 143 | converter.convert(Seq(IOUtils.toString(request.getInputStream, "UTF-8"))) 144 | case ParamDef.InjectParam(_, _, clazz, _) => 145 | Right(injector.get(clazz)) 146 | } 147 | } 148 | 149 | val errors = converted.collect { case Left(errorMessage) => errorMessage } 150 | 151 | if(errors.nonEmpty){ 152 | Left(errors) 153 | } else { 154 | Right(converted.collect { case Right(value) => value }) 155 | } 156 | } 157 | 158 | protected def processResponse(response: HttpServletResponse, result: Any): Unit = { 159 | result match { 160 | case null => {} 161 | case x: Unit => {} 162 | case x: String => { 163 | if(response.getContentType == null) { 164 | response.setContentType("text/plain; charset=UTF-8") 165 | } 166 | val writer = response.getWriter 167 | writer.println(x) 168 | writer.flush() 169 | } 170 | case x: ActionResult[_] => { 171 | response.setStatus(x.status) 172 | x.headers.foreach { case (key, value) => 173 | response.addHeader(key, value) 174 | } 175 | x.body match { 176 | case body: AnyRef => processResponse(response, body) 177 | case _ => 178 | } 179 | } 180 | case x: Array[Byte] => 181 | if(response.getContentType == null) { 182 | response.setContentType("application/octet-stream") 183 | } 184 | val out = response.getOutputStream 185 | out.write(x) 186 | out.flush() 187 | case x: InputStream => 188 | if(response.getContentType == null) { 189 | response.setContentType("application/octet-stream") 190 | } 191 | try { 192 | val out = response.getOutputStream 193 | IOUtils.copy(x, out) 194 | out.flush() 195 | } finally { 196 | IOUtils.closeQuietly(x) 197 | } 198 | case x: File => { 199 | if(response.getContentType == null) { 200 | response.setContentType("application/octet-stream") 201 | response.setHeader("Content-Disposition", "attachment; filename=\"" + x.getName + "\"") 202 | } 203 | val in = new FileInputStream(x) 204 | try { 205 | val out = response.getOutputStream 206 | IOUtils.copy(in, out) 207 | out.flush() 208 | } finally { 209 | IOUtils.closeQuietly(in) 210 | } 211 | } 212 | case x: AnyRef => { 213 | if(response.getContentType == null) { 214 | response.setContentType("application/json") 215 | } 216 | val out = response.getOutputStream 217 | out.write(JsonUtils.serialize(x).getBytes("UTF-8")) 218 | out.flush() 219 | } 220 | } 221 | } 222 | 223 | } 224 | 225 | -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/SwaggerController.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty 2 | 3 | import java.io.{File, InputStream} 4 | import java.lang.reflect.{Field, Method} 5 | 6 | import com.fasterxml.jackson.annotation.{JsonIgnoreProperties, JsonProperty} 7 | import com.github.takezoe.resty.model.ParamConverter.JsonConverter 8 | import com.github.takezoe.resty.model.ParamDef 9 | import com.github.takezoe.resty.util.{ReflectionUtils, ScaladocUtils} 10 | import io.swagger.models._ 11 | import io.swagger.models.parameters._ 12 | import io.swagger.models.properties._ 13 | 14 | import scala.collection.mutable 15 | import scala.concurrent.Future 16 | 17 | /** 18 | * Endpoint that provides Swagger 2.0 JSON. 19 | */ 20 | class SwaggerController { 21 | 22 | @Action(method = "GET", path = "/swagger.json") 23 | def swaggerJson() = { 24 | val swagger = new Swagger() 25 | 26 | val appInfo = Resty.appInfo 27 | if(appInfo.nonEmpty){ 28 | val info = new Info() 29 | if(appInfo.title.nonEmpty){ info.setTitle(appInfo.title) } 30 | if(appInfo.version.nonEmpty){ info.setVersion(appInfo.version) } 31 | if(appInfo.description.nonEmpty){ info.setDescription(appInfo.description) } 32 | swagger.setInfo(info) 33 | } 34 | 35 | val paths = new mutable.HashMap[String, Path]() 36 | val models = new mutable.HashMap[String, Model]() 37 | val tags = new mutable.HashMap[String, Tag]() 38 | 39 | Resty.allActions.filterNot(_._1.instance.isInstanceOf[SwaggerController]).foreach { case (controller, action) => 40 | val tag = new Tag() 41 | val tagName = if(controller.name.nonEmpty) controller.name else controller.instance.getClass.getSimpleName 42 | tag.setName(tagName) 43 | if(controller.description.nonEmpty){ 44 | tag.setDescription(controller.description) 45 | } else { 46 | ScaladocUtils.getScaladoc(controller.instance.getClass).foreach { scaladoc => 47 | tag.setDescription(scaladoc.description) 48 | } 49 | } 50 | tags.put(tag.getName, tag) 51 | 52 | val path = paths.getOrElseUpdate(action.path, new Path()) 53 | val operation = new Operation() 54 | operation.setOperationId(action.function.getName) 55 | operation.addTag(tagName) 56 | val scaladoc = ScaladocUtils.getScaladoc(action.function) 57 | if(action.description.nonEmpty){ 58 | operation.setDescription(action.description) 59 | } else { 60 | scaladoc.foreach { scaladoc => 61 | operation.setDescription(scaladoc.description) 62 | } 63 | } 64 | if(action.deprecated || ScaladocUtils.isDeprecated(action.function, scaladoc)){ 65 | operation.setDeprecated(true) 66 | } 67 | 68 | action.params.foreach { paramDef => 69 | paramDef match { 70 | case ParamDef.PathParam(name, description, converter) => 71 | val parameter = new PathParameter() 72 | if(description.nonEmpty){ 73 | parameter.setDescription(description) 74 | } else { 75 | ScaladocUtils.getParamDescription(name, scaladoc).foreach { description => 76 | parameter.setDescription(description) 77 | } 78 | } 79 | operation.addParameter(converter.parameter(parameter)) 80 | 81 | case ParamDef.QueryParam(name, description, converter) => 82 | val parameter = new QueryParameter() 83 | if(description.nonEmpty){ 84 | parameter.setDescription(description) 85 | } else { 86 | ScaladocUtils.getParamDescription(name, scaladoc).foreach { description => 87 | parameter.setDescription(description) 88 | } 89 | } 90 | operation.addParameter(converter.parameter(parameter)) 91 | 92 | case ParamDef.HeaderParam(name, description, converter) => 93 | val parameter = new HeaderParameter() 94 | if(description.nonEmpty){ 95 | parameter.setDescription(description) 96 | } else { 97 | ScaladocUtils.getParamDescription(name, scaladoc).foreach { description => 98 | parameter.setDescription(description) 99 | } 100 | } 101 | operation.addParameter(converter.parameter(parameter)) 102 | 103 | case ParamDef.BodyParam(name, description, clazz, converter) => 104 | val parameter = new BodyParameter() 105 | if(description.nonEmpty){ 106 | parameter.setDescription(description) 107 | } else { 108 | ScaladocUtils.getParamDescription(name, scaladoc).foreach { description => 109 | parameter.setDescription(description) 110 | } 111 | } 112 | operation.addParameter(converter.parameter(parameter)) 113 | if(converter.isInstanceOf[JsonConverter]){ 114 | models.put(clazz.getSimpleName, createModel(action.function, clazz, models)) 115 | } 116 | case ParamDef.InjectParam(_, _, _, _) => // Ignore inject parameter 117 | } 118 | } 119 | 120 | { 121 | val response = new Response() 122 | createProperty(action.function, models).map { property => 123 | response.setSchema(property) 124 | } 125 | ScaladocUtils.getReturnDescription(scaladoc).foreach { description => 126 | response.setDescription(description) 127 | } 128 | operation.addResponse("200", response) 129 | } 130 | 131 | { 132 | val returnType = classOf[ErrorModel] 133 | val response = new Response() 134 | response.setSchema(new RefProperty(returnType.getSimpleName)) 135 | models.put(returnType.getSimpleName, createModel(action.function, returnType, models)) 136 | operation.addResponse("default", response) 137 | operation.produces("application/json") 138 | } 139 | 140 | action.method match { 141 | case "get" => path.get(operation) 142 | case "post" => path.post(operation) 143 | case "put" => path.put(operation) 144 | case "delete" => path.delete(operation) 145 | } 146 | } 147 | 148 | tags.foreach { case (key, tag) => swagger.addTag(tag) } 149 | paths.foreach { case (key, path) => swagger.path(key, path) } 150 | models.foreach { case (key, model) => swagger.addDefinition(key, model) } 151 | 152 | swagger 153 | } 154 | 155 | protected def createModel(actionMethod: Method, clazz: Class[_], models: mutable.HashMap[String, Model]): Model = { 156 | // TODO: Jackson can not deserialize Seq[T] in default... 157 | if(clazz == classOf[Seq[_]]) { 158 | val model = new ArrayModel() 159 | ReflectionUtils.getWrappedTypeOfMethodArgument(actionMethod, 0).foreach { wrappedType => 160 | createSimpleProperty(actionMethod, wrappedType, models).foreach { wrappedProperty => 161 | model.setItems(wrappedProperty) 162 | } 163 | } 164 | model 165 | } else if(clazz.isArray){ 166 | val model = new ArrayModel() 167 | createSimpleProperty(actionMethod, clazz.getComponentType, models).foreach { wrappedProperty => 168 | model.setItems(wrappedProperty) 169 | } 170 | model 171 | } else { 172 | val model = new ModelImpl() 173 | model.setName(clazz.getSimpleName) 174 | 175 | clazz.getDeclaredFields.foreach { field => 176 | if(field.getName != "$outer"){ 177 | createProperty(actionMethod, field, models).foreach { property => 178 | val param = clazz.getConstructors.head.getParameters.find(_.getName == field.getName) 179 | 180 | val ignore = 181 | // Check @JsonIgnoreProperties 182 | Option(clazz.getAnnotation(classOf[JsonIgnoreProperties])).map { a => 183 | a.value().contains(field.getName) 184 | }.getOrElse(false) //|| // TODO jackson-module-scala does not seem to support @JsonIgnore 185 | // // Check @JsonIgnore 186 | // param.flatMap { param => 187 | // Option(param.getAnnotation(classOf[JsonIgnore])).map { a => 188 | // a.value() 189 | // } 190 | // }.getOrElse(false) 191 | 192 | if(!ignore){ 193 | val propertyName = param.flatMap { param => 194 | Option(param.getAnnotation(classOf[JsonProperty])).map { a => 195 | a.value() 196 | } 197 | }.getOrElse(field.getName) 198 | 199 | model.addProperty(propertyName, property) 200 | } 201 | } 202 | } 203 | } 204 | 205 | model 206 | } 207 | } 208 | 209 | protected def createProperty(actionMethod: Method, models: mutable.HashMap[String, Model]): Option[Property] = { 210 | val fieldType = actionMethod.getReturnType 211 | 212 | // TODO Map support? 213 | if(fieldType == classOf[Option[_]]){ 214 | ReflectionUtils.getWrappedTypeOfMethod(actionMethod).flatMap { wrappedType => 215 | createSimpleProperty(actionMethod, wrappedType, models) 216 | } 217 | } else if(fieldType == classOf[Seq[_]]) { 218 | ReflectionUtils.getWrappedTypeOfMethod(actionMethod).map { wrappedType => 219 | val property = new ArrayProperty() 220 | createSimpleProperty(actionMethod, wrappedType, models).foreach { wrappedProperty => 221 | property.setItems(wrappedProperty) 222 | } 223 | property 224 | } 225 | } else if(fieldType == classOf[ActionResult[_]]){ 226 | ReflectionUtils.getWrappedTypeOfMethod(actionMethod).flatMap { wrappedType => 227 | createSimpleProperty(actionMethod, wrappedType, models) 228 | } 229 | } else if(fieldType == classOf[Future[_]]){ 230 | // TODO When wrapped type is ActionResult...? 231 | ReflectionUtils.getWrappedTypeOfMethod(actionMethod).flatMap { wrappedType => 232 | createSimpleProperty(actionMethod, wrappedType, models) 233 | } 234 | } else { 235 | createSimpleProperty(actionMethod, fieldType, models).map { property => 236 | property.setRequired(true) 237 | property 238 | } 239 | } 240 | } 241 | 242 | protected def createProperty(actionMethod: Method, field: Field, models: mutable.HashMap[String, Model]): Option[Property] = { 243 | val fieldType = field.getType 244 | 245 | // TODO Map support? 246 | if(fieldType == classOf[Option[_]]){ 247 | ReflectionUtils.getWrappedTypeOfField(field).flatMap { wrappedType => 248 | createSimpleProperty(actionMethod, wrappedType, models) 249 | } 250 | } else if(fieldType == classOf[Seq[_]]){ 251 | ReflectionUtils.getWrappedTypeOfField(field).map { wrappedType => 252 | val property = new ArrayProperty() 253 | createSimpleProperty(actionMethod, wrappedType, models).foreach { wrappedProperty => 254 | property.setItems(wrappedProperty) 255 | } 256 | property 257 | } 258 | } else { 259 | createSimpleProperty(actionMethod, fieldType, models).map { property => 260 | property.setRequired(true) 261 | property 262 | } 263 | } 264 | } 265 | 266 | protected def createSimpleProperty(actionMethod: Method, clazz: Class[_], models: mutable.HashMap[String, Model]): Option[Property] = { 267 | if(clazz == classOf[String]){ 268 | Some(new StringProperty()) 269 | } else if(clazz == classOf[Int]){ 270 | Some(new IntegerProperty()) 271 | } else if(clazz == classOf[Long]) { 272 | Some(new LongProperty()) 273 | } else if(clazz == classOf[Double]) { 274 | Some(new DoubleProperty()) 275 | } else if(clazz == classOf[Boolean]) { 276 | Some(new BooleanProperty()) 277 | } else if(clazz.isArray && clazz.getComponentType == classOf[Byte]){ 278 | Some(new ByteArrayProperty()) 279 | } else if(clazz == classOf[File] || clazz == classOf[InputStream]){ 280 | Some(new FileProperty()) 281 | } else if(clazz == classOf[Unit]){ 282 | None 283 | } else { 284 | models.put(clazz.getSimpleName, createModel(actionMethod, clazz, models)) 285 | Some(new RefProperty(clazz.getSimpleName)) 286 | } 287 | } 288 | 289 | } 290 | -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/model/ActionDef.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty.model 2 | 3 | import java.lang.reflect.Method 4 | 5 | case class ActionDef( 6 | method: String, 7 | path: String, 8 | description: String, 9 | deprecated: Boolean, 10 | params: Seq[ParamDef], 11 | function: Method, 12 | async: Boolean 13 | ) 14 | 15 | -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/model/AppInfo.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty.model 2 | 3 | case class AppInfo(title: String = "", version: String = "", description: String = ""){ 4 | val isEmpty = title.isEmpty && version.isEmpty && description.isEmpty 5 | val nonEmpty = !isEmpty 6 | } -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/model/ControllerDef.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty.model 2 | 3 | case class ControllerDef ( 4 | name: String, 5 | description: String, 6 | instance: AnyRef 7 | ) -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/model/ParamConverter.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty.model 2 | 3 | import java.util.Base64 4 | 5 | import com.github.takezoe.resty.util.JsonUtils 6 | import io.swagger.models.RefModel 7 | import io.swagger.models.parameters.{BodyParameter, Parameter, SerializableParameter} 8 | import io.swagger.models.properties.{BooleanProperty, IntegerProperty, LongProperty, StringProperty} 9 | 10 | trait ParamConverter { 11 | def convert(values: Seq[String]): Either[String, AnyRef] 12 | def parameter(model: Parameter): Parameter 13 | } 14 | 15 | object ParamConverter { 16 | 17 | class StringConverter(name: String) extends ParamConverter { 18 | override def convert(values: Seq[String]): Either[String, AnyRef] = { 19 | if (values == null || values.isEmpty) { 20 | Left(s"${name} is required.") 21 | } else { 22 | Right(values.head) 23 | } 24 | } 25 | override def parameter(model: Parameter): Parameter = { 26 | val param = model.asInstanceOf[SerializableParameter] 27 | param.setName(name) 28 | param.setType("string") 29 | param 30 | } 31 | } 32 | 33 | class IntConverter(name: String) extends ParamConverter { 34 | override def convert(values: Seq[String]): Either[String, AnyRef] = { 35 | if (values == null || values.isEmpty) { 36 | Left(s"${name} is required.") 37 | } else { 38 | Right(new java.lang.Integer(values.head)) 39 | } 40 | } 41 | override def parameter(model: Parameter): Parameter = { 42 | val param = model.asInstanceOf[SerializableParameter] 43 | param.setName(name) 44 | param.setType("integer") 45 | param.setFormat("int32") 46 | param 47 | } 48 | } 49 | 50 | class LongConverter(name: String) extends ParamConverter { 51 | override def convert(values: Seq[String]): Either[String, AnyRef] = { 52 | if (values == null || values.isEmpty) { 53 | Left(s"${name} is required.") 54 | } else { 55 | Right(new java.lang.Long(values.head)) 56 | } 57 | } 58 | override def parameter(model: Parameter): Parameter = { 59 | val param = model.asInstanceOf[SerializableParameter] 60 | param.setName(name) 61 | param.setType("integer") 62 | param.setFormat("int64") 63 | param 64 | } 65 | } 66 | 67 | class BooleanConverter(name: String) extends ParamConverter { 68 | override def convert(values: Seq[String]): Either[String, AnyRef] = { 69 | if (values == null || values.isEmpty) { 70 | Left(s"${name} is required.") 71 | } else { 72 | Right(new java.lang.Boolean(values.head)) 73 | } 74 | } 75 | override def parameter(model: Parameter): Parameter = { 76 | val param = model.asInstanceOf[SerializableParameter] 77 | param.setName(name) 78 | param.setType("boolean") 79 | param 80 | } 81 | } 82 | 83 | class SimpleSeqConverter(name: String, converter: ParamConverter) extends ParamConverter { 84 | override def convert(values: Seq[String]): Either[String, AnyRef] = { 85 | if (values == null) { 86 | Right(Seq.empty) 87 | } else { 88 | val converted: Seq[Either[String, AnyRef]] = values.map { x => converter.convert(Seq(x)) } 89 | converted.find(_.isLeft).getOrElse(Right(converted.map(_.right.get))) 90 | } 91 | } 92 | override def parameter(model: Parameter): Parameter = { 93 | val param = model.asInstanceOf[SerializableParameter] 94 | param.setName(name) 95 | param.setType("array") 96 | converter match { 97 | case _: StringConverter => param.setItems(new StringProperty()) 98 | case _: IntConverter => param.setItems(new IntegerProperty()) 99 | case _: LongConverter => param.setItems(new LongProperty()) 100 | case _: BooleanConverter => param.setItems(new BooleanProperty()) 101 | } 102 | param 103 | } 104 | } 105 | 106 | class SimpleArrayConverter(name: String, converter: ParamConverter) extends SimpleSeqConverter(name, converter) { 107 | override def convert(values: Seq[String]): Either[String, AnyRef] = { 108 | if (values == null) { 109 | Right(Array[Int]()) 110 | } else { 111 | val converted: Seq[Either[String, AnyRef]] = values.map { x => converter.convert(Seq(x)) } 112 | converted.find(_.isLeft).getOrElse { 113 | val values = converted.map(_.right.get) 114 | Right(converter match { 115 | case _: StringConverter => values.asInstanceOf[Seq[String]].toArray 116 | case _: IntConverter => values.asInstanceOf[Seq[Int]].toArray 117 | case _: LongConverter => values.asInstanceOf[Seq[Long]].toArray 118 | case _: BooleanConverter => values.asInstanceOf[Seq[Boolean]].toArray 119 | }) 120 | } 121 | } 122 | } 123 | } 124 | 125 | class ByteArrayConverter(name: String) extends ParamConverter { 126 | override def convert(values: Seq[String]): Either[String, AnyRef] = { 127 | if (values == null || values.isEmpty) { 128 | Left(s"${name} is required.") 129 | } else { 130 | try { 131 | Right(Base64.getDecoder.decode(values.head)) 132 | } catch { 133 | case e: IllegalArgumentException => Left(e.toString) 134 | } 135 | } 136 | } 137 | 138 | override def parameter(model: Parameter): Parameter = { 139 | val param = model.asInstanceOf[SerializableParameter] 140 | param.setName(name) 141 | param.setType("string") 142 | param.setFormat("byte") 143 | param 144 | } 145 | } 146 | 147 | class OptionConverter(name: String, converter: ParamConverter) extends ParamConverter { 148 | override def convert(values: Seq[String]): Either[String, AnyRef] = { 149 | if (values == null || values.isEmpty) { 150 | Right(None) 151 | } else { 152 | converter.convert(values) match { 153 | case Right(x) => Right(Some(x)) 154 | case Left(x) => Left(x) 155 | } 156 | } 157 | } 158 | override def parameter(model: Parameter): Parameter = { 159 | val param = model.asInstanceOf[SerializableParameter] 160 | converter.parameter(param) 161 | param.setRequired(false) 162 | param 163 | } 164 | } 165 | 166 | class JsonConverter(name: String, clazz: Class[_]) extends ParamConverter { 167 | override def convert(values: Seq[String]): Either[String, AnyRef] = { 168 | if (values == null || values.isEmpty) { 169 | Left("Body is required") 170 | } else { 171 | try { 172 | Right(JsonUtils.deserialize(values.head, clazz)) 173 | } catch { 174 | case e: Exception => { 175 | e.getCause match { 176 | case cause: AssertionError => Left(cause.getMessage.replaceFirst("assertion failed: ", "")) 177 | case _ => Left(e.getMessage) 178 | } 179 | } 180 | } 181 | } 182 | } 183 | override def parameter(model: Parameter): Parameter = { 184 | val param = model.asInstanceOf[BodyParameter] 185 | param.setSchema(new RefModel(clazz.getSimpleName)) 186 | param.setName(name) 187 | param.setRequired(true) 188 | param 189 | } 190 | } 191 | 192 | /** 193 | * This is a dummy converter for inject parameters. 194 | */ 195 | object DummyConverter extends ParamConverter { 196 | override def convert(values: Seq[String]): Either[String, AnyRef] = ??? 197 | override def parameter(model: Parameter): Parameter = ??? 198 | } 199 | 200 | } 201 | -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/model/ParamDef.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty.model 2 | 3 | import java.lang.reflect.Method 4 | 5 | import com.github.takezoe.resty.model.ParamConverter.DummyConverter 6 | import com.github.takezoe.resty.util.ReflectionUtils 7 | 8 | sealed trait ParamDef { 9 | val name: String 10 | val description: String 11 | val converter: ParamConverter 12 | } 13 | 14 | object ParamDef { 15 | 16 | def apply(from: String, name: String, description: String, method: Method, index: Int, clazz: Class[_]): ParamDef = { 17 | val converter = if(from == "inject") DummyConverter else { 18 | simpleTypeConverter(name, clazz).getOrElse { 19 | if(clazz == classOf[Seq[_]] && isSimpleContainerType(method, index, clazz)) { 20 | new ParamConverter.SimpleSeqConverter(name, getWrappedTypeConverter(name, method, index)) 21 | } else if(clazz.isArray && isSimpleContainerType(method, index, clazz)) { 22 | if(clazz.getComponentType == classOf[Byte]){ 23 | new ParamConverter.ByteArrayConverter(name) 24 | } else { 25 | new ParamConverter.SimpleArrayConverter(name, 26 | simpleTypeConverter(name, clazz.getComponentType).getOrElse(new ParamConverter.StringConverter(name)) 27 | ) 28 | } 29 | } else if(clazz == classOf[Option[_]]){ 30 | new ParamConverter.OptionConverter(name, getWrappedTypeConverter(name, method, index)) 31 | } else { 32 | new ParamConverter.JsonConverter(name, clazz) 33 | } 34 | } 35 | } 36 | 37 | from.toLowerCase() match { 38 | case "query" => QueryParam(name, description, converter) 39 | case "path" => PathParam(name, description, converter) 40 | case "header" => HeaderParam(name, description, converter) 41 | case "body" => BodyParam(name, description, clazz, converter) 42 | case "inject" => InjectParam(name, description, clazz, converter) 43 | } 44 | } 45 | 46 | def isSimpleType(clazz: Class[_]): Boolean = { 47 | clazz == classOf[String] || clazz == classOf[Int] || clazz == classOf[Long] || clazz == classOf[Boolean] || clazz == classOf[Byte] 48 | } 49 | 50 | def isSimpleContainerType(method: Method, index: Int, clazz: Class[_]): Boolean = { 51 | if(clazz == classOf[Option[_]]){ 52 | true 53 | } else if(clazz == classOf[Seq[_]]) { 54 | val t = ReflectionUtils.getWrappedTypeOfMethodArgument(method, index) 55 | t.exists(isSimpleType) 56 | } else if(clazz.isArray){ 57 | isSimpleType(clazz.getComponentType) 58 | } else { 59 | false 60 | } 61 | } 62 | 63 | protected def getWrappedTypeConverter(name: String, method: java.lang.reflect.Method, index: Int): ParamConverter = { 64 | ReflectionUtils.getWrappedTypeOfMethodArgument(method, index) 65 | .flatMap { t => simpleTypeConverter(name, t) } 66 | .getOrElse(new ParamConverter.StringConverter(name)) 67 | } 68 | 69 | protected def simpleTypeConverter(name: String, clazz: Class[_]): Option[ParamConverter] = { 70 | if(clazz == classOf[String]) { 71 | Some(new ParamConverter.StringConverter(name)) 72 | } else if(clazz == classOf[Int]){ 73 | Some(new ParamConverter.IntConverter(name)) 74 | } else if(clazz == classOf[Long]){ 75 | Some(new ParamConverter.LongConverter(name)) 76 | } else if(clazz == classOf[Boolean]){ 77 | Some(new ParamConverter.BooleanConverter(name)) 78 | } else { 79 | None 80 | } 81 | } 82 | 83 | case class PathParam(name: String, description: String, converter: ParamConverter) extends ParamDef 84 | case class QueryParam(name: String, description: String, converter: ParamConverter) extends ParamDef 85 | case class HeaderParam(name: String, description: String, converter: ParamConverter) extends ParamDef 86 | case class BodyParam(name: String, description: String, clazz: Class[_], converter: ParamConverter) extends ParamDef 87 | case class InjectParam(name: String, description: String, clazz: Class[_], converter: ParamConverter) extends ParamDef 88 | 89 | } 90 | 91 | -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/servlet/FileResourceServlet.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty.servlet 2 | 3 | import java.io.{FileInputStream, InputStream} 4 | 5 | /** 6 | * A base class for servlets that provide resources on the file system as web contents. 7 | */ 8 | abstract class FileResourceServlet(basePath: String) extends ResourceServlet(basePath) { 9 | 10 | override protected def getResource(path: String): InputStream = { 11 | new FileInputStream(basePath + path) 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/servlet/InitializeListener.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty.servlet 2 | 3 | import java.util.EnumSet 4 | import javax.servlet.{DispatcherType, ServletContextEvent, ServletContextListener} 5 | import javax.servlet.annotation.WebListener 6 | 7 | import com.github.takezoe.resty._ 8 | import com.github.takezoe.resty.util.StringUtils 9 | import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet 10 | 11 | @WebListener 12 | class InitializeListener extends ServletContextListener { 13 | 14 | override def contextInitialized(sce: ServletContextEvent): Unit = { 15 | val context = sce.getServletContext 16 | 17 | // Initialize HttpClientSupport support 18 | HttpClientSupport.initialize(sce) 19 | if("enable" == StringUtils.trim(context.getInitParameter(ConfigKeys.ZipkinSupport))){ 20 | context.addFilter("ZipkinBraveFilter", new ZipkinBraveFilter()).setAsyncSupported(true) 21 | context.getFilterRegistration("ZipkinBraveFilter") 22 | .addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*") 23 | 24 | 25 | } 26 | 27 | // Initialize Swagger support 28 | if("enable" == StringUtils.trim(context.getInitParameter(ConfigKeys.SwaggerSupport))){ 29 | Resty.register(new SwaggerController()) 30 | context.addServlet("SwaggerUIServlet", new SwaggerUIServlet()) 31 | context.getServletRegistration("SwaggerUIServlet").addMapping("/swagger-ui/*") 32 | } 33 | 34 | // Initialize Hystrix support 35 | HystrixSupport.initialize(sce) 36 | if("enable" == StringUtils.trim(context.getInitParameter(ConfigKeys.HystrixSupport))){ 37 | context.addServlet("HystrixMetricsStreamServlet", new HystrixMetricsStreamServlet()) 38 | context.getServletRegistration("HystrixMetricsStreamServlet").addMapping("/hystrix.stream") 39 | } 40 | 41 | // Initialize WebJars support 42 | if("enable" == StringUtils.trim(context.getInitParameter(ConfigKeys.WebJarsSupport))){ 43 | val path = StringUtils.trim(context.getInitParameter(ConfigKeys.WebJarsPath)) 44 | context.addServlet("WebJarsServlet", new WebJarsServlet()) 45 | context.getServletRegistration("WebJarsServlet").addMapping(path) 46 | } 47 | 48 | // Initialize CORS support 49 | CORSSupport.initialize(sce) 50 | } 51 | 52 | override def contextDestroyed(sce: ServletContextEvent): Unit = { 53 | HttpClientSupport.shutdown(sce) 54 | HystrixSupport.shutdown(sce) 55 | } 56 | 57 | } 58 | 59 | object ConfigKeys { 60 | val ZipkinSupport = "resty.zipkin" 61 | val ZipkinServerUrl = "resty.zipkin.server.url" 62 | val ZipkinSampleRate = "resty.zipkin.sample.rate" 63 | val ZipkinServiceName = "resty.zipkin.service.name" 64 | val SwaggerSupport = "resty.swagger" 65 | val HystrixSupport = "resty.hystrix" 66 | val WebJarsSupport = "resty.webjars" 67 | val WebJarsPath = "resty.webjars.path" 68 | val CORSSupport = "resty.cors" 69 | val CORSAllowedOrigins = "resty.cors.allowedOrigins" 70 | val CORSAllowedMethods = "resty.cors.allowedMethods" 71 | val CORSAllowedHeaders = "resty.cors.allowedHeaders" 72 | val CORSPreflightMaxAge = "resty.cors.preflightMaxAge" 73 | val CORSAllowCredentials = "resty.cors.allowCredentials" 74 | } 75 | -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/servlet/ResourceServlet.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty.servlet 2 | 3 | import java.io.InputStream 4 | import java.net.URLConnection 5 | import javax.servlet.http.{HttpServlet, HttpServletRequest, HttpServletResponse} 6 | 7 | import org.apache.commons.io.IOUtils 8 | 9 | import scala.util.control.Exception 10 | 11 | /** 12 | * A base class for servlets that provide resources on the classpath as web contents. 13 | */ 14 | abstract class ResourceServlet(basePath: String) extends HttpServlet { 15 | 16 | protected override def doGet(request: HttpServletRequest, response: HttpServletResponse): Unit = { 17 | val resourcePath = request.getRequestURI.substring((request.getContextPath + request.getServletPath).length) 18 | val path = if(resourcePath.endsWith("/")) resourcePath + "index.html" else resourcePath 19 | 20 | val in = getResource(path) 21 | if(in != null){ 22 | try { 23 | val content = IOUtils.toByteArray(in) 24 | val out = response.getOutputStream 25 | 26 | response.setContentType(getContentType(path.toLowerCase)) 27 | response.setContentLength(content.length) 28 | out.write(content) 29 | 30 | } finally { 31 | Exception.ignoring(classOf[Exception]){ 32 | in.close() 33 | } 34 | } 35 | } 36 | } 37 | 38 | protected def getResource(path: String): InputStream = { 39 | Thread.currentThread.getContextClassLoader.getResourceAsStream(basePath + path) 40 | } 41 | 42 | protected def getContentType(path: String): String = { 43 | if(path.endsWith(".html")){ 44 | "text/html; charset=UTF-8" 45 | } else if(path.endsWith(".css")){ 46 | "text/css; charset=UTF-8" 47 | } else if(path.endsWith(".js")){ 48 | "text/javascript; charset=UTF-8" 49 | } else { 50 | val contentType = URLConnection.guessContentTypeFromName(path) 51 | if(contentType == null){ 52 | "application/octet-stream" 53 | } else { 54 | contentType 55 | } 56 | } 57 | } 58 | 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/servlet/RestyServlet.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty.servlet 2 | 3 | import javax.servlet.{ServletRequest, ServletResponse} 4 | import javax.servlet.annotation.WebServlet 5 | import javax.servlet.http.{HttpServlet, HttpServletRequest, HttpServletResponse} 6 | 7 | import com.github.takezoe.resty.{CORSSupport, RestyKernel} 8 | 9 | @WebServlet(name="RestyServlet", urlPatterns=Array("/*"), asyncSupported = true) 10 | class RestyServlet extends HttpServlet with RestyKernel { 11 | 12 | protected override def doGet(request: HttpServletRequest, response: HttpServletResponse): Unit = { 13 | processAction(request, response, "get") 14 | } 15 | 16 | protected override def doPost(request: HttpServletRequest, response: HttpServletResponse): Unit = { 17 | processAction(request, response, "post") 18 | } 19 | 20 | protected override def doPut(request: HttpServletRequest, response: HttpServletResponse): Unit = { 21 | processAction(request, response, "put") 22 | } 23 | 24 | protected override def doDelete(request: HttpServletRequest, response: HttpServletResponse): Unit = { 25 | processAction(request, response, "delete") 26 | } 27 | 28 | protected override def doOptions(request: HttpServletRequest, response: HttpServletResponse): Unit = { 29 | processAction(request, response, "option") 30 | } 31 | 32 | 33 | override def service(req: ServletRequest, res: ServletResponse): Unit = { 34 | val request = req.asInstanceOf[HttpServletRequest] 35 | val response = res.asInstanceOf[HttpServletResponse] 36 | 37 | CORSSupport.processCORSRequest(request).foreach { allowed => 38 | CORSSupport.setCORSResponseHeaders(response, allowed) 39 | if(allowed.isPreflight){ 40 | return 41 | } 42 | } 43 | 44 | super.service(req, res) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/servlet/SwaggerUIServlet.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty.servlet 2 | 3 | class SwaggerUIServlet extends ResourceServlet("/public/vendors/swagger-ui") 4 | -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/servlet/WebJarsServlet.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty.servlet 2 | 3 | import java.io.InputStream 4 | 5 | import org.webjars.WebJarAssetLocator 6 | 7 | class WebJarsServlet extends ResourceServlet("") { 8 | 9 | private val locator = new WebJarAssetLocator() 10 | 11 | override protected def getResource(path: String): InputStream = { 12 | Thread.currentThread.getContextClassLoader.getResourceAsStream(locator.getFullPath(path)) 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/servlet/ZipkinBraveFilter.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty.servlet 2 | 3 | import javax.servlet._ 4 | 5 | import brave.servlet.TracingFilter 6 | import com.github.takezoe.resty.HttpClientSupport 7 | 8 | class ZipkinBraveFilter extends Filter { 9 | 10 | protected val filter = TracingFilter.create(HttpClientSupport.tracing) 11 | 12 | override def init(filterConfig: FilterConfig): Unit = filter.init(filterConfig) 13 | 14 | override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = filter.doFilter(request, response, chain) 15 | 16 | override def destroy(): Unit = filter.destroy() 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/util/JsonUtils.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty.util 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude.Include 4 | import com.fasterxml.jackson.databind._ 5 | import com.fasterxml.jackson.module.scala.DefaultScalaModule 6 | //import com.fasterxml.jackson.databind.module.SimpleModule 7 | //import com.fasterxml.jackson.core.{JsonGenerator, JsonParser, Version} 8 | //import org.joda.time.DateTime 9 | //import org.joda.time.format.DateTimeFormat 10 | //import scala.reflect.ClassTag 11 | 12 | object JsonUtils { 13 | 14 | val mapper = new ObjectMapper() 15 | mapper.enable(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) 16 | mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) 17 | mapper.setSerializationInclusion(Include.NON_NULL) 18 | mapper.registerModule(DefaultScalaModule) 19 | // TODO Support conversion for Java8 Date & Time API 20 | // mapper.registerModule(new SimpleModule("MyModule", Version.unknownVersion()) 21 | // .addSerializer(classOf[DateTime], new JsonSerializer[DateTime] { 22 | // override def serialize(value: DateTime, generator: JsonGenerator, provider: SerializerProvider): Unit = { 23 | // val formatter = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").withZoneUTC() 24 | // generator.writeString(formatter.print(value)) 25 | // } 26 | // }) 27 | // .addDeserializer(classOf[DateTime], new JsonDeserializer[DateTime](){ 28 | // override def deserialize(parser: JsonParser, context: DeserializationContext): DateTime = { 29 | // val formatter = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").withZoneUTC() 30 | // formatter.parseDateTime(if(parser.getValueAsString != null) parser.getValueAsString else parser.nextTextValue) 31 | // } 32 | // }) 33 | // ) 34 | 35 | def serialize(doc: AnyRef): String = mapper.writeValueAsString(doc) 36 | 37 | def deserialize(json: String, c: Class[_]): AnyRef = mapper.readValue(json, c).asInstanceOf[AnyRef] 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/util/ReflectionUtils.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty.util 2 | 3 | import java.lang.reflect.Member 4 | 5 | import org.json4s.scalap.scalasig._ 6 | 7 | import scala.annotation.tailrec 8 | 9 | object ReflectionUtils { 10 | 11 | def getWrappedTypeOfMethod[T](method: java.lang.reflect.Method)(implicit m: Manifest[T]): Option[Class[_]] = { 12 | 13 | def findArgType(c: ClassSymbol, s: MethodSymbol, typeArgIdx: Int): Class[_] = { 14 | val t = s.infoType match { 15 | case MethodType(TypeRefType(_, _, args), _) => args(0) 16 | } 17 | toClass(t match { 18 | case TypeRefType(_, symbol, _) => symbol 19 | case x => throw new Exception("Unexpected type info " + x) 20 | }) 21 | } 22 | 23 | ScalaSigParser.parse(getTopLevelClass(method)).flatMap { scalaSig => 24 | val syms = scalaSig.topLevelClasses.flatMap(getAllClassSymbols) 25 | val _type = syms.collectFirst { 26 | case c if (c.path == method.getDeclaringClass.getName.replace('$', '.')) => 27 | findMethodSymbol(c, method.getName).map { f => findArgType(c, f, 0) } 28 | } 29 | _type.flatten 30 | } 31 | } 32 | 33 | def getWrappedTypeOfMethodArgument[T](method: java.lang.reflect.Method, index: Int)(implicit m: Manifest[T]): Option[Class[_]] = { 34 | 35 | def findArgType(c: ClassSymbol, s: MethodSymbol, typeArgIdx: Int): Class[_] = { 36 | val t = s.infoType match { 37 | case MethodType(TypeRefType(_, _, args), paramSymbols) => { 38 | paramSymbols(typeArgIdx) match { 39 | case sym: MethodSymbol => sym.infoType match { 40 | case TypeRefType(_, _, args) => args(0) 41 | } 42 | } 43 | } 44 | } 45 | toClass(t match { 46 | case TypeRefType(_, symbol, _) => symbol 47 | case x => throw new Exception("Unexpected type info " + x) 48 | }) 49 | } 50 | 51 | ScalaSigParser.parse(getTopLevelClass(method)).flatMap { scalaSig => 52 | val syms = scalaSig.topLevelClasses.flatMap(getAllClassSymbols) 53 | val _type = syms.collectFirst { 54 | case c if (c.path == method.getDeclaringClass.getName.replace('$', '.')) => 55 | findMethodSymbol(c, method.getName).map { f => findArgType(c, f, index) } 56 | } 57 | _type.flatten 58 | } 59 | } 60 | 61 | def getWrappedTypeOfField[T](field: java.lang.reflect.Field)(implicit m: Manifest[T]): Option[Class[_]] = { 62 | 63 | def findArgType(c: ClassSymbol, s: MethodSymbol, typeArgIdx: Int): Class[_] = { 64 | val t = s.infoType match { 65 | case NullaryMethodType(TypeRefType(_, _, args)) => args(typeArgIdx) 66 | } 67 | toClass(t match { 68 | case TypeRefType(_, symbol, _) => symbol 69 | case x => throw new Exception("Unexpected type info " + x) 70 | }) 71 | } 72 | 73 | ScalaSigParser.parse(getTopLevelClass(field)).flatMap { scalaSig => 74 | val syms = scalaSig.topLevelClasses.flatMap(getAllClassSymbols) 75 | val _type = syms.collectFirst { 76 | case c if (c.path == field.getDeclaringClass.getName.replace('$', '.')) => 77 | findMethodSymbol(c, field.getName).map { f => findArgType(c, f, 0) } 78 | } 79 | _type.flatten 80 | } 81 | } 82 | 83 | protected def getAllClassSymbols(sym: ClassSymbol): Seq[ClassSymbol] = { 84 | sym.children.collect { case c: ClassSymbol => 85 | getAllClassSymbols(c) 86 | }.flatten :+ sym 87 | } 88 | 89 | protected def getTopLevelClass(member: Member): Class[_] = { 90 | member.getDeclaringClass match { 91 | case c if c.getName.contains("$") => getTopLevelClass(c) 92 | case c => c 93 | } 94 | } 95 | 96 | @tailrec 97 | protected def getTopLevelClass(clazz: Class[_]): Class[_] = { 98 | clazz.getDeclaringClass match { 99 | case c if c.getName.contains("$") => getTopLevelClass(c) 100 | case c => c 101 | } 102 | } 103 | 104 | protected def findMethodSymbol(c: ClassSymbol, name: String): Option[MethodSymbol] = 105 | (c.children collect { case m: MethodSymbol if m.name == name => m }).headOption 106 | 107 | protected def toClass(s: Symbol) = s.path match { 108 | case "scala.Short" => classOf[Short] 109 | case "scala.Int" => classOf[Int] 110 | case "scala.Long" => classOf[Long] 111 | case "scala.Boolean" => classOf[Boolean] 112 | case "scala.Float" => classOf[Float] 113 | case "scala.Double" => classOf[Double] 114 | case "scala.Byte" => classOf[Byte] 115 | case "scala.Predef.String" => classOf[String] 116 | case x => Class.forName(if(s.parent.exists(_.isInstanceOf[ClassSymbol])) x.replaceFirst("\\.([^.]+?)$", "\\$$1") else x) 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/util/ScaladocUtils.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty.util 2 | 3 | import java.lang.reflect.Method 4 | 5 | import scala.collection.mutable.ListBuffer 6 | 7 | object ScaladocUtils { 8 | 9 | def getScaladoc(clazz: Class[_]): Option[Scaladoc] = { 10 | val scaladoc = clazz.getAnnotation(classOf[com.github.takezoe.scaladoc.Scaladoc]) 11 | if(scaladoc != null){ 12 | Some(parseScaladoc(scaladoc.value())) 13 | } else { 14 | None 15 | } 16 | } 17 | 18 | def getScaladoc(method: Method): Option[Scaladoc] = { 19 | val scaladoc = method.getAnnotation(classOf[com.github.takezoe.scaladoc.Scaladoc]) 20 | if(scaladoc != null){ 21 | Some(parseScaladoc(scaladoc.value())) 22 | } else { 23 | None 24 | } 25 | } 26 | 27 | def isDeprecated(method: Method, scaladoc: Option[Scaladoc]): Boolean = { 28 | if(method.getAnnotation(classOf[Deprecated]) != null){ 29 | true 30 | } else { 31 | scaladoc.map { scaladoc => 32 | scaladoc.tags.exists(_.name == "@deprecated") 33 | }.getOrElse(false) 34 | } 35 | } 36 | 37 | def getParamDescription(name: String, scaladoc: Option[Scaladoc]): Option[String] = for { 38 | doc <- scaladoc 39 | tag <- doc.tags.find(tag => tag.name == "@param" && tag.paramName.contains(name)) 40 | desc <- tag.paramDescription 41 | } yield desc 42 | 43 | def getReturnDescription(scaladoc: Option[Scaladoc]): Option[String] = for { 44 | doc <- scaladoc 45 | tag <- doc.tags.find(_.name == "@return") 46 | desc <- tag.description 47 | } yield desc 48 | 49 | def parseScaladoc(scaladoc: String): Scaladoc = { 50 | val sb = new StringBuilder() 51 | val tags = new ListBuffer[Tag]() 52 | var tag: Tag = null 53 | 54 | scaladoc.trim().replaceFirst("^/\\*+", "").replaceFirst("\\*+/$", "").split("\n").foreach { line => 55 | val s = line.trim().replaceFirst("^\\*\\s*", "") 56 | if(s.startsWith("@")){ 57 | if(tag != null){ 58 | tags += tag 59 | } 60 | s.split("\\s", 2) match { 61 | case Array(tagName, description) => 62 | tag = Tag(tagName, Some(description)) 63 | case Array(tagName) => 64 | tag = Tag(tagName, None) 65 | } 66 | } else if(tag != null){ 67 | tag = tag.copy(description = Some(tag.description.getOrElse("") + "\n" + s)) 68 | } else { 69 | sb.append(s + "\n") 70 | } 71 | } 72 | if(tag != null){ 73 | tags += tag 74 | } 75 | 76 | Scaladoc(sb.toString().trim(), tags) 77 | } 78 | 79 | case class Scaladoc(description: String, tags: Seq[Tag]) 80 | case class Tag(name: String, description: Option[String]){ 81 | lazy val paramName: Option[String] = { 82 | description.flatMap { case s => 83 | s.split("\\s", 2) match { 84 | case Array(paramName, paramDescription) => 85 | Some(paramName) 86 | case Array(paramName) => 87 | Some(paramName) 88 | } 89 | } 90 | } 91 | lazy val paramDescription: Option[String] = { 92 | description.flatMap { case s => 93 | s.split("\\s", 2) match { 94 | case Array(paramName, paramDescription) => 95 | Some(paramDescription) 96 | case Array(paramName) => 97 | None 98 | } 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/scala/com/github/takezoe/resty/util/StringUtils.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty.util 2 | 3 | object StringUtils { 4 | 5 | def trim(value: String): String = Option(value).map(_.trim).getOrElse("") 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/test/scala/com/github/takezoe/resty/RandomRequestTargetSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty 2 | 3 | import okhttp3.{OkHttpClient, Request} 4 | import org.scalatest._ 5 | 6 | import scala.collection.mutable.ListBuffer 7 | 8 | class RandomRequestTargetSpec extends FunSuite { 9 | 10 | test("selects a url from given urls randomly"){ 11 | val urls = Seq( 12 | "http://localhost:8080", 13 | "http://localhost:8081", 14 | "http://localhost:8082" 15 | ) 16 | 17 | val result = new ListBuffer[Option[String]]() 18 | 19 | val target = new RandomRequestTarget(urls, HttpClientConfig()){ 20 | override def execute[T](httpClient: OkHttpClient, configurer: (String, Request.Builder) => Unit, clazz: Class[_]): Either[ErrorModel, T] = { 21 | result += nextTarget.map { case target: SimpleRequestTarget => target.url } 22 | null 23 | } 24 | } 25 | 26 | for(i <- 1 to 100){ 27 | target.execute(null, null, null) 28 | } 29 | 30 | val count1 = result.filter(_ == Some("http://localhost:8080")).length 31 | val count2 = result.filter(_ == Some("http://localhost:8081")).length 32 | val count3 = result.filter(_ == Some("http://localhost:8082")).length 33 | 34 | assert(count1 >= 20 && count1 <= 40) 35 | assert(count2 >= 20 && count2 <= 40) 36 | assert(count3 >= 20 && count3 <= 40) 37 | assert(count1 + count2 + count3 == 100) 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/test/scala/com/github/takezoe/resty/util/ReflectionUtilsSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.takezoe.resty.util 2 | 3 | import java.lang.reflect.{Field, Method} 4 | 5 | import org.scalatest._ 6 | 7 | case class ReflectionUtilsTest1(optionField: Option[String], seqField: Seq[String], nestField: Option[ReflectionUtilsNestTest1]) { 8 | def optionMethod(): Option[String] = None 9 | def seqMethod(): Seq[String] = Nil 10 | def nestMethod(): Option[ReflectionUtilsNestTest1] = None 11 | } 12 | 13 | case class ReflectionUtilsNestTest1(field: String) 14 | 15 | class ReflectionUtilsSpec extends FunSuite { 16 | 17 | case class ReflectionUtilsTest2(optionField: Option[String], seqField: Seq[String], nestField: Option[ReflectionUtilsNestTest2]) { 18 | def optionMethod(): Option[String] = None 19 | def seqMethod(): Seq[String] = Nil 20 | def nestMethod(): Option[ReflectionUtilsNestTest2] = None 21 | } 22 | case class ReflectionUtilsNestTest2(field: String) 23 | 24 | test("Get generic type of method return type"){ 25 | val clazz = classOf[ReflectionUtilsTest1] 26 | 27 | { 28 | val method = getMethod(clazz, "optionMethod") 29 | val result = ReflectionUtils.getWrappedTypeOfMethod(method) 30 | 31 | assert(result == Some(classOf[String])) 32 | } 33 | 34 | { 35 | val method = getMethod(clazz, "seqMethod") 36 | val result = ReflectionUtils.getWrappedTypeOfMethod(method) 37 | 38 | assert(result == Some(classOf[String])) 39 | } 40 | 41 | { 42 | val method = getMethod(clazz, "nestMethod") 43 | val result = ReflectionUtils.getWrappedTypeOfMethod(method) 44 | 45 | assert(result == Some(classOf[ReflectionUtilsNestTest1])) 46 | } 47 | } 48 | 49 | test("Get generic type of field type"){ 50 | val clazz = classOf[ReflectionUtilsTest1] 51 | 52 | { 53 | val field = getField(clazz, "optionField") 54 | val result = ReflectionUtils.getWrappedTypeOfField(field) 55 | 56 | assert(result == Some(classOf[String])) 57 | } 58 | 59 | { 60 | val field = getField(clazz, "seqField") 61 | val result = ReflectionUtils.getWrappedTypeOfField(field) 62 | 63 | assert(result == Some(classOf[String])) 64 | } 65 | 66 | { 67 | val field = getField(clazz, "nestField") 68 | val result = ReflectionUtils.getWrappedTypeOfField(field) 69 | 70 | assert(result == Some(classOf[ReflectionUtilsNestTest1])) 71 | } 72 | } 73 | 74 | test("Get generic type of method type of inner class"){ 75 | val clazz = classOf[ReflectionUtilsTest2] 76 | 77 | { 78 | val method = getMethod(clazz, "optionMethod") 79 | val result = ReflectionUtils.getWrappedTypeOfMethod(method) 80 | 81 | assert(result == Some(classOf[String])) 82 | } 83 | 84 | { 85 | val method = getMethod(clazz, "seqMethod") 86 | val result = ReflectionUtils.getWrappedTypeOfMethod(method) 87 | 88 | assert(result == Some(classOf[String])) 89 | } 90 | 91 | { 92 | val method = getMethod(clazz, "nestMethod") 93 | val result = ReflectionUtils.getWrappedTypeOfMethod(method) 94 | 95 | assert(result == Some(classOf[ReflectionUtilsNestTest2])) 96 | } 97 | } 98 | 99 | test("Get generic type of field type of inner class"){ 100 | val clazz = classOf[ReflectionUtilsTest2] 101 | 102 | { 103 | val field = getField(clazz, "optionField") 104 | val result = ReflectionUtils.getWrappedTypeOfField(field) 105 | 106 | assert(result == Some(classOf[String])) 107 | } 108 | 109 | { 110 | val field = getField(clazz, "seqField") 111 | val result = ReflectionUtils.getWrappedTypeOfField(field) 112 | 113 | assert(result == Some(classOf[String])) 114 | } 115 | 116 | { 117 | val field = getField(clazz, "nestField") 118 | val result = ReflectionUtils.getWrappedTypeOfField(field) 119 | 120 | assert(result == Some(classOf[ReflectionUtilsNestTest2])) 121 | } 122 | } 123 | 124 | private def getField(clazz: Class[_], name: String): Field = clazz.getDeclaredFields.find(_.getName == name).get 125 | private def getMethod(clazz: Class[_], name: String): Method = clazz.getDeclaredMethods.find(_.getName == name).get 126 | 127 | } 128 | -------------------------------------------------------------------------------- /swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takezoe/resty/a96025f5742a78fca2ed63ca92019af0b27945c8/swagger.png --------------------------------------------------------------------------------