├── .travis.yml ├── binder-swagger-java.png ├── .gitignore ├── src ├── main │ └── java │ │ └── com │ │ └── github │ │ └── tminglei │ │ ├── swagger │ │ ├── route │ │ │ ├── package-info.java │ │ │ ├── RouteFactoryImpl.java │ │ │ ├── RouteFactory.java │ │ │ ├── Router.java │ │ │ ├── PathStaticElement.java │ │ │ ├── PathSplatParamElement.java │ │ │ ├── Route.java │ │ │ ├── PathNamedParamElement.java │ │ │ ├── PathElementComparator.java │ │ │ ├── PathElement.java │ │ │ ├── RouteHelper.java │ │ │ ├── TreeRouterImpl.java │ │ │ ├── TreeNode.java │ │ │ └── RouteImpl.java │ │ ├── fake │ │ │ ├── ConstDataProvider.java │ │ │ ├── DataWriter.java │ │ │ ├── ParamDataProvider.java │ │ │ ├── ListDataProvider.java │ │ │ ├── MapDataProvider.java │ │ │ ├── DataProvider.java │ │ │ ├── ObjectDataProvider.java │ │ │ ├── OrDataProvider.java │ │ │ ├── AbstractDataProvider.java │ │ │ ├── DataWriterImpl.java │ │ │ └── DataProviders.java │ │ ├── SimpleUtils.java │ │ ├── bind │ │ │ ├── MParamBuilder.java │ │ │ ├── MappingConverter.java │ │ │ └── Attachment.java │ │ ├── ExOperation.java │ │ ├── SharingHolder.java │ │ ├── SwaggerContext.java │ │ └── SwaggerFilter.java │ │ └── bind │ │ └── OptionsOps.java └── test │ └── java │ └── com │ └── github │ └── tminglei │ └── swagger │ ├── SimpleUtilsTest.java │ ├── SwaggerScanTest.java │ ├── route │ ├── TestRouteHelper.java │ └── RouterContractTest.java │ ├── fake │ └── DataProvidersTest.java │ └── bind │ └── MappingConverterImplTest.java ├── example └── java-jaxrs │ ├── src │ └── main │ │ ├── resources │ │ └── logback.xml │ │ ├── java │ │ └── com │ │ │ └── example │ │ │ ├── exception │ │ │ ├── ApiException.java │ │ │ ├── NotFoundException.java │ │ │ └── BadRequestException.java │ │ │ ├── data │ │ │ ├── StoreData.java │ │ │ ├── UserData.java │ │ │ ├── PetData.java │ │ │ └── H2DB.java │ │ │ ├── model │ │ │ └── ApiResponse.java │ │ │ ├── resource │ │ │ ├── SampleExceptionMapper.java │ │ │ ├── PetStoreResource.java │ │ │ ├── PetResource.java │ │ │ └── UserResource.java │ │ │ └── Bootstrap.java │ │ └── webapp │ │ ├── WEB-INF │ │ └── web.xml │ │ └── index.html │ ├── README.md │ └── pom.xml ├── LICENSE ├── pom.xml └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 -------------------------------------------------------------------------------- /binder-swagger-java.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tminglei/binder-swagger-java/HEAD/binder-swagger-java.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .settings 3 | .vscode 4 | 5 | .classpath 6 | .project 7 | 8 | target/ 9 | binder-swagger-java.iml 10 | 11 | example/java-jaxrs/target/ 12 | example/java-jaxrs/swagger-java-sample.iml 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/route/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The codes source from [lantunes/routd](https://github.com/lantunes/routd), 3 | * copied it here, since: 4 | * 1) `routd` is not actively maintained 5 | * 2) we can modify it freely 6 | */ 7 | package com.github.tminglei.swagger.route; -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/fake/ConstDataProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger.fake; 2 | 3 | /** 4 | * Created by minglei on 4/15/17. 5 | */ 6 | public class ConstDataProvider extends AbstractDataProvider implements DataProvider { 7 | private Object value; 8 | 9 | public ConstDataProvider(Object value) { 10 | this(value, "root"); 11 | } 12 | public ConstDataProvider(Object value, String name) { 13 | super(name); 14 | this.value = value; 15 | } 16 | 17 | @Override 18 | protected Object create() { 19 | return value; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example/java-jaxrs/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/route/RouteFactoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger.route; 2 | 3 | import com.github.tminglei.swagger.fake.DataProvider; 4 | import io.swagger.models.HttpMethod; 5 | 6 | /** 7 | * Created by minglei on 4/17/17. 8 | */ 9 | public class RouteFactoryImpl implements RouteFactory { 10 | 11 | @Override 12 | public Route create(HttpMethod method, 13 | String pathPattern, 14 | boolean implemented, 15 | DataProvider dataProvider) { 16 | return new RouteImpl(method, pathPattern, implemented, dataProvider); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/fake/DataWriter.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger.fake; 2 | 3 | import java.io.IOException; 4 | import java.io.Writer; 5 | 6 | /** 7 | * Used to write data to target writer with specified format 8 | */ 9 | public interface DataWriter { 10 | 11 | /** 12 | * transform inputting data to target format, and write it to target writer 13 | * 14 | * @param writer target writer 15 | * @param format target format 16 | * @param provider data provider 17 | * @throws IOException 18 | */ 19 | void write(Writer writer, String format, DataProvider provider) throws IOException; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/route/RouteFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger.route; 2 | 3 | import com.github.tminglei.swagger.fake.DataProvider; 4 | import io.swagger.models.HttpMethod; 5 | 6 | /** 7 | * route factory 8 | */ 9 | public interface RouteFactory { 10 | 11 | /** 12 | * 13 | * @param method http method 14 | * @param pathPattern url pattern 15 | * @param implemented whether it is implemented 16 | * @param dataProvider data provider used to generate fake data 17 | * @return route implementation object 18 | */ 19 | Route create(HttpMethod method, 20 | String pathPattern, 21 | boolean implemented, 22 | DataProvider dataProvider); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/fake/ParamDataProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger.fake; 2 | 3 | /** 4 | * Created by minglei on 4/15/17. 5 | */ 6 | public class ParamDataProvider extends AbstractDataProvider implements DataProvider { 7 | private String paramKey; 8 | 9 | public ParamDataProvider(String paramKey) { 10 | this(paramKey, "root"); 11 | } 12 | public ParamDataProvider(String paramKey, String name) { 13 | super(name); 14 | this.paramKey = paramKey; 15 | } 16 | 17 | @Override 18 | public Object get() { 19 | return create(); // ignore the required's value 20 | } 21 | 22 | @Override 23 | protected Object create() { 24 | return params != null ? params.get(paramKey) 25 | : null; 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/route/Router.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger.route; 2 | 3 | import io.swagger.models.HttpMethod; 4 | 5 | /** 6 | * Used to locate request path to matched route object 7 | */ 8 | public interface Router { 9 | 10 | void add(Route route); 11 | 12 | /** 13 | * Returns a Route that matches the given URL path. 14 | * Note that the path may be expected to be an undecoded 15 | * URL path. This URL encoding requirement is determined 16 | * by the Router implementation. 17 | * 18 | * @param method http method 19 | * @param path a decoded or undecoded URL path, 20 | * depending on the Router implementation 21 | * @return the matching route, or null if none is found 22 | */ 23 | Route route(HttpMethod method, String path); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/fake/ListDataProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger.fake; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * Created by minglei on 4/15/17. 8 | */ 9 | public class ListDataProvider extends AbstractDataProvider implements DataProvider { 10 | private DataProvider itemProvider; 11 | 12 | public ListDataProvider(DataProvider itemProvider) { 13 | this(itemProvider, "root"); 14 | } 15 | public ListDataProvider(DataProvider itemProvider, String name) { 16 | super(name); 17 | this.itemProvider = itemProvider; 18 | } 19 | 20 | @Override 21 | protected Object create() { 22 | List list = new ArrayList(); 23 | for (int i = 0; i < 5; i++) { 24 | itemProvider.setRequestParams(params); 25 | list.add(itemProvider.get()); 26 | } 27 | return list; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/fake/MapDataProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger.fake; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * Created by minglei on 4/15/17. 8 | */ 9 | public class MapDataProvider extends AbstractDataProvider implements DataProvider { 10 | private DataProvider valueProvider; 11 | 12 | public MapDataProvider(DataProvider valueProvider) { 13 | this(valueProvider, "root"); 14 | } 15 | public MapDataProvider(DataProvider valueProvider, String name) { 16 | super(name); 17 | this.valueProvider = valueProvider; 18 | } 19 | 20 | @Override 21 | protected Object create() { 22 | Map map = new HashMap(); 23 | for (int i = 0; i < 5; i++) { 24 | valueProvider.setRequestParams(params); 25 | map.put("key"+i, valueProvider.get()); 26 | } 27 | return map; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/fake/DataProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger.fake; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * Used to generate fake data 7 | */ 8 | public interface DataProvider { 9 | 10 | /** 11 | * 12 | * @param params flattened request param map 13 | */ 14 | default void setRequestParams(Map params) {} 15 | 16 | /** 17 | * 18 | * @param required whether target data is required 19 | */ 20 | default void setRequired(boolean required) {} 21 | 22 | /** 23 | * 24 | * @return name of the data 25 | */ 26 | default String name() { return null; } 27 | 28 | /** 29 | * 30 | * @return data object or null 31 | */ 32 | Object get(); 33 | 34 | 35 | ///--- 36 | 37 | default DataProvider or(DataProvider other) { 38 | return new OrDataProvider(this, other); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/route/PathStaticElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 BigTesting.org 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.tminglei.swagger.route; 17 | 18 | /** 19 | * 20 | * @author Luis Antunes 21 | */ 22 | public class PathStaticElement extends PathElement { 23 | 24 | public PathStaticElement(String name, int index) { 25 | super(name, index); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/fake/ObjectDataProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger.fake; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * Created by minglei on 4/15/17. 8 | */ 9 | public class ObjectDataProvider extends AbstractDataProvider implements DataProvider { 10 | private Map fields; 11 | 12 | public ObjectDataProvider(Map fields) { 13 | this(fields, "root"); 14 | } 15 | public ObjectDataProvider(Map fields, String name) { 16 | super(name); 17 | this.fields = fields; 18 | } 19 | 20 | @Override 21 | protected Object create() { 22 | Map valueMap = new HashMap<>(); 23 | for (String name : fields.keySet()) { 24 | fields.get(name).setRequestParams(params); 25 | valueMap.put(name, fields.get(name).get()); 26 | } 27 | return valueMap; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/route/PathSplatParamElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 BigTesting.org 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.tminglei.swagger.route; 17 | 18 | /** 19 | * 20 | * @author Luis Antunes 21 | */ 22 | public class PathSplatParamElement extends PathElement { 23 | 24 | public PathSplatParamElement(int index) { 25 | super(RouteHelper.WILDCARD, index); 26 | } 27 | } -------------------------------------------------------------------------------- /example/java-jaxrs/src/main/java/com/example/exception/ApiException.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 SmartBear Software 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.exception; 18 | 19 | public class ApiException extends Exception{ 20 | private static final long serialVersionUID = 1L; 21 | private int code; 22 | public ApiException (int code, String msg) { 23 | super(msg); 24 | this.code = code; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/bind/OptionsOps.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.bind; 2 | 3 | import com.github.tminglei.bind.spi.Constraint; 4 | import com.github.tminglei.bind.spi.ExtraConstraint; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Util methods to access `form-binder-java` [[Options]]'s non public properties 10 | */ 11 | public class OptionsOps { 12 | 13 | public static List _constraints(Options o) { 14 | return o._constraints(); 15 | } 16 | 17 | public static Options append_constraints(Options o, List constraints) { 18 | return o.append_constraints(constraints); 19 | } 20 | 21 | public static List> _extraConstraints(Options o) { 22 | return o._extraConstraints(); 23 | } 24 | 25 | public static Object _attachment(Options o) { 26 | return o._attachment(); 27 | } 28 | 29 | public static Options _attachment(Options o, Object attachment) { 30 | return o._attachment(attachment); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/java-jaxrs/src/main/java/com/example/exception/NotFoundException.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 SmartBear Software 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.exception; 18 | 19 | public class NotFoundException extends ApiException { 20 | private static final long serialVersionUID = 1L; 21 | private int code; 22 | public NotFoundException (int code, String msg) { 23 | super(code, msg); 24 | this.code = code; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/java-jaxrs/src/main/java/com/example/exception/BadRequestException.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 SmartBear Software 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.exception; 18 | 19 | public class BadRequestException extends ApiException{ 20 | private static final long serialVersionUID = 1L; 21 | private int code; 22 | public BadRequestException (int code, String msg) { 23 | super(code, msg); 24 | this.code = code; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/route/Route.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger.route; 2 | 3 | import com.github.tminglei.swagger.fake.DataProvider; 4 | import io.swagger.models.HttpMethod; 5 | 6 | import java.util.Map; 7 | 8 | /** 9 | * Used to hold some related info/objects 10 | */ 11 | public interface Route { 12 | 13 | /** 14 | * 15 | * @return binded http method 16 | */ 17 | HttpMethod getMethod(); 18 | 19 | /** 20 | * 21 | * @return binded path pattern 22 | */ 23 | String getPath(); 24 | 25 | /** 26 | * 27 | * @param path request path, should match the binded path pattern 28 | * @return params extracted from path 29 | */ 30 | Map getPathParams(String path); 31 | 32 | /** 33 | * 34 | * @return whether target operation is implemented 35 | */ 36 | boolean isImplemented(); 37 | 38 | /** 39 | * 40 | * @return data provider used to generate fake data 41 | */ 42 | DataProvider getDataProvider(); 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/fake/OrDataProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger.fake; 2 | 3 | import java.util.Map; 4 | import java.util.Optional; 5 | 6 | /** 7 | * Created by minglei on 4/23/17. 8 | */ 9 | public class OrDataProvider implements DataProvider { 10 | private DataProvider first; 11 | private DataProvider second; 12 | 13 | public OrDataProvider(DataProvider first, DataProvider second) { 14 | this.first = first; 15 | this.second = second; 16 | } 17 | 18 | @Override 19 | public void setRequestParams(Map params) { 20 | first.setRequestParams(params); 21 | second.setRequestParams(params); 22 | } 23 | 24 | @Override 25 | public void setRequired(boolean required) { 26 | first.setRequired(required); 27 | second.setRequired(required); 28 | } 29 | 30 | @Override 31 | public String name() { 32 | return first.name(); 33 | } 34 | 35 | @Override 36 | public Object get() { 37 | return Optional.ofNullable(first.get()).orElseGet(second::get); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/fake/AbstractDataProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger.fake; 2 | 3 | import java.util.Map; 4 | import java.util.Random; 5 | 6 | /** 7 | * Created by minglei on 4/16/17. 8 | */ 9 | public abstract class AbstractDataProvider implements DataProvider { 10 | protected String name; 11 | protected Map params; 12 | protected boolean required; 13 | 14 | private Random random; 15 | 16 | protected AbstractDataProvider(String name) { 17 | this.name = name; 18 | this.random = new Random(); 19 | } 20 | 21 | @Override 22 | public void setRequestParams(Map params) { 23 | this.params = params; 24 | } 25 | 26 | @Override 27 | public void setRequired(boolean required) { 28 | this.required = required; 29 | } 30 | 31 | @Override 32 | public String name() { 33 | return this.name; 34 | } 35 | 36 | @Override 37 | public Object get() { 38 | return required || random.nextBoolean() ? create() : null; 39 | } 40 | 41 | ///--- 42 | 43 | protected abstract Object create(); 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/com/github/tminglei/swagger/SimpleUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Created by minglei on 4/18/17. 9 | */ 10 | public class SimpleUtilsTest { 11 | 12 | @Test 13 | public void testIsEmpty() { 14 | assertEquals(true, SimpleUtils.isEmpty(null)); 15 | assertEquals(true, SimpleUtils.isEmpty("")); 16 | assertEquals(true, SimpleUtils.isEmpty(" ")); 17 | assertEquals(false, SimpleUtils.isEmpty("ab c")); 18 | assertEquals(false, SimpleUtils.isEmpty(new Object())); 19 | } 20 | 21 | @Test 22 | public void testNotEmpty() { 23 | assertEquals("abc", SimpleUtils.notEmpty("abc", null)); 24 | 25 | Object object = new Object(); 26 | assertEquals(object, SimpleUtils.notEmpty(object, null)); 27 | 28 | try { 29 | assertEquals(" ", SimpleUtils.notEmpty(" ", "value is null or empty")); 30 | } catch (Exception e) { 31 | assertEquals("value is null or empty", e.getMessage()); 32 | } 33 | 34 | try { 35 | assertTrue(null == SimpleUtils.notEmpty(null, "value is null or empty")); 36 | } catch (Exception e) { 37 | assertEquals("value is null or empty", e.getMessage()); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, 涂名雷 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/SimpleUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger; 2 | 3 | import java.util.Collection; 4 | import java.util.Map; 5 | 6 | /** 7 | * Some util methods 8 | */ 9 | public class SimpleUtils { 10 | 11 | public static boolean isEmpty(Object value) { 12 | return (value instanceof String && ((String) value).trim().length() == 0) 13 | || (value instanceof Map && ((Map) value).isEmpty()) 14 | || (value instanceof Collection && ((Collection) value).isEmpty()) 15 | || value == null; 16 | } 17 | 18 | public static T notEmpty(T value, String message) { 19 | if (value == null) throw new IllegalArgumentException(message); 20 | if (value instanceof String && "".equals(((String) value).trim())) { 21 | throw new IllegalArgumentException(message); 22 | } 23 | return value; 24 | } 25 | 26 | public static T newInstance(String clazzName) { 27 | try { 28 | Class clazz = (Class) Class.forName(clazzName); 29 | return clazz.newInstance(); 30 | } catch (ClassNotFoundException e) { 31 | throw new RuntimeException("INVALID class: '" + clazzName + "'!!!"); 32 | } catch (InstantiationException e) { 33 | throw new RuntimeException("FAILED to instantiate class: '" + clazzName + "'!!!"); 34 | } catch (IllegalAccessException e) { 35 | throw new RuntimeException(e); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example/java-jaxrs/README.md: -------------------------------------------------------------------------------- 1 | # Swagger Sample App 2 | 3 | ## Overview 4 | This is a java project to build a stand-alone server which implements the Swagger spec. You can find out 5 | more about both the spec and the framework at http://swagger.io. 6 | 7 | ### To run (with Maven) 8 | To run the server, run this task: 9 | ``` 10 | mvn jetty:run-war 11 | ``` 12 | 13 | This will start Jetty embedded on port 8002. 14 | 15 | ### Testing the server 16 | Once started, you can navigate to http://localhost:8002/api/swagger.json to view the Swagger Resource Listing. 17 | This tells you that the server is up and ready to demonstrate Swagger. 18 | 19 | ### Using the UI 20 | There is an HTML5-based API tool bundled in this sample--you can view it it at [http://localhost:8002](http://localhost:8002). This lets you inspect the API using an interactive UI. You can access the source of this code from [here](https://github.com/swagger-api/swagger-ui) 21 | 22 | You can then open the dist/index.html file in any HTML5-enabled browser. Upen opening, enter the 23 | URL of your server in the top-centered input box (default is http://localhost:8002/api/swagger.json). Click the "Explore" 24 | button and you should see the resources available on the server. 25 | 26 | ### Applying an API key 27 | The sample app has an implementation of the Swagger ApiAuthorizationFilter. This restricts access to resources 28 | based on api-key. There are two keys defined in the sample app: 29 | 30 |
  • - default-key
  • 31 | 32 |
  • - special-key
  • 33 | 34 | When no key is applied, the "default-key" is applied to all operations. If the "special-key" is entered, a 35 | number of other resources are shown in the UI, including sample CRUD operations. 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/bind/MParamBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger.bind; 2 | 3 | import com.github.tminglei.bind.Framework; 4 | import com.github.tminglei.swagger.SwaggerContext; 5 | import io.swagger.models.parameters.Parameter; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * Helper class to build `Parameter` from a `com.github.tminglei.bind.Framework.Mapping` 11 | */ 12 | public class MParamBuilder { 13 | private SwaggerContext context; 14 | 15 | private Attachment.Builder attachBuilder; 16 | private String name; 17 | 18 | public MParamBuilder(SwaggerContext context, Framework.Mapping mapping) { 19 | this.context = context; 20 | this.attachBuilder = new Attachment.Builder(mapping); 21 | } 22 | public MParamBuilder name(String name) { 23 | this.name = name; 24 | return this; 25 | } 26 | 27 | public MParamBuilder in(String where) { 28 | attachBuilder = attachBuilder.in(where); 29 | return this; 30 | } 31 | public MParamBuilder desc(String desc) { 32 | attachBuilder = attachBuilder.desc(desc); 33 | return this; 34 | } 35 | public MParamBuilder example(Object example) { 36 | attachBuilder = attachBuilder.example(example); 37 | return this; 38 | } 39 | 40 | /// 41 | public Parameter get() { 42 | List ret = build(); 43 | if (ret.size() > 1) throw new RuntimeException("MORE than 1 parameters were built!!!"); 44 | return ret.get(0); 45 | } 46 | public List build() { 47 | context.scanAndRegisterNamedModels(attachBuilder.$$); 48 | return context.getMappingConverter().mToParameters(name, attachBuilder.$$); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/java/com/github/tminglei/swagger/SwaggerScanTest.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger; 2 | 3 | import com.github.tminglei.bind.Framework; 4 | import org.junit.Test; 5 | 6 | import java.io.IOException; 7 | import java.net.URISyntaxException; 8 | import java.util.List; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Created by tminglei on 9/10/15. 14 | */ 15 | public class SwaggerScanTest { 16 | 17 | @Test 18 | public void testScanPackage() throws IOException, URISyntaxException { 19 | SwaggerFilter filter = new SwaggerFilter(); 20 | List classes = filter.scan(SimpleUtils.class, "com.github.tminglei"); 21 | assertTrue(classes.contains(SwaggerContext.class.getName())); //in folder 22 | assertTrue(classes.contains(Framework.class.getName())); //in jar 23 | /// 24 | List classes1 = filter.scan(SimpleUtils.class, "com/github/tminglei/"); 25 | assertTrue(classes1.contains(SwaggerContext.class.getName())); //in folder 26 | assertTrue(classes1.contains(Framework.class.getName())); //in jar 27 | } 28 | 29 | @Test 30 | public void testScanClass() throws IOException, URISyntaxException { 31 | SwaggerFilter filter = new SwaggerFilter(); 32 | List classes = filter.scan(SimpleUtils.class, "com.github.tminglei.swagger.SwaggerContext"); 33 | assertEquals(classes.size(), 1); 34 | assertEquals(classes.get(0), SwaggerContext.class.getName()); 35 | /// 36 | List classes1 = filter.scan(SimpleUtils.class, "/com/github/tminglei/swagger/SwaggerContext.class"); 37 | assertEquals(classes1.size(), 1); 38 | assertEquals(classes1.get(0), SwaggerContext.class.getName()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/route/PathNamedParamElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 BigTesting.org 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.tminglei.swagger.route; 17 | 18 | /** 19 | * 20 | * @author Luis Antunes 21 | */ 22 | public class PathNamedParamElement extends PathElement { 23 | private final String regex; 24 | 25 | public PathNamedParamElement(String name, int index, String regex) { 26 | super(name, index); 27 | this.regex = regex; 28 | } 29 | 30 | /** 31 | * Returns the regex pattern for the element if it exists, 32 | * or null if no regex pattern was provided. 33 | * 34 | * @return the regex pattern for the element if it exists, 35 | * or null otherwise 36 | */ 37 | public String regex() { 38 | return regex; 39 | } 40 | 41 | public boolean hasRegex() { 42 | return regex != null && regex.trim().length() > 0; 43 | } 44 | 45 | public boolean equals(Object o) { 46 | if (o == null) return false; 47 | if (o == this) return true; 48 | if (!(o instanceof PathNamedParamElement)) return false; 49 | PathNamedParamElement that = (PathNamedParamElement) o; 50 | 51 | return super.equals(o) 52 | && (this.regex == null ? that.regex == null : this.regex.equals(that.regex)); 53 | } 54 | } -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/bind/MappingConverter.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger.bind; 2 | 3 | import com.github.tminglei.bind.Framework; 4 | import io.swagger.models.Model; 5 | import io.swagger.models.Response; 6 | import io.swagger.models.parameters.Parameter; 7 | import io.swagger.models.properties.Property; 8 | 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | /** 13 | * Used to convert form-binder mapping to swagger components 14 | */ 15 | public interface MappingConverter { 16 | 17 | /** 18 | * convert mapping to swagger parameters 19 | * 20 | * @param name param name 21 | * @param mapping the mapping 22 | * @return converted parameters 23 | */ 24 | List mToParameters(String name, Framework.Mapping mapping); 25 | 26 | /** 27 | * convert mapping to swagger parameter 28 | * 29 | * @param name param name 30 | * @param mapping the mapping 31 | * @return converted parameter 32 | */ 33 | Parameter mToParameter(String name, Framework.Mapping mapping); 34 | 35 | /** 36 | * convert mapping to swagger response 37 | * 38 | * @param mapping the mapping 39 | * @return converted response 40 | */ 41 | Response mToResponse(Framework.Mapping mapping); 42 | 43 | /** 44 | * convert mapping to swagger property 45 | * 46 | * @param mapping the mapping 47 | * @return converted property 48 | */ 49 | Property mToProperty(Framework.Mapping mapping); 50 | 51 | /** 52 | * convert mapping to swagger model 53 | * 54 | * @param mapping the mapping 55 | * @return converted model 56 | */ 57 | Model mToModel(Framework.Mapping mapping); 58 | 59 | /** 60 | * scan mapping and find/convert swagger models 61 | * 62 | * @param mapping the mapping 63 | * @return found models 64 | */ 65 | List> scanModels(Framework.Mapping mapping); 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/route/PathElementComparator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 BigTesting.org 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.tminglei.swagger.route; 17 | 18 | import java.util.Comparator; 19 | 20 | /** 21 | * 22 | * @author Luis Antunes 23 | */ 24 | public class PathElementComparator implements Comparator { 25 | 26 | public int compare(String r1Elem, String r2Elem) { 27 | if (r1Elem.equals("")) return -1; 28 | if (r2Elem.equals("")) return 1; 29 | 30 | if (r1Elem.equals(RouteHelper.WILDCARD) && !r2Elem.equals("")) return -1; 31 | if (r2Elem.equals(RouteHelper.WILDCARD) && !r1Elem.equals("")) return 1; 32 | 33 | if (r1Elem.equals(RouteHelper.WILDCARD) && r2Elem.equals("")) return 1; 34 | if (r2Elem.equals(RouteHelper.WILDCARD) && r1Elem.equals("")) return -1; 35 | 36 | if (r1Elem.startsWith(RouteHelper.PARAM_PREFIX) && !r2Elem.equals("") && !r2Elem.equals(RouteHelper.WILDCARD)) return 1; 37 | if (r2Elem.startsWith(RouteHelper.PARAM_PREFIX) && !r1Elem.equals("") && !r1Elem.equals(RouteHelper.WILDCARD)) return -1; 38 | 39 | if (r1Elem.startsWith(RouteHelper.PARAM_PREFIX) && (r2Elem.equals(RouteHelper.WILDCARD) || r2Elem.equals(""))) return -1; 40 | if (r2Elem.startsWith(RouteHelper.PARAM_PREFIX) && (r1Elem.equals(RouteHelper.WILDCARD) || r1Elem.equals(""))) return 1; 41 | 42 | return r1Elem.compareTo(r2Elem); 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/route/PathElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 BigTesting.org 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.tminglei.swagger.route; 17 | 18 | /** 19 | * 20 | * @author Luis Antunes 21 | */ 22 | public abstract class PathElement { 23 | protected final String name; 24 | protected final int index; 25 | 26 | public PathElement(String name, int index) { 27 | this.name = name; 28 | this.index = index; 29 | } 30 | 31 | /** 32 | * Returns the name of the element in the route. 33 | * 34 | * @return the name of the element in the route 35 | */ 36 | public String name() { 37 | return name; 38 | } 39 | 40 | /** 41 | * Returns the absolute position of the element 42 | * in the route. 43 | * 44 | * @return the index of the element in the route 45 | */ 46 | public int index() { 47 | return index; 48 | } 49 | 50 | public int hashCode() { 51 | int result = 1; 52 | result = 31 * result + (name == null ? 0 : name.hashCode()); 53 | result = 31 * result + index; 54 | return result; 55 | } 56 | 57 | public boolean equals(Object o) { 58 | if (o == null) return false; 59 | if (o == this) return true; 60 | if (!(o instanceof PathElement)) return false; 61 | 62 | PathElement that = (PathElement)o; 63 | return this.index == that.index 64 | && (this.name == null ? that.name == null : this.name.equals(that.name)); 65 | } 66 | } -------------------------------------------------------------------------------- /example/java-jaxrs/src/main/java/com/example/data/StoreData.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 SmartBear Software 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.data; 18 | 19 | import java.sql.SQLException; 20 | import java.util.Map; 21 | 22 | import com.github.tminglei.bind.BindObject; 23 | import org.apache.commons.dbutils.QueryRunner; 24 | 25 | public class StoreData { 26 | 27 | public Map findOrderById(long orderId) throws SQLException { 28 | QueryRunner run = new QueryRunner( H2DB.getDataSource() ); 29 | 30 | return run.query("select * from order where id=?", H2DB.mkResultSetHandler( 31 | "id", "petId", "quantity", "shipDate", "status" 32 | ), orderId).stream().findFirst().orElse(null); 33 | } 34 | 35 | public void placeOrder(BindObject bindObj) throws SQLException { 36 | deleteOrder(bindObj.get("id")); 37 | 38 | QueryRunner run = new QueryRunner( H2DB.getDataSource() ); 39 | run.update("insert into order(id, pet_id, quantity, ship_date, status) " + 40 | "values(?, ?, ?, ?, ?)", 41 | bindObj.get("id"), 42 | bindObj.get("petId"), 43 | bindObj.get("quantity"), 44 | bindObj.get("shipDate"), 45 | bindObj.get("status")); 46 | } 47 | 48 | public void deleteOrder(long orderId) throws SQLException { 49 | QueryRunner run = new QueryRunner( H2DB.getDataSource() ); 50 | run.update("delete from order where id=?", orderId); 51 | } 52 | } -------------------------------------------------------------------------------- /example/java-jaxrs/src/main/java/com/example/model/ApiResponse.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 SmartBear Software 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.model; 18 | 19 | import javax.xml.bind.annotation.XmlTransient; 20 | 21 | @javax.xml.bind.annotation.XmlRootElement 22 | public class ApiResponse { 23 | public static final int ERROR = 1; 24 | public static final int WARNING = 2; 25 | public static final int INFO = 3; 26 | public static final int OK = 4; 27 | public static final int TOO_BUSY = 5; 28 | 29 | int code; 30 | String type; 31 | String message; 32 | 33 | public ApiResponse(){} 34 | 35 | public ApiResponse(int code, String message){ 36 | this.code = code; 37 | switch(code){ 38 | case ERROR: 39 | setType("error"); 40 | break; 41 | case WARNING: 42 | setType("warning"); 43 | break; 44 | case INFO: 45 | setType("info"); 46 | break; 47 | case OK: 48 | setType("ok"); 49 | break; 50 | case TOO_BUSY: 51 | setType("too busy"); 52 | break; 53 | default: 54 | setType("unknown"); 55 | break; 56 | } 57 | this.message = message; 58 | } 59 | 60 | @XmlTransient 61 | public int getCode() { 62 | return code; 63 | } 64 | 65 | public void setCode(int code) { 66 | this.code = code; 67 | } 68 | 69 | public String getType() { 70 | return type; 71 | } 72 | 73 | public void setType(String type) { 74 | this.type = type; 75 | } 76 | 77 | public String getMessage() { 78 | return message; 79 | } 80 | 81 | public void setMessage(String message) { 82 | this.message = message; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /example/java-jaxrs/src/main/java/com/example/data/UserData.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 SmartBear Software 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.data; 18 | 19 | import java.sql.SQLException; 20 | import java.util.Map; 21 | 22 | import com.github.tminglei.bind.BindObject; 23 | import org.apache.commons.dbutils.QueryRunner; 24 | 25 | public class UserData { 26 | 27 | public Map findUserByName(String username) throws SQLException { 28 | QueryRunner run = new QueryRunner( H2DB.getDataSource() ); 29 | 30 | return run.query("select id, user_name, first_name, last_name, email, phone, status from user where user_name=?", 31 | H2DB.mkResultSetHandler( 32 | "id", "username", "firstName", "lastName", "email", "phone", "status" 33 | ), username).stream().findFirst().orElse(null); 34 | } 35 | 36 | public void addUser(BindObject bindObj) throws SQLException { 37 | QueryRunner run = new QueryRunner( H2DB.getDataSource() ); 38 | run.update("insert into user(id, user_name, first_name, last_name, email, password, phone, status) " + 39 | "values(?, ?, ?, ?, ?, ?, ?, ?)", 40 | bindObj.get("id"), 41 | bindObj.get("username"), 42 | bindObj.get("firstName"), 43 | bindObj.get("lastName"), 44 | bindObj.get("email"), 45 | bindObj.get("password"), 46 | bindObj.get("phone"), 47 | bindObj.get("status")); 48 | } 49 | 50 | public void removeUser(String username) throws SQLException { 51 | QueryRunner run = new QueryRunner( H2DB.getDataSource() ); 52 | run.update("delete from user where user_name=?", username); 53 | } 54 | } -------------------------------------------------------------------------------- /example/java-jaxrs/src/main/java/com/example/resource/SampleExceptionMapper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 SmartBear Software 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.resource; 18 | 19 | import javax.ws.rs.core.Response; 20 | import javax.ws.rs.core.Response.Status; 21 | import javax.ws.rs.ext.ExceptionMapper; 22 | import javax.ws.rs.ext.Provider; 23 | 24 | import com.example.exception.BadRequestException; 25 | import com.example.exception.NotFoundException; 26 | import com.example.model.ApiResponse; 27 | import com.example.exception.ApiException; 28 | 29 | @Provider 30 | public class SampleExceptionMapper implements ExceptionMapper { 31 | public Response toResponse(Exception exception) { 32 | if (exception instanceof javax.ws.rs.WebApplicationException) { 33 | javax.ws.rs.WebApplicationException e = (javax.ws.rs.WebApplicationException) exception; 34 | return Response 35 | .status(e.getResponse().getStatus()) 36 | .entity(new ApiResponse(e.getResponse().getStatus(), 37 | exception.getMessage())).build(); 38 | } else if (exception instanceof com.fasterxml.jackson.core.JsonParseException) { 39 | return Response.status(400) 40 | .entity(new ApiResponse(400, "bad input")).build(); 41 | } else if (exception instanceof NotFoundException) { 42 | return Response 43 | .status(Status.NOT_FOUND) 44 | .entity(new ApiResponse(ApiResponse.ERROR, exception 45 | .getMessage())).build(); 46 | } else if (exception instanceof BadRequestException) { 47 | return Response 48 | .status(Status.BAD_REQUEST) 49 | .entity(new ApiResponse(ApiResponse.ERROR, exception 50 | .getMessage())).build(); 51 | } else if (exception instanceof ApiException) { 52 | return Response 53 | .status(Status.BAD_REQUEST) 54 | .entity(new ApiResponse(ApiResponse.ERROR, exception 55 | .getMessage())).build(); 56 | } else { 57 | return Response.status(500) 58 | .entity(new ApiResponse(500, "something bad happened")) 59 | .build(); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /example/java-jaxrs/src/main/java/com/example/Bootstrap.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 SmartBear Software 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example; 18 | 19 | import javax.servlet.ServletConfig; 20 | import javax.servlet.ServletException; 21 | import javax.servlet.http.HttpServlet; 22 | 23 | import com.example.data.H2DB; 24 | import io.swagger.models.auth.In; 25 | 26 | import static com.github.tminglei.swagger.SwaggerContext.*; 27 | 28 | public class Bootstrap extends HttpServlet { 29 | private static final long serialVersionUID = 1L; 30 | 31 | static { // for swagger 32 | swagger().info(info() 33 | .title("Swagger Sample App") 34 | .description("This is a sample server Petstore server. You can find out more about Swagger " + 35 | "at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, " + 36 | "you can use the api key `special-key` to test the authorization filters.") 37 | .termsOfService("http://swagger.io/terms/") 38 | .contact(contact().email("apiteam@swagger.io")) 39 | .license(license().name("Apache 2.0") 40 | .url("http://www.apache.org/licenses/LICENSE-2.0.html") 41 | ) 42 | ).host("localhost:8002") 43 | .basePath("/api") 44 | .consumes("application/json").consumes("application/xml") 45 | .produces("application/json").produces("application/xml") 46 | .securityDefinition("api_key", apiKeyAuth("api_key", In.HEADER)) 47 | .securityDefinition("petstore_auth", oAuth2() 48 | .implicit("http://petstore.swagger.io/api/oauth/dialog") 49 | .scope("read:pets", "read your pets") 50 | .scope("write:pets", "modify pets in your account") 51 | ).tag(tag("pet").description("Everything about your Pets") 52 | .externalDocs(externalDocs().description("Find out more").url("http://swagger.io")) 53 | ).tag(tag("store").description("Access to Petstore orders") 54 | ).tag(tag("user").description("Operations about user") 55 | .externalDocs(externalDocs().description("Find out more about our store").url("http://swagger.io")) 56 | ); 57 | } 58 | 59 | @Override 60 | public void init(ServletConfig config) throws ServletException { 61 | H2DB.setupDatabase(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/bind/Attachment.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger.bind; 2 | 3 | import com.github.tminglei.bind.Framework; 4 | 5 | import static com.github.tminglei.bind.OptionsOps.*; 6 | 7 | /** 8 | * Extension class to be used to associate extra data to a `com.github.tminglei.bind.Framework.Mapping` 9 | */ 10 | public class Attachment { 11 | public static final Attachment NULL_OBJECT = new Attachment(); 12 | 13 | private String in; 14 | private String desc; 15 | private String format; 16 | private Object example; 17 | private String refName; 18 | 19 | public String in() { 20 | return this.in; 21 | } 22 | 23 | public String desc() { 24 | return this.desc; 25 | } 26 | 27 | public String format() { 28 | return this.format; 29 | } 30 | 31 | public Object example() { 32 | return this.example; 33 | } 34 | 35 | public String refName() { 36 | return this.refName; 37 | } 38 | 39 | /// 40 | 41 | protected Attachment clone() { 42 | Attachment clone = new Attachment(); 43 | clone.in = this.in; 44 | clone.desc = this.desc; 45 | clone.format = this.format; 46 | clone.example = this.example; 47 | clone.refName = this.refName; 48 | return clone; 49 | } 50 | 51 | ///--- 52 | 53 | public static Attachment attach(Framework.Mapping mapping) { 54 | Attachment attach = (Attachment) _attachment(mapping.options()); 55 | return attach == null ? NULL_OBJECT : attach; 56 | } 57 | 58 | public static Framework.Mapping mergeAttach(Framework.Mapping mapping, Attachment other) { 59 | Attachment merged = attach(mapping).clone(); 60 | merged.in = other.in; 61 | return mapping.options(o -> _attachment(o, merged)); 62 | } 63 | 64 | ///--- 65 | 66 | public static class Builder { 67 | public final Framework.Mapping $$; 68 | private final Attachment _attach; 69 | 70 | public Builder(Framework.Mapping mapping) { 71 | this._attach = attach(mapping).clone(); 72 | this.$$ = mapping.options(o -> _attachment(o, _attach)); 73 | } 74 | 75 | public Builder in(String in) { 76 | Builder clone = new Builder<>($$); 77 | clone._attach.in = in; 78 | return clone; 79 | } 80 | 81 | public Builder desc(String desc) { 82 | Builder clone = new Builder<>($$); 83 | clone._attach.desc = desc; 84 | return clone; 85 | } 86 | 87 | public Builder format(String format) { 88 | Builder clone = new Builder<>($$); 89 | clone._attach.format = format; 90 | return clone; 91 | } 92 | 93 | public Builder example(Object example) { 94 | Builder clone = new Builder<>($$); 95 | clone._attach.example = example; 96 | return clone; 97 | } 98 | 99 | public Builder refName(String refName) { 100 | Builder clone = new Builder<>($$); 101 | clone._attach.refName = refName; 102 | return clone; 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/fake/DataWriterImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger.fake; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.databind.SerializationFeature; 6 | 7 | import java.io.IOException; 8 | import java.io.Writer; 9 | import java.util.Collection; 10 | import java.util.Map; 11 | 12 | import static com.github.tminglei.swagger.SimpleUtils.isEmpty; 13 | 14 | /** 15 | * Created by minglei on 4/17/17. 16 | */ 17 | public class DataWriterImpl implements DataWriter { 18 | private static final String FORMAT_JSON = "application/json"; 19 | private static final String FORMAT_XML = "application/xml"; 20 | 21 | private static final ObjectMapper objectMapper = new ObjectMapper() 22 | .setSerializationInclusion(JsonInclude.Include.NON_NULL) 23 | .configure(SerializationFeature.INDENT_OUTPUT, true); 24 | 25 | @Override 26 | public void write(Writer writer, String format, DataProvider provider) throws IOException { 27 | switch (format.toLowerCase()) { 28 | case FORMAT_JSON: 29 | String dataJson = objectMapper.writeValueAsString(provider.get()); 30 | writer.write(dataJson); 31 | break; 32 | case FORMAT_XML: 33 | toXml(writer, provider.get(), provider.name(), 0); 34 | break; 35 | default: 36 | throw new IllegalArgumentException("Unsupported format: " + format); 37 | } 38 | } 39 | 40 | ///--- 41 | 42 | private void toXml(Writer writer, Object obj, String name, int level) throws IOException { 43 | if (isEmpty(obj)) { 44 | emptyNode(writer, name, level); 45 | } else if (obj instanceof Map) { 46 | startNode(writer, name, level); 47 | for (Map.Entry entry : ((Map) obj).entrySet()) { 48 | toXml(writer, entry.getValue(), entry.getKey(), level+1); 49 | } 50 | endNode(writer, name, level); 51 | } else if (obj instanceof Collection) { 52 | startNode(writer, name, level); 53 | String cname = name.endsWith("s") ? name.substring(0, name.length() -1) : "value"; 54 | for (Object item : (Collection) obj) { 55 | toXml(writer, item, cname, level+1); 56 | } 57 | endNode(writer, name, level); 58 | } else { 59 | startNode(writer, name, level); 60 | writer.write(obj.toString()); 61 | endNode(writer, name, -1); 62 | } 63 | } 64 | 65 | private void emptyNode(Writer writer, String name, int level) throws IOException { 66 | indentWithNewLine(writer, level); 67 | writer.write("<" + name + "/>"); 68 | } 69 | 70 | private void startNode(Writer writer, String name, int level) throws IOException { 71 | indentWithNewLine(writer, level); 72 | writer.write("<" + name + ">"); 73 | } 74 | 75 | private void endNode(Writer writer, String name, int level) throws IOException { 76 | indentWithNewLine(writer, level); 77 | writer.write(""); 78 | } 79 | 80 | private void indentWithNewLine(Writer writer, int level) throws IOException { 81 | if (level >= 0) { 82 | writer.write("\n"); 83 | for (int i=0; i < 2*level; i++) { 84 | writer.write(" "); 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /example/java-jaxrs/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | SwaggerFilter 9 | com.github.tminglei.swagger.SwaggerFilter 10 | 11 | 17 | 18 | 19 | scan-packages-and-classes 20 | com.example.resource; com.example.Bootstrap 21 | 22 | 23 | 29 | 30 | 36 | 37 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | SwaggerFilter 60 | /api/* 61 | 62 | 63 | 64 | jersey 65 | com.sun.jersey.spi.container.servlet.ServletContainer 66 | 67 | com.sun.jersey.config.property.packages 68 | com.example.resource;io.swagger.jaxrs.listing 69 | 70 | 71 | com.sun.jersey.spi.container.ContainerRequestFilters 72 | com.sun.jersey.api.container.filter.PostReplaceFilter 73 | 74 | 75 | com.sun.jersey.api.json.POJOMappingFeature 76 | true 77 | 78 | 79 | com.sun.jersey.config.feature.DisableWADL 80 | true 81 | 82 | 1 83 | 84 | 85 | jersey 86 | /api/* 87 | 88 | 89 | 90 | Bootstrap 91 | com.example.Bootstrap 92 | 2 93 | 94 | 95 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/route/RouteHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 BigTesting.org 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.tminglei.swagger.route; 17 | 18 | import java.io.UnsupportedEncodingException; 19 | import java.net.URLDecoder; 20 | import java.text.CharacterIterator; 21 | import java.text.StringCharacterIterator; 22 | import java.util.Arrays; 23 | import java.util.Collections; 24 | import java.util.HashSet; 25 | import java.util.Set; 26 | import java.util.regex.Pattern; 27 | 28 | /** 29 | * 30 | * @author Luis Antunes 31 | */ 32 | public class RouteHelper { 33 | 34 | public static final String PATH_ELEMENT_SEPARATOR = "/"; 35 | public static final String PARAM_PREFIX = ":"; 36 | public static final char CUSTOM_REGEX_START = '<'; 37 | public static final char CUSTOM_REGEX_END = '>'; 38 | public static final String WILDCARD = "*"; 39 | 40 | /* 41 | * From the Java API documentation for the Pattern class: 42 | * 43 | * Instances of this (Pattern) class are immutable and are safe for use by 44 | * multiple concurrent threads. Instances of the Matcher class are not safe 45 | * for such use. 46 | */ 47 | public static final Pattern CUSTOM_REGEX_PATTERN = Pattern.compile("<[^>]+>"); 48 | 49 | /* 50 | * set of regex special chars to escape 51 | */ 52 | private static final Set REGEX_SPECIAL_CHARS = Collections.unmodifiableSet( 53 | new HashSet<>(Arrays.asList( 54 | '[',']','(',')','{','}','+','*','^','?','$','.','\\'))); 55 | 56 | public static String[] getPathElements(String path) { 57 | return getPathElements(path, true); 58 | } 59 | 60 | public static String[] getPathElements(String path, boolean ignoreTrailingSeparator) { 61 | if (path == null) throw new IllegalArgumentException("path cannot be null"); 62 | path = path.trim(); 63 | if (path.length() == 0) throw new IllegalArgumentException("path cannot be empty"); 64 | path = path.startsWith(PATH_ELEMENT_SEPARATOR) ? path.substring(1) : path; 65 | return path.split(PATH_ELEMENT_SEPARATOR, ignoreTrailingSeparator ? 0 : -1); 66 | } 67 | 68 | public static String urlDecodeForPathParams(String s) { 69 | s = s.replaceAll("\\+", "%2b"); 70 | return urlDecode(s); 71 | } 72 | 73 | public static String urlDecodeForRouting(String s) { 74 | s = s.replaceAll("%2f|%2F", "%252f"); 75 | return urlDecode(s); 76 | } 77 | 78 | public static String urlDecode(String s) { 79 | try { 80 | return URLDecoder.decode(s, "UTF-8"); 81 | } catch (UnsupportedEncodingException e) { 82 | throw new RuntimeException("could not URL decode string: " + s, e); 83 | } 84 | } 85 | 86 | public static String escapeNonCustomRegex(String path) { 87 | /* 88 | * TODO replace with a regular expression 89 | */ 90 | StringBuilder sb = new StringBuilder(); 91 | boolean inCustomRegion = false; 92 | CharacterIterator it = new StringCharacterIterator(path); 93 | for (char ch = it.first(); ch != CharacterIterator.DONE; ch = it.next()) { 94 | 95 | if (ch == CUSTOM_REGEX_START) { 96 | inCustomRegion = true; 97 | } else if (ch == CUSTOM_REGEX_END) { 98 | inCustomRegion = false; 99 | } 100 | 101 | if (REGEX_SPECIAL_CHARS.contains(ch) && !inCustomRegion) { 102 | sb.append('\\'); 103 | } 104 | 105 | sb.append(ch); 106 | } 107 | 108 | return sb.toString(); 109 | } 110 | } -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/ExOperation.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger; 2 | 3 | import com.github.tminglei.swagger.bind.MParamBuilder; 4 | import io.swagger.models.*; 5 | import io.swagger.models.parameters.Parameter; 6 | 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | /** 11 | * Extend `Operation` to provide some more helper methods 12 | */ 13 | public class ExOperation extends Operation { 14 | private SwaggerContext context; 15 | private HttpMethod method; 16 | private String path; 17 | 18 | public ExOperation(SwaggerContext context, HttpMethod method, String path) { 19 | this.context = context; 20 | this.method = method; 21 | this.path = path; 22 | } 23 | 24 | @Override 25 | public ExOperation summary(String summary) { 26 | this.setSummary(summary); 27 | return this; 28 | } 29 | 30 | @Override 31 | public ExOperation description(String description) { 32 | this.setDescription(description); 33 | return this; 34 | } 35 | 36 | @Override 37 | public ExOperation operationId(String operationId) { 38 | this.setOperationId(operationId); 39 | return this; 40 | } 41 | 42 | @Override 43 | public ExOperation schemes(List schemes) { 44 | this.setSchemes(schemes); 45 | return this; 46 | } 47 | 48 | @Override 49 | public ExOperation scheme(Scheme scheme) { 50 | this.addScheme(scheme); 51 | return this; 52 | } 53 | 54 | @Override 55 | public ExOperation consumes(List consumes) { 56 | this.setConsumes(consumes); 57 | return this; 58 | } 59 | 60 | @Override 61 | public ExOperation consumes(String consumes) { 62 | this.addConsumes(consumes); 63 | return this; 64 | } 65 | 66 | @Override 67 | public ExOperation produces(List produces) { 68 | this.setProduces(produces); 69 | return this; 70 | } 71 | 72 | @Override 73 | public ExOperation produces(String produces) { 74 | this.addProduces(produces); 75 | return this; 76 | } 77 | 78 | @Override 79 | public ExOperation security(SecurityRequirement security) { 80 | return this.security(security.getName(), security.getScopes()); 81 | } 82 | public ExOperation security(String name, List scopes) { 83 | this.addSecurity(name, scopes); 84 | return this; 85 | } 86 | 87 | @Override 88 | public ExOperation parameter(Parameter parameter) { 89 | this.addParameter(parameter); 90 | return this; 91 | } 92 | // helper method 93 | public ExOperation parameter(MParamBuilder builder) { 94 | builder.build().forEach(p -> parameter(p)); 95 | return this; 96 | } 97 | 98 | @Override 99 | public ExOperation response(int code, Response response) { 100 | this.addResponse(String.valueOf(code), response); 101 | return this; 102 | } 103 | 104 | @Override 105 | public ExOperation defaultResponse(Response response) { 106 | this.addResponse("default", response); 107 | return this; 108 | } 109 | 110 | @Override 111 | public ExOperation tags(List tags) { 112 | this.setTags(tags); 113 | return this; 114 | } 115 | 116 | @Override 117 | public ExOperation tag(String tag) { 118 | this.addTag(tag); 119 | return this; 120 | } 121 | 122 | @Override 123 | public ExOperation externalDocs(ExternalDocs externalDocs) { 124 | this.setExternalDocs(externalDocs); 125 | return this; 126 | } 127 | 128 | @Override 129 | public ExOperation deprecated(Boolean deprecated) { 130 | this.setDeprecated(deprecated); 131 | return this; 132 | } 133 | 134 | @Override 135 | public ExOperation vendorExtensions(Map vendorExtensions) { 136 | super.vendorExtensions( vendorExtensions ); 137 | return this; 138 | } 139 | 140 | // mark the operation not implemented 141 | public ExOperation notImplemented() { 142 | context.markNotImplemented(method, path); 143 | return this; 144 | } 145 | } -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/route/TreeRouterImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 BigTesting.org 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.tminglei.swagger.route; 17 | 18 | import io.swagger.models.HttpMethod; 19 | 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | 23 | /** 24 | * 25 | * @author Luis Antunes 26 | */ 27 | public class TreeRouterImpl implements Router { 28 | private TreeNode root; 29 | 30 | public synchronized void add(Route route) { 31 | RouteImpl routeImpl = (RouteImpl) route; 32 | List pathElements = routeImpl.getPathElements(); 33 | if (!pathElements.isEmpty() && routeImpl.endsWithPathSeparator()) { 34 | pathElements.add( 35 | new PathStaticElement(RouteHelper.PATH_ELEMENT_SEPARATOR, pathElements.size() - 1)); 36 | } 37 | 38 | if (root == null) { 39 | root = new TreeNode(new PathStaticElement(RouteHelper.PATH_ELEMENT_SEPARATOR, 0)); 40 | } 41 | 42 | TreeNode currentNode = root; 43 | for (PathElement elem : pathElements) { 44 | TreeNode matchingNode = currentNode.getMatchingChild(elem); 45 | if (matchingNode == null) { 46 | TreeNode newChild = new TreeNode(elem); 47 | currentNode.addChild(newChild); 48 | currentNode = newChild; 49 | } else { 50 | currentNode = matchingNode; 51 | } 52 | } 53 | currentNode.addRoute(route); 54 | } 55 | 56 | /** 57 | * Returns a Route that matches the given URL path. 58 | * Note that the path is expected to be an undecoded URL path. 59 | * The router will handle any decoding that might be required. 60 | * 61 | * @param path an undecoded URL path 62 | * @return the matching route, or null if none is found 63 | */ 64 | public Route route(HttpMethod method, String path) { 65 | List searchTokens = getPathAsSearchTokens(path); 66 | 67 | /* handle the case where path is '/' and route '/*' exists */ 68 | if (searchTokens.isEmpty() && root.containsSplatChild() && !root.hasRoute()) { 69 | return root.getSplatChild().getRoute(method); 70 | } 71 | 72 | TreeNode currentMatchingNode = root; 73 | for (String token : searchTokens) { 74 | TreeNode matchingNode = currentMatchingNode.getMatchingChild(token); 75 | if (matchingNode == null) return null; 76 | 77 | currentMatchingNode = matchingNode; 78 | if (currentMatchingNode.isSplat() && 79 | !currentMatchingNode.hasChildren()) { 80 | return currentMatchingNode.getRoute(method); 81 | } 82 | } 83 | 84 | return currentMatchingNode.getRoute(method); 85 | } 86 | 87 | private List getPathAsSearchTokens(String path) { 88 | List tokens = new ArrayList<>(); 89 | 90 | path = RouteHelper.urlDecodeForRouting(path); 91 | String[] pathElements = RouteHelper.getPathElements(path); 92 | for (int i = 0; i < pathElements.length; i++) { 93 | String token = pathElements[i]; 94 | if (token != null && token.trim().length() > 0) { 95 | tokens.add(token); 96 | } 97 | } 98 | if (!tokens.isEmpty() && 99 | path.trim().endsWith(RouteHelper.PATH_ELEMENT_SEPARATOR)) { 100 | tokens.add(RouteHelper.PATH_ELEMENT_SEPARATOR); 101 | } 102 | 103 | return tokens; 104 | } 105 | 106 | public TreeNode getRoot() { 107 | return root; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/test/java/com/github/tminglei/swagger/route/TestRouteHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 BigTesting.org 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.tminglei.swagger.route; 17 | 18 | import java.util.Arrays; 19 | import org.junit.Test; 20 | 21 | import static org.junit.Assert.*; 22 | 23 | /** 24 | * 25 | * @author Luis Antunes 26 | */ 27 | public class TestRouteHelper { 28 | 29 | @Test 30 | // returns an empty string if given a path with just '/' 31 | public void getPathElementsTest1() { 32 | String[] expected = new String[]{""}; 33 | String[] actual = RouteHelper.getPathElements("/"); 34 | assertTrue(Arrays.equals(expected, actual)); 35 | } 36 | 37 | @Test 38 | // returns the correct strings if given a path with multiple static elements and a named parameter 39 | public void getPathElementsTest2() { 40 | String[] expected = new String[]{"cntrl","actn","clients",":id"}; 41 | String[] actual = RouteHelper.getPathElements("/cntrl/actn/clients/:id"); 42 | assertTrue(Arrays.equals(expected, actual)); 43 | } 44 | 45 | @Test 46 | // returns the correct strings even if the path does not start with '/' 47 | public void getPathElementsTest2b() { 48 | String[] expected = new String[]{"cntrl","actn","clients",":id"}; 49 | String[] actual = RouteHelper.getPathElements("cntrl/actn/clients/:id"); 50 | assertTrue(Arrays.equals(expected, actual)); 51 | } 52 | 53 | @Test 54 | // returns the correct strings if given a path with a single static element and a named parameter 55 | public void getPathElementsTest3() { 56 | String[] expected = new String[]{"clients",":id"}; 57 | String[] actual = RouteHelper.getPathElements("/clients/:id"); 58 | assertTrue(Arrays.equals(expected, actual)); 59 | } 60 | 61 | @Test 62 | // returns the correct strings if given a path with a single static element and a named parameter with custom regex 63 | public void getPathElementsTest4() { 64 | String[] expected = new String[]{"clients",":id<[0-9]+>"}; 65 | String[] actual = RouteHelper.getPathElements("/clients/:id<[0-9]+>"); 66 | assertTrue(Arrays.equals(expected, actual)); 67 | } 68 | 69 | @Test 70 | // escapes non-custom regex 71 | public void escapeNonCustomRegex() { 72 | String path = "/cntrl/[](){}*^?$.\\/a+b/:id<[^/]+>/:name<[a-z]+>"; 73 | String expected = "/cntrl/\\[\\]\\(\\)\\{\\}\\*\\^\\?\\$\\.\\\\/a\\+b/:id<[^/]+>/:name<[a-z]+>"; 74 | String actual = RouteHelper.escapeNonCustomRegex(path); 75 | assertEquals(expected, actual); 76 | } 77 | 78 | @Test 79 | // url decodes for routing correctly 80 | public void urlDecodeForRoutingTest() { 81 | String path = "/hello"; 82 | assertEquals("/hello", RouteHelper.urlDecodeForRouting(path)); 83 | 84 | path = "/hello/foo%2Fbar/there"; 85 | assertEquals("/hello/foo%2fbar/there", RouteHelper.urlDecodeForRouting(path)); 86 | 87 | path = "/hello/foo%2fbar/there"; 88 | assertEquals("/hello/foo%2fbar/there", RouteHelper.urlDecodeForRouting(path)); 89 | 90 | path = "/hello/foo%2Fbar/there/foo%2Bbar/a+space"; 91 | assertEquals("/hello/foo%2fbar/there/foo+bar/a space", RouteHelper.urlDecodeForRouting(path)); 92 | } 93 | 94 | @Test 95 | // url decodes for path params correctly 96 | public void urlDecodeForPathParamsTest() { 97 | String param = "hello"; 98 | assertEquals("hello", RouteHelper.urlDecodeForPathParams(param)); 99 | 100 | param = "foo%2Fbar"; 101 | assertEquals("foo/bar", RouteHelper.urlDecodeForPathParams(param)); 102 | 103 | param = "foo+bar"; 104 | assertEquals("foo+bar", RouteHelper.urlDecodeForPathParams(param)); 105 | 106 | param = "foo%2Bbar"; 107 | assertEquals("foo+bar", RouteHelper.urlDecodeForPathParams(param)); 108 | 109 | param = "foo%20bar"; 110 | assertEquals("foo bar", RouteHelper.urlDecodeForPathParams(param)); 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /example/java-jaxrs/src/main/webapp/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 | -------------------------------------------------------------------------------- /example/java-jaxrs/src/main/java/com/example/resource/PetStoreResource.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 SmartBear Software 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.resource; 18 | 19 | import javax.ws.rs.*; 20 | import javax.ws.rs.core.Response; 21 | 22 | import com.example.data.StoreData; 23 | import com.example.exception.BadRequestException; 24 | import com.example.exception.NotFoundException; 25 | import com.github.tminglei.bind.BindObject; 26 | import com.github.tminglei.bind.FormBinder; 27 | import com.github.tminglei.bind.Messages; 28 | import com.github.tminglei.swagger.SharingHolder; 29 | 30 | import java.sql.SQLException; 31 | import java.util.Arrays; 32 | import java.util.Map; 33 | import java.util.ResourceBundle; 34 | 35 | import static io.swagger.models.HttpMethod.*; 36 | import static com.github.tminglei.swagger.SwaggerContext.*; 37 | import static com.github.tminglei.bind.Simple.*; 38 | import static com.github.tminglei.bind.Mappings.*; 39 | import static com.github.tminglei.bind.Constraints.*; 40 | import static com.github.tminglei.bind.Processors.*; 41 | 42 | @Path("/store") 43 | @Produces({"application/json", "application/xml"}) 44 | public class PetStoreResource { 45 | static StoreData storeData = new StoreData(); 46 | private ResourceBundle bundle = ResourceBundle.getBundle("bind-messages"); 47 | private Messages messages = (key) -> bundle.getString(key); 48 | 49 | static Mapping orderStatus = $(text(oneOf(Arrays.asList("placed", "approved", "delivered")))) 50 | .desc("order status").$$; 51 | static Mapping order = $(mapping( 52 | field("id", $(longv()).desc("order id").$$), 53 | field("petId", $(longv(required())).desc("pet id").$$), 54 | field("quantity", $(intv(required())).desc("number to be sold").$$), 55 | field("shipDate", $(datetime()).desc("delivery time").$$), 56 | field("status", orderStatus) 57 | )).refName("Order").desc("order info").$$; 58 | 59 | static SharingHolder sharing = sharing().pathPrefix("/store").tag("store"); 60 | 61 | /// 62 | static { 63 | sharing.operation(GET, "/order/{orderId}") 64 | .summary("get order by id") 65 | .parameter(param(longv()).in("path").name("orderId").desc("order id")) 66 | .response(200, response(order)) 67 | .response(404, response().description("order not found")) 68 | ; 69 | } 70 | @GET 71 | @Path("/order/{orderId}") 72 | public Response getOrderById(@PathParam("orderId") String orderId) 73 | throws NotFoundException, SQLException { 74 | Map order = storeData.findOrderById(Long.parseLong(orderId)); 75 | if (null != order) { 76 | return Response.ok().entity(order).build(); 77 | } else { 78 | throw new NotFoundException(404, "Order not found"); 79 | } 80 | } 81 | 82 | static { 83 | sharing.operation(POST, "/order") 84 | .summary("add an order") 85 | .parameter(param(order).in("body")) 86 | .response(200, response()) 87 | ; 88 | } 89 | @POST 90 | @Path("/order") 91 | public Response placeOrder(String data) throws BadRequestException, SQLException { 92 | BindObject bindObj = new FormBinder(messages).bind( 93 | attach(expandJson()).to(order), 94 | hashmap(entry("", data))); 95 | if (bindObj.errors().isPresent()) { 96 | throw new BadRequestException(400, "invalid pet"); 97 | } else { 98 | storeData.placeOrder(bindObj); 99 | return Response.ok().entity("").build(); 100 | } 101 | } 102 | 103 | static { 104 | sharing.operation(DELETE, "/order/{orderId}") 105 | .summary("delete specified order") 106 | .parameter(param(longv()).in("path").name("orderId").desc("order id")) 107 | .response(200, response()) 108 | ; 109 | } 110 | @DELETE 111 | @Path("/order/{orderId}") 112 | public Response deleteOrder(@PathParam("orderId") String orderId) throws SQLException { 113 | storeData.deleteOrder(Long.parseLong(orderId)); 114 | return Response.ok().entity("").build(); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/route/TreeNode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 BigTesting.org 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.tminglei.swagger.route; 17 | 18 | import java.util.*; 19 | import java.util.regex.Pattern; 20 | 21 | import io.swagger.models.HttpMethod; 22 | 23 | /** 24 | * 25 | * @author Luis Antunes 26 | */ 27 | public class TreeNode { 28 | 29 | private final List children = new ArrayList<>(); 30 | 31 | /* 32 | * From the Java API documentation for the Pattern class: 33 | * Instances of this (Pattern) class are immutable and are safe for use by 34 | * multiple concurrent threads. Instances of the Matcher class are not 35 | * safe for such use. 36 | */ 37 | private final Pattern pattern; 38 | private final PathElement pathElement; 39 | 40 | private final Map routes = new HashMap<>(); 41 | 42 | private final TreeNodeComparator treeNodeComparator = new TreeNodeComparator(); 43 | 44 | public TreeNode(PathElement elem) { 45 | this.pattern = compilePattern(elem); 46 | this.pathElement = elem; 47 | } 48 | 49 | private Pattern compilePattern(PathElement elem) { 50 | StringBuilder routeRegex = new StringBuilder("^"); 51 | 52 | if (elem instanceof PathNamedParamElement) { 53 | PathNamedParamElement namedElem = (PathNamedParamElement) elem; 54 | if (namedElem.hasRegex()) { 55 | routeRegex.append("(").append(namedElem.regex()).append(")"); 56 | } else { 57 | routeRegex.append("([^").append(RouteHelper.PATH_ELEMENT_SEPARATOR).append("]+)"); 58 | } 59 | } else if (elem instanceof PathSplatParamElement) { 60 | routeRegex.append("(.*)"); 61 | } else { 62 | routeRegex.append(RouteHelper.escapeNonCustomRegex(elem.name())); 63 | } 64 | 65 | routeRegex.append("$"); 66 | return Pattern.compile(routeRegex.toString()); 67 | } 68 | 69 | public boolean matches(String token) { 70 | return pattern().matcher(token).find(); 71 | } 72 | 73 | public boolean matches(PathElement elem) { 74 | if (pathElement != null) { 75 | return pathElement.equals(elem); 76 | } 77 | return false; 78 | } 79 | 80 | public void addChild(TreeNode node) { 81 | children.add(node); 82 | Collections.sort(children, treeNodeComparator); 83 | } 84 | 85 | public List getChildren() { 86 | return new ArrayList<>(children); 87 | } 88 | 89 | public TreeNode getMatchingChild(PathElement elem) { 90 | for (TreeNode node : children) { 91 | if (node.matches(elem)) return node; 92 | } 93 | return null; 94 | } 95 | 96 | public TreeNode getMatchingChild(String token) { 97 | for (TreeNode node : children) { 98 | if (node.matches(token)) return node; 99 | } 100 | return null; 101 | } 102 | 103 | public boolean hasChildren() { 104 | return !children.isEmpty(); 105 | } 106 | 107 | public boolean containsSplatChild() { 108 | return getSplatChild() != null; 109 | } 110 | 111 | public TreeNode getSplatChild() { 112 | for (TreeNode child : children) { 113 | if (child.pathElement instanceof PathSplatParamElement) { 114 | return child; 115 | } 116 | } 117 | return null; 118 | } 119 | 120 | public Pattern pattern() { 121 | return pattern; 122 | } 123 | 124 | public boolean isSplat() { 125 | return pathElement instanceof PathSplatParamElement; 126 | } 127 | 128 | public Route getRoute(HttpMethod method) { 129 | return routes.get(method); 130 | } 131 | 132 | public void addRoute(Route route) { 133 | this.routes.put(route.getMethod(), route); 134 | } 135 | 136 | public boolean hasRoute() { 137 | return !this.routes.isEmpty(); 138 | } 139 | 140 | public String toString() { 141 | return pattern.toString(); 142 | } 143 | 144 | private static class TreeNodeComparator implements Comparator { 145 | 146 | public int compare(TreeNode node1, TreeNode node2) { 147 | String r1Elem = getElem(node1.pathElement); 148 | String r2Elem = getElem(node2.pathElement); 149 | 150 | return new PathElementComparator().compare(r1Elem, r2Elem); 151 | } 152 | 153 | private String getElem(PathElement element) { 154 | String elem = element.name(); 155 | if (element instanceof PathNamedParamElement) { 156 | elem = RouteHelper.PARAM_PREFIX + elem; 157 | } 158 | return elem; 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /example/java-jaxrs/src/main/java/com/example/data/PetData.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 SmartBear Software 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.data; 18 | 19 | import java.sql.SQLException; 20 | import java.util.*; 21 | import java.util.stream.Collectors; 22 | 23 | import com.github.tminglei.bind.BindObject; 24 | import org.apache.commons.dbutils.QueryRunner; 25 | import org.apache.commons.lang3.StringUtils; 26 | 27 | public class PetData { 28 | 29 | public Map getPetById(long petId) throws SQLException { 30 | QueryRunner run = new QueryRunner( H2DB.getDataSource() ); 31 | 32 | return run.query("select * from pet where id=?", H2DB.mkResultSetHandler( 33 | "id", "name", "categoryId", "photoUrls", "tags", "status" 34 | ), petId).stream().map(m -> { 35 | m.put("photoUrls", H2DB.strToList((String) m.get("photoUrls"))); 36 | m.put("tags", H2DB.strToList((String) m.get("tags"))); 37 | m.put("category", getCategory(run, (Long) m.get("categoryId"))); 38 | m.remove("categoryId"); 39 | return m; 40 | }).findFirst().orElse(null); 41 | } 42 | 43 | private Map getCategory(QueryRunner run, Long categoryId) { 44 | try { 45 | List> results = run.query("select * from category where id=?", H2DB.mkResultSetHandler("id", "name"), categoryId); 46 | return results.isEmpty() ? null : results.get(0); 47 | } catch (SQLException e) { 48 | throw new RuntimeException(e); 49 | } 50 | } 51 | 52 | public List> findPetByStatus(String status) throws SQLException { 53 | QueryRunner run = new QueryRunner( H2DB.getDataSource() ); 54 | String[] statues = status.split(","); 55 | String statusInStr = StringUtils.join( 56 | Arrays.asList(statues).stream() 57 | .map(s -> "'" + s.trim() + "'") 58 | .collect(Collectors.toList()), 59 | ","); 60 | 61 | if (statues.length > 0) { 62 | return run.query("select * from pet where status in (" + statusInStr + ")", 63 | H2DB.mkResultSetHandler("id", "name", "categoryId", "photoUrls", "tags", "status") 64 | ).stream().map(m -> { 65 | m.put("photoUrls", H2DB.strToList((String) m.get("photoUrls"))); 66 | m.put("tags", H2DB.strToList((String) m.get("tags"))); 67 | m.put("category", getCategory(run, (Long) m.get("categoryId"))); 68 | m.remove("categoryId"); 69 | return m; 70 | }).collect(Collectors.toList()); 71 | } 72 | return Collections.EMPTY_LIST; 73 | } 74 | 75 | public List> findPetByTags(String tags) throws SQLException { 76 | QueryRunner run = new QueryRunner( H2DB.getDataSource() ); 77 | List tagList = Arrays.asList(tags.split(",")); 78 | 79 | if (tagList.isEmpty()) return Collections.EMPTY_LIST; 80 | else { 81 | return run.query("select * from pet", 82 | H2DB.mkResultSetHandler("id", "name", "categoryId", "photoUrls", "tags", "status") 83 | ).stream().map(m -> { 84 | m.put("photoUrls", H2DB.strToList((String) m.get("photoUrls"))); 85 | m.put("tags", H2DB.strToList((String) m.get("tags"))); 86 | m.put("category", getCategory(run, (Long) m.get("categoryId"))); 87 | m.remove("categoryId"); 88 | return m; 89 | }).filter(m -> { 90 | if (m.get("tags") == null) return false; 91 | else { 92 | List its = (List) m.get("tags"); 93 | List tmp = new ArrayList<>(its); 94 | tmp.removeAll(tagList); 95 | return tmp.size() != its.size(); 96 | } 97 | }).collect(Collectors.toList()); 98 | } 99 | } 100 | 101 | public void addPet(BindObject bindObj) throws SQLException { 102 | QueryRunner run = new QueryRunner( H2DB.getDataSource() ); 103 | run.update("insert into pet(id, category_id, name, photo_urls, tags, status) " + 104 | "values(?, ?, ?, ?, ?, ?)", 105 | bindObj.get("id"), 106 | bindObj.obj("category").get("id"), 107 | bindObj.get("name"), 108 | bindObj.get("photoUrls"), 109 | bindObj.get("tags"), 110 | bindObj.get("status")); 111 | } 112 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | com.github.tminglei 6 | binder-swagger-java 7 | 0.8.0 8 | jar 9 | 10 | binder-swagger-java 11 | A framework to bring form-binder-java to swagger 12 | 13 | binder-swagger-java 14 | 15 | https://github.com/tminglei/binder-swagger-java 16 | 17 | 18 | BSD-style 19 | http://www.opensource.org/licenses/bsd-license.php 20 | repo 21 | 22 | 23 | 24 | git@github.com:tminglei/binder-swagger-java.git 25 | scm:git:git@github.com:tminglei/binder-swagger-java.git 26 | 27 | 28 | 29 | tminglei 30 | Minglei Tu 31 | +8 32 | 33 | 34 | 35 | 36 | 1.5.12 37 | 0.13.3 38 | 2.8.4 39 | 0.13 40 | 1.0.2 41 | 3.1.0 42 | 1.7.12 43 | 4.12 44 | 45 | 46 | 47 | 48 | io.swagger 49 | swagger-models 50 | ${swagger-version} 51 | 52 | 53 | com.github.tminglei 54 | form-binder-java 55 | ${binder-version} 56 | 57 | 58 | com.fasterxml.jackson.core 59 | jackson-databind 60 | ${jackson-version} 61 | 62 | 63 | com.github.javafaker 64 | javafaker 65 | ${javafaker-version} 66 | 67 | 68 | com.github.mifmif 69 | generex 70 | ${generex-version} 71 | 72 | 73 | javax.servlet 74 | javax.servlet-api 75 | ${servlet-version} 76 | provided 77 | 78 | 79 | org.slf4j 80 | slf4j-api 81 | ${slf4j-version} 82 | true 83 | 84 | 85 | org.slf4j 86 | slf4j-simple 87 | ${slf4j-version} 88 | provided 89 | 90 | 91 | junit 92 | junit 93 | ${junit-version} 94 | test 95 | 96 | 97 | 98 | 99 | binder-swagger-java 100 | 101 | 102 | org.apache.maven.plugins 103 | maven-compiler-plugin 104 | 3.3 105 | 106 | 1.8 107 | 1.8 108 | UTF-8 109 | 110 | 111 | 112 | org.apache.maven.plugins 113 | maven-surefire-plugin 114 | 2.18.1 115 | 116 | 117 | org.apache.maven.plugins 118 | maven-javadoc-plugin 119 | 2.10.3 120 | 121 | 122 | attach-javadocs 123 | 124 | jar 125 | 126 | 127 | 128 | 129 | 130 | 131 | org.apache.maven.plugins 132 | maven-source-plugin 133 | 2.4 134 | 135 | 136 | attach-sources 137 | verify 138 | 139 | jar-no-fork 140 | 141 | 142 | 143 | 144 | 160 | 161 | org.apache.maven.plugins 162 | maven-release-plugin 163 | 2.5.2 164 | 165 | 166 | 167 | 168 | 169 | 170 | false 171 | deployment 172 | Maven Central Repository 173 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 174 | 175 | 176 | true 177 | deployment 178 | Maven Central Repository 179 | https://oss.sonatype.org/content/repositories/snapshots/ 180 | 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /example/java-jaxrs/src/main/java/com/example/resource/PetResource.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 SmartBear Software 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.resource; 18 | 19 | import javax.ws.rs.*; 20 | import javax.ws.rs.core.Response; 21 | 22 | import com.example.data.PetData; 23 | import com.example.exception.BadRequestException; 24 | import com.example.exception.NotFoundException; 25 | import com.github.javafaker.Faker; 26 | import com.github.tminglei.bind.BindObject; 27 | import com.github.tminglei.bind.FormBinder; 28 | import com.github.tminglei.bind.Messages; 29 | import com.github.tminglei.swagger.SharingHolder; 30 | 31 | import java.sql.SQLException; 32 | import java.util.Arrays; 33 | import java.util.Map; 34 | import java.util.ResourceBundle; 35 | 36 | import static io.swagger.models.HttpMethod.*; 37 | import static com.github.tminglei.swagger.SwaggerContext.*; 38 | import static com.github.tminglei.bind.Simple.*; 39 | import static com.github.tminglei.bind.Mappings.*; 40 | import static com.github.tminglei.bind.Constraints.*; 41 | import static com.github.tminglei.bind.Processors.*; 42 | 43 | @Path("/pet") 44 | @Produces({"application/json", "application/xml"}) 45 | public class PetResource { 46 | private static PetData petData = new PetData(); 47 | private static Faker faker = new Faker(); 48 | 49 | private ResourceBundle bundle = ResourceBundle.getBundle("bind-messages"); 50 | private Messages messages = (key) -> bundle.getString(key); 51 | 52 | static Mapping petStatus = $(text(oneOf(Arrays.asList("available", "pending", "sold")))) 53 | .desc("pet status in the store").example("available").$$; 54 | static Mapping pet = $(mapping( 55 | field("id", $(longv()).desc("pet id").example(gen("petId").or(gen(() -> faker.number().randomNumber()))).$$), 56 | field("name", $(text(required())).desc("pet name").$$), 57 | field("category", $(mapping( 58 | field("id", longv(required())), 59 | field("name", text(required())) 60 | )).desc("category belonged to").$$), 61 | field("photoUrls", $(list(text())).desc("pet's photo urls").example(Arrays.asList("http://example.com/photo1")).$$), 62 | field("tags", $(list(text())).desc("tags for the pet").example(Arrays.asList("tag1", "tag2")).$$), 63 | field("status", petStatus) 64 | )).refName("Pet").desc("pet info").$$; 65 | 66 | static SharingHolder sharing = sharing().pathPrefix("/pet").tag("pet"); 67 | 68 | /// 69 | static { 70 | sharing.operation(GET, "/:petId<[0-9]+>") 71 | .summary("get pet by id") 72 | .parameter(param(longv()).in("path").name("petId").example(1l)) 73 | .response(200, response(pet)) 74 | .response(404, response().description("pet not found")) 75 | .notImplemented() 76 | ; 77 | } 78 | @GET 79 | @Path("/{petId}") 80 | public Response getPetById(@PathParam("petId") String petId) 81 | throws NotFoundException, SQLException { 82 | Map pet = petData.getPetById(Long.parseLong(petId)); 83 | if (null != pet) { 84 | return Response.ok().entity(pet).build(); 85 | } else { 86 | throw new NotFoundException(404, "Pet not found"); 87 | } 88 | } 89 | 90 | static { 91 | sharing.operation(POST, "/") 92 | .summary("create a pet") 93 | .parameter(param(pet).in("body")) 94 | .response(200, response().description("success")) 95 | .response(400, response()) 96 | ; 97 | } 98 | @POST 99 | public Response addPet(String data) throws BadRequestException, SQLException { 100 | BindObject bindObj = new FormBinder(messages).bind( 101 | attach(expandJson()).to(pet), 102 | hashmap(entry("", data))); 103 | if (bindObj.errors().isPresent()) { 104 | throw new BadRequestException(400, "invalid pet"); 105 | } else { 106 | petData.addPet(bindObj); 107 | return Response.ok().entity("SUCCESS").build(); 108 | } 109 | } 110 | 111 | static { 112 | sharing.operation(PUT, "/") 113 | .summary("update pet") 114 | .parameter(param(pet).in("body")) 115 | .response(200, response().description("success")) 116 | .response(400, response()) 117 | ; 118 | } 119 | @PUT 120 | public Response updatePet(String data) throws BadRequestException, SQLException { 121 | BindObject bindObj = new FormBinder(messages).bind( 122 | attach(expandJson()).to(pet), 123 | hashmap(entry("", data))); 124 | if (bindObj.errors().isPresent()) { 125 | throw new BadRequestException(400, "invalid pet"); 126 | } else { 127 | petData.addPet(bindObj); 128 | return Response.ok().entity("SUCCESS").build(); 129 | } 130 | } 131 | 132 | static { 133 | sharing.operation(GET, "/findByStatus") 134 | .summary("find pets by status") 135 | .parameter(param(list(petStatus, required())).in("query").name("status")) 136 | .response(200, response(list(pet)).description("pet list")) 137 | ; 138 | } 139 | @GET 140 | @Path("/findByStatus") 141 | public Response findPetsByStatus(@QueryParam("status") String status) throws SQLException { 142 | return Response.ok(petData.findPetByStatus(status)).build(); 143 | } 144 | 145 | static { 146 | sharing.operation(GET, "/findByTags") 147 | .summary("find pets by tags") 148 | .parameter(param(list(text())).in("query").name("tags").desc("pet tags")) 149 | .response(200, response(list(pet)).description("pet list")) 150 | .deprecated(true); 151 | } 152 | @GET 153 | @Path("/findByTags") 154 | @Deprecated 155 | public Response findPetsByTags(@QueryParam("tags") String tags) throws SQLException { 156 | return Response.ok(petData.findPetByTags(tags)).build(); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/SharingHolder.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger; 2 | 3 | import com.github.tminglei.swagger.bind.MParamBuilder; 4 | import io.swagger.models.HttpMethod; 5 | import io.swagger.models.Response; 6 | import io.swagger.models.Scheme; 7 | import io.swagger.models.parameters.Parameter; 8 | 9 | import java.nio.file.Paths; 10 | import java.util.*; 11 | 12 | /** 13 | * Used to hold sharing configurations (implemented as copy-and-write) 14 | */ 15 | public class SharingHolder { 16 | private SwaggerContext context; 17 | 18 | private String pathPrefix = ""; 19 | private List tags = new ArrayList<>(); 20 | private List schemes = new ArrayList<>(); 21 | private List consumes = new ArrayList<>(); 22 | private List produces = new ArrayList<>(); 23 | private Map> securities = new HashMap<>(); 24 | private List params = new ArrayList<>(); 25 | private Map responses = new HashMap<>(); 26 | 27 | public SharingHolder(SwaggerContext context) { 28 | this.context = context; 29 | } 30 | 31 | /// 32 | public String pathPrefix() { 33 | return this.pathPrefix; 34 | } 35 | public SharingHolder pathPrefix(String path) { 36 | SharingHolder clone = this.clone(); 37 | clone.pathPrefix = path; 38 | return clone; 39 | } 40 | 41 | /// 42 | public List tags() { 43 | return Collections.unmodifiableList(this.tags); 44 | } 45 | public SharingHolder tags(List tags) { 46 | SharingHolder clone = this.clone(); 47 | clone.tags = tags != null ? tags : new ArrayList<>(); 48 | return clone; 49 | } 50 | public SharingHolder tag(String... tags) { 51 | SharingHolder clone = this.clone(); 52 | for (String tag : tags) 53 | clone.tags.add(tag); 54 | return clone; 55 | } 56 | 57 | /// 58 | public List schemes() { 59 | return Collections.unmodifiableList(this.schemes); 60 | } 61 | public SharingHolder schemes(List schemes) { 62 | SharingHolder clone = this.clone(); 63 | clone.schemes = schemes != null ? schemes : new ArrayList<>(); 64 | return clone; 65 | } 66 | public SharingHolder scheme(Scheme... schemes) { 67 | SharingHolder clone = this.clone(); 68 | for (Scheme scheme : schemes) 69 | clone.schemes.add(scheme); 70 | return clone; 71 | } 72 | 73 | /// 74 | public List consumes() { 75 | return Collections.unmodifiableList(this.consumes); 76 | } 77 | public SharingHolder consumes(List consumes) { 78 | SharingHolder clone = this.clone(); 79 | clone.consumes = consumes != null ? consumes : new ArrayList<>(); 80 | return clone; 81 | } 82 | public SharingHolder consume(String... consumes) { 83 | SharingHolder clone = this.clone(); 84 | for (String consume : consumes) 85 | clone.consumes.add(consume); 86 | return clone; 87 | } 88 | 89 | /// 90 | public List produces() { 91 | return Collections.unmodifiableList(this.produces); 92 | } 93 | public SharingHolder produces(List produces) { 94 | SharingHolder clone = this.clone(); 95 | clone.produces = produces != null ? produces : new ArrayList<>(); 96 | return clone; 97 | } 98 | public SharingHolder produce(String... produces) { 99 | SharingHolder clone = this.clone(); 100 | for (String produce : produces) 101 | clone.produces.add(produce); 102 | return clone; 103 | } 104 | 105 | /// 106 | public Map> securities() { 107 | return Collections.unmodifiableMap(this.securities); 108 | } 109 | public SharingHolder securities(Map> securities) { 110 | SharingHolder clone = this.clone(); 111 | clone.securities = securities != null ? securities : new HashMap<>(); 112 | return clone; 113 | } 114 | public SharingHolder security(String name, List scopes) { 115 | SharingHolder clone = this.clone(); 116 | scopes = scopes != null ? scopes : new ArrayList<>(); 117 | clone.securities.put(name, scopes); 118 | return clone; 119 | } 120 | 121 | /// 122 | public List parameters() { 123 | return Collections.unmodifiableList(this.params); 124 | } 125 | public SharingHolder parameters(List params) { 126 | SharingHolder clone = this.clone(); 127 | clone.params = params != null ? params : new ArrayList<>(); 128 | return clone; 129 | } 130 | public SharingHolder parameter(MParamBuilder builder) { 131 | SharingHolder clone = this.clone(); 132 | builder.build().forEach(p -> clone.params.add(p)); 133 | return clone; 134 | } 135 | public SharingHolder parameter(Parameter param) { 136 | SharingHolder clone = this.clone(); 137 | clone.params.add(param); 138 | return clone; 139 | } 140 | 141 | /// 142 | public Map responses() { 143 | return Collections.unmodifiableMap(this.responses); 144 | } 145 | public SharingHolder responses(Map responses) { 146 | SharingHolder clone = this.clone(); 147 | clone.responses = responses != null ? responses : new HashMap<>(); 148 | return clone; 149 | } 150 | public SharingHolder response(int code, Response response) { 151 | SharingHolder clone = this.clone(); 152 | clone.responses.put(code, response); 153 | return clone; 154 | } 155 | 156 | /// 157 | public ExOperation operation(HttpMethod method, String path) { 158 | path = Paths.get(pathPrefix, path).toString(); 159 | ExOperation operation = context.operation(method, path); 160 | operation.setTags(new ArrayList<>(tags)); // !!!NOTE: use clone object here 161 | operation.setSchemes(new ArrayList<>(schemes)); 162 | operation.setConsumes(new ArrayList<>(consumes)); 163 | operation.setProduces(new ArrayList<>(produces)); 164 | securities.forEach((name, scopes) -> operation.security(name, new ArrayList<>(scopes))); 165 | operation.setParameters(new ArrayList<>(params)); 166 | responses.forEach((code, response) -> operation.response(code, response)); 167 | return operation; 168 | } 169 | 170 | /// 171 | @Override 172 | protected SharingHolder clone() { 173 | SharingHolder clone = new SharingHolder(context); 174 | clone.pathPrefix = this.pathPrefix; 175 | clone.tags = new ArrayList<>(this.tags); 176 | clone.params = new ArrayList<>(this.params); 177 | clone.responses = new HashMap<>(this.responses); 178 | clone.schemes = new ArrayList<>(this.schemes); 179 | clone.consumes = new ArrayList<>(this.consumes); 180 | clone.produces = new ArrayList<>(this.produces); 181 | clone.securities = new HashMap<>(this.securities); 182 | return clone; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /example/java-jaxrs/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | com.example 5 | swagger-java-sample 6 | 1.0.0 7 | war 8 | swagger-java-sample 9 | 10 | 11 | 1.5.12 12 | 0.8.0 13 | 1.19 14 | 9.2.2.v20140723 15 | 1.0.1 16 | 2.18.1 17 | 18 | 19 | 20 | 21 | com.github.tminglei 22 | binder-swagger-java 23 | ${binder-swagger-version} 24 | compile 25 | 26 | 27 | com.h2database 28 | h2 29 | 1.4.188 30 | compile 31 | 32 | 33 | commons-dbutils 34 | commons-dbutils 35 | 1.6 36 | compile 37 | 38 | 39 | org.apache.commons 40 | commons-lang3 41 | 3.4 42 | compile 43 | 44 | 45 | ch.qos.logback 46 | logback-classic 47 | ${logback-version} 48 | compile 49 | 50 | 51 | ch.qos.logback 52 | logback-core 53 | ${logback-version} 54 | compile 55 | 56 | 57 | org.scalatest 58 | scalatest_2.10 59 | 2.1.3 60 | test 61 | 62 | 63 | junit 64 | junit 65 | 4.8.1 66 | test 67 | 68 | 69 | javax.servlet 70 | servlet-api 71 | 2.5 72 | provided 73 | 74 | 75 | com.sun.jersey 76 | jersey-core 77 | ${jersey-version} 78 | 79 | 80 | com.sun.jersey 81 | jersey-json 82 | ${jersey-version} 83 | 84 | 85 | com.sun.jersey 86 | jersey-servlet 87 | ${jersey-version} 88 | 89 | 90 | 91 | 92 | src/main/java 93 | 94 | 95 | src/main/resources 96 | 97 | logback.xml 98 | 99 | 100 | 101 | 102 | 103 | org.apache.maven.plugins 104 | maven-compiler-plugin 105 | 3.3 106 | 107 | 1.8 108 | 1.8 109 | UTF-8 110 | 111 | 112 | 113 | org.apache.maven.plugins 114 | maven-war-plugin 115 | 2.1.1 116 | 117 | 118 | maven-failsafe-plugin 119 | ${failsafe-version} 120 | 121 | 122 | 123 | integration-test 124 | verify 125 | 126 | 127 | 128 | 129 | 130 | org.eclipse.jetty 131 | jetty-maven-plugin 132 | ${jetty-version} 133 | 134 | 135 | / 136 | 137 | ${project.build.directory}/${project.artifactId}-${project.version} 138 | 8079 139 | stopit 140 | 141 | 8002 142 | 60000 143 | 144 | 145 | 146 | 147 | start-jetty 148 | pre-integration-test 149 | 150 | run 151 | 152 | 153 | 0 154 | true 155 | 156 | 157 | 158 | stop-jetty 159 | post-integration-test 160 | 161 | stop 162 | 163 | 164 | 165 | 166 | 167 | com.googlecode.maven-download-plugin 168 | download-maven-plugin 169 | 1.2.1 170 | 171 | 172 | swagger-ui 173 | 174 | wget 175 | 176 | 177 | https://github.com/swagger-api/swagger-ui/archive/v2.2.5.tar.gz 178 | true 179 | ${project.build.directory} 180 | 181 | 182 | 183 | 184 | 185 | maven-resources-plugin 186 | 2.6 187 | 188 | 189 | copy-resources 190 | process-resources 191 | 192 | copy-resources 193 | 194 | 195 | ${project.build.directory}/${project.artifactId}-${project.version} 196 | 197 | 198 | ${project.build.directory}/swagger-ui-2.2.5/dist 199 | true 200 | 201 | index.html 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # binder-swagger-java 2 | 3 | [![Build Status](https://travis-ci.org/tminglei/binder-swagger-java.svg?branch=master)](https://travis-ci.org/tminglei/binder-swagger-java) 4 | 5 | `binder-swagger-java` is a simple api management solution, which let api maintainence and dev based on api easily. 6 | 7 | 8 | ## Features 9 | - lightweight, less than 3000 line codes (framework + built-in route/fake data generating) 10 | - based on `form-binder-java`, allowing dynamic objects in operation's parameter/response definitions 11 | - directly integrate with `swagger-models`, allowing to operate swagger object when necessary 12 | - can generate mock response w/ fake data on demand for unimplemented api operations 13 | - high customizable, you can replace almost all of the core components 14 | 15 | 16 | ## How it works 17 | You define the api meta data in classes' static code blocks, then it was collected to a static global swagger object when class scan/loading, so when requested, the program can serve it right now. 18 | _With swagger.json, the swagger-ui can render the API menu in the browser. Then you can browse, fill parameters and send to/receive from service impls (p.s. the service urls were included in swagger.json)._ 19 | 20 | ![binder-swagger description](https://raw.githubusercontent.com/tminglei/binder-swagger-java/master/binder-swagger-java.png) 21 | 22 | > _p.s. based on [`form-binder-java`](https://github.com/tminglei/form-binder-java) and [`swagger-models`](https://github.com/swagger-api/swagger-core), `binder-swagger-java` enable you to define dynamic data structures and operate the swagger object directly when necessary, so it's more expressive in theory._ 23 | 24 | 25 | ## How to use it 26 | #### 0) add the dependency to your project: 27 | ```xml 28 | 29 | com.github.tminglei 30 | binder-swagger-java 31 | 0.8.0 32 | 33 | ``` 34 | #### 1) define and register your api operations: 35 | ```java 36 | // in `PetResource.java` 37 | static Mapping petStatus = $(text(oneOf(Arrays.asList("available", "pending", "sold")))) 38 | .desc("pet status in the store").example("available").$$; 39 | static Mapping pet = $(mapping( 40 | field("id", $(vLong()).desc("pet id").example(gen("petId").or(gen(() -> faker.number().randomNumber()))).$$), 41 | field("name", $(text(required())).desc("pet name").$$), 42 | field("category", attach(required()).to($(mapping( 43 | field("id", vLong(required())), 44 | field("name", text(required())) 45 | )).refName("category").desc("category belonged to").$$)), 46 | field("photoUrls", $(list(text())).desc("pet's photo urls").example(Arrays.asList("http://example.com/photo1")).$$), 47 | field("tags", $(list(text())).desc("tags for the pet").example(Arrays.asList("tag1", "tag2")).$$), 48 | field("status", petStatus) 49 | )).refName("pet").desc("pet info").$$; 50 | 51 | static SharingHolder sharing = sharing().pathPrefix("/pet").tag("pet"); 52 | 53 | static { 54 | sharing.operation(GET, "/{petId}") 55 | .summary("get pet by id") 56 | .parameter(param(longv()).in("path").name("petId").example(1l)) 57 | .response(200, response(pet)) 58 | .response(404, response().description("pet not found")) 59 | .notImplemented() // MARK IT `notImplemented`, THEN `binder-swagger-java` WILL GENERATE MOCK RESPONSE FOR YOU 60 | ; 61 | } 62 | @GET 63 | @Path("/{petId}") 64 | public Response getPetById(@PathParam("petId") String petId) throws NotFoundException, SQLException { 65 | ... 66 | ``` 67 | #### 2) supplement your other swagger info: 68 | ```java 69 | // in `Bootstrap.java` 70 | static { // for swagger 71 | swagger().info(info() 72 | .title("Swagger Sample App") 73 | .description("This is a sample server Petstore server. You can find out more about Swagger " + 74 | "at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, " + 75 | "you can use the api key `special-key` to test the authorization filters.") 76 | .termsOfService("http://swagger.io/terms/") 77 | .contact(contact().email("apiteam@swagger.io")) 78 | .license(license().name("Apache 2.0") 79 | .url("http://www.apache.org/licenses/LICENSE-2.0.html") 80 | ) 81 | ).host("localhost:8002") 82 | .basePath("/api") 83 | .consumes("application/json") 84 | .produces("application/json") 85 | .securityDefinition("api_key", apiKeyAuth("api_key", In.HEADER)) 86 | .securityDefinition("petstore_auth", oAuth2() 87 | .implicit("http://petstore.swagger.io/api/oauth/dialog") 88 | .scope("read:pets", "read your pets") 89 | .scope("write:pets", "modify pets in your account") 90 | ).tag(tag("pet").description("Everything about your Pets") 91 | .externalDocs(externalDocs().description("Find out more").url("http://swagger.io")) 92 | ).tag(tag("store").description("Access to Petstore orders") 93 | ).tag(tag("user").description("Operations about user") 94 | .externalDocs(externalDocs().description("Find out more about our store").url("http://swagger.io")) 95 | ); 96 | } 97 | ``` 98 | #### 3) configure the filter, which will serv the `swagger.json`: 99 | ```xml 100 | // in `web.xml` 101 | 102 | SwaggerFilter 103 | com.github.tminglei.swagger.SwaggerFilter 104 | 105 | 111 | 112 | 113 | scan-packages-and-classes 114 | com.example.resource; com.example.Bootstrap 115 | 116 | 117 | 123 | 124 | 130 | 131 | 137 | 138 | 144 | 145 | 151 | 152 | 153 | SwaggerFilter 154 | /api/* 155 | 156 | ... 157 | ``` 158 | 159 | 160 | ##### That's all. Enjoy it! 161 | 162 | 163 | > For more usage details, pls check the example project [here](https://github.com/tminglei/binder-swagger-java/tree/master/example/java-jaxrs). 164 | 165 | 166 | ## Q & A 167 | **Q:** Why use static code blocks to associate/register operation meta info instead of annotations? 168 | **A:** Well, because we can't use annotations here. Annotation requires static defined data types, but we didn't define java beans in our project. 169 | _(p.s. because of this, we can't also use existing frameworks, like `springfox`.)_ 170 | 171 | 172 | ## License 173 | The BSD License, Minglei Tu <tmlneu@gmail.com> 174 | -------------------------------------------------------------------------------- /example/java-jaxrs/src/main/java/com/example/data/H2DB.java: -------------------------------------------------------------------------------- 1 | package com.example.data; 2 | 3 | import org.apache.commons.dbutils.QueryRunner; 4 | import org.apache.commons.dbutils.ResultSetHandler; 5 | import org.h2.jdbcx.JdbcDataSource; 6 | 7 | import javax.sql.DataSource; 8 | import java.io.IOException; 9 | import java.sql.ResultSet; 10 | import java.sql.SQLException; 11 | import java.util.*; 12 | import java.util.function.Function; 13 | 14 | /** 15 | * Created by tminglei on 9/9/15. 16 | */ 17 | public class H2DB { 18 | private static DataSource dataSource; 19 | 20 | public static DataSource getDataSource() { 21 | if (dataSource == null) { 22 | JdbcDataSource ds = new JdbcDataSource(); 23 | ds.setURL("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"); 24 | ds.setUser("sa"); 25 | ds.setPassword("sa"); 26 | dataSource = ds; 27 | } 28 | return dataSource; 29 | } 30 | 31 | public static ResultSetHandler>> 32 | mkResultSetHandler(String... names) { 33 | return (rs) -> { 34 | try { 35 | int columnCount = rs.getMetaData().getColumnCount(); 36 | List> result = new ArrayList<>(); 37 | while (rs.next()) { 38 | Object[] data = readOneRow(rs, columnCount); 39 | result.add(arrayToMap(data, names)); 40 | } 41 | return result; 42 | } catch (SQLException e) { 43 | throw new RuntimeException(e); 44 | } 45 | }; 46 | } 47 | 48 | public static Object[] readOneRow(ResultSet rs, int columnCount) throws SQLException { 49 | Object[] result = new Object[columnCount]; 50 | for(int i=1; i<=columnCount; i++) { 51 | result[i-1] = rs.getObject(i); 52 | } 53 | return result; 54 | } 55 | 56 | public static Map arrayToMap(Object[] data, String... names) { 57 | if (data == null) throw new IllegalArgumentException("data is null!!!"); 58 | if (data.length != names.length) throw new IllegalArgumentException( 59 | "data has different length with names!!!"); 60 | 61 | Map result = new HashMap(); 62 | for(int i=0; i < names.length; i++) { 63 | result.put(names[i], data[i]); 64 | } 65 | return result; 66 | } 67 | 68 | static List strToList(String listStr) { 69 | if (listStr == null) return null; 70 | List result = new ArrayList<>(); 71 | String[] parts = listStr.split(","); 72 | for(int i=0; i 0) { 74 | result.add(parts[i].trim()); 75 | } 76 | } 77 | return result; 78 | } 79 | 80 | /// 81 | public static void setupDatabase() { 82 | QueryRunner run = new QueryRunner( getDataSource() ); 83 | 84 | try { 85 | run.update("create table category(" + 86 | "id bigint primary key, " + 87 | "name varchar(255)" + 88 | ")"); 89 | run.update("create table pet(" + 90 | "id bigint primary key, " + 91 | "name varchar(255), " + 92 | "category_id bigint, " + 93 | "photo_urls varchar(2000) default '', " + 94 | "tags varchar(500) default '', " + 95 | "status varchar(25)" + 96 | ")"); 97 | run.update("create table order1(" + 98 | "id bigint primary key, " + 99 | "pet_id bigint, " + 100 | "quantity int, " + 101 | "ship_date timestamp, " + 102 | "status varchar(25)" + 103 | ")"); 104 | run.update("create table user(" + 105 | "id bigint primary key, " + 106 | "user_name varchar(50), " + 107 | "first_name varchar(50), " + 108 | "last_name varchar(50), " + 109 | "email varchar(100), " + 110 | "password varchar(250), " + 111 | "phone varchar(50), " + 112 | "status int" + 113 | ")"); 114 | 115 | run.batch("insert into category(id, name) values(?, ?)", new Object[][]{ 116 | new Object[]{1, "Dogs"}, 117 | new Object[]{2, "Cats"}, 118 | new Object[]{3, "Rabbits"}, 119 | new Object[]{4, "Lions"} 120 | }); 121 | run.batch("insert into pet(id, category_id, name, photo_urls, tags, status) " + 122 | "values(?, ?, ?, ?, ?, ?)", new Object[][]{ 123 | new Object[]{1, 2, "Cat 1", ",url1,url2,", ",tag1,tag2,", "available"}, 124 | new Object[]{2, 2, "Cat 2", ",url1,url2,", ",tag2,tag3,", "available"}, 125 | new Object[]{3, 2, "Cat 3", ",url1,url2,", ",tag3,tag4,", "pending"}, 126 | 127 | new Object[]{4, 1, "Dog 1", ",url1,url2,", ",tag1,tag2,", "available"}, 128 | new Object[]{5, 1, "Dog 2", ",url1,url2,", ",tag2,tag3,", "sold"}, 129 | new Object[]{6, 1, "Dog 3", ",url1,url2,", ",tag3,tag4,", "pending"}, 130 | 131 | new Object[]{7, 4, "Lion 1", ",url1,url2,", ",tag1,tag2,", "available"}, 132 | new Object[]{8, 4, "Lion 1", ",url1,url2,", ",tag2,tag3,", "available"}, 133 | new Object[]{9, 4, "Lion 1", ",url1,url2,", ",tag3,tag4,", "available"}, 134 | 135 | new Object[]{10, 3, "Rabbit 1", ",url1,url2,", ",tag3,tag4,", "available"} 136 | }); 137 | 138 | run.batch("insert into order1(id, pet_id, quantity, ship_date, status) " + 139 | "values(?, ?, ?, ?, ?)", new Object[][]{ 140 | new Object[]{1, 1, 2, new Date(), "placed"}, 141 | new Object[]{2, 1, 2, new Date(), "delivered"}, 142 | new Object[]{3, 2, 2, new Date(), "placed"}, 143 | new Object[]{4, 2, 2, new Date(), "delivered"}, 144 | new Object[]{5, 3, 2, new Date(), "placed"}, 145 | new Object[]{11, 3, 2, new Date(), "placed"}, 146 | new Object[]{12, 3, 2, new Date(), "placed"}, 147 | new Object[]{13, 3, 2, new Date(), "placed"}, 148 | new Object[]{14, 3, 2, new Date(), "placed"}, 149 | new Object[]{15, 3, 2, new Date(), "placed"} 150 | }); 151 | 152 | run.batch("insert into user(id, user_name, first_name, last_name, email, password, phone, status) " + 153 | "values(?, ?, ?, ?, ?, ?, ?, ?)", new Object[][]{ 154 | new Object[]{1, "user1", "fname1", "lname1", "email1@test.com", "XXXXXXXX", "123-456-7890", 1}, 155 | new Object[]{2, "user2", "fname2", "lname2", "email2@test.com", "XXXXXXXX", "123-456-7890", 2}, 156 | new Object[]{3, "user3", "fname3", "lname3", "email3@test.com", "XXXXXXXX", "123-456-7890", 3}, 157 | new Object[]{4, "user4", "fname4", "lname4", "email4@test.com", "XXXXXXXX", "123-456-7890", 1}, 158 | new Object[]{5, "user5", "fname5", "lname5", "email5@test.com", "XXXXXXXX", "123-456-7890", 2}, 159 | new Object[]{6, "user6", "fname6", "lname6", "email6@test.com", "XXXXXXXX", "123-456-7890", 3}, 160 | new Object[]{7, "user7", "fname7", "lname7", "email7@test.com", "XXXXXXXX", "123-456-7890", 1}, 161 | new Object[]{8, "user8", "fname8", "lname8", "email8@test.com", "XXXXXXXX", "123-456-7890", 2}, 162 | new Object[]{9, "user9", "fname9", "lname9", "email9@test.com", "XXXXXXXX", "123-456-7890", 3}, 163 | new Object[]{10, "user10", "fname10", "lname10", "email10@test.com", "XXXXXXXX", "123-456-7890", 1}, 164 | new Object[]{11, "user11", "fname11", "lname11", "email11@test.com", "XXXXXXXX", "123-456-7890", 1} 165 | }); 166 | } catch (SQLException e) { 167 | throw new RuntimeException(e); 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /example/java-jaxrs/src/main/java/com/example/resource/UserResource.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 SmartBear Software 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.resource; 18 | 19 | import javax.ws.rs.*; 20 | import javax.ws.rs.core.Response; 21 | 22 | import com.example.data.UserData; 23 | import com.example.exception.BadRequestException; 24 | import com.example.exception.NotFoundException; 25 | import com.example.exception.ApiException; 26 | import com.github.tminglei.bind.BindObject; 27 | import com.github.tminglei.bind.FormBinder; 28 | import com.github.tminglei.bind.Messages; 29 | import com.github.tminglei.swagger.SharingHolder; 30 | 31 | import java.sql.SQLException; 32 | import java.util.Arrays; 33 | import java.util.List; 34 | import java.util.Map; 35 | import java.util.ResourceBundle; 36 | 37 | import static io.swagger.models.HttpMethod.*; 38 | import static com.github.tminglei.swagger.SwaggerContext.*; 39 | import static com.github.tminglei.bind.Simple.*; 40 | import static com.github.tminglei.bind.Mappings.*; 41 | import static com.github.tminglei.bind.Constraints.*; 42 | import static com.github.tminglei.bind.Processors.*; 43 | 44 | @Path("/user") 45 | @Produces({"application/json", "application/xml"}) 46 | public class UserResource { 47 | static UserData userData = new UserData(); 48 | private ResourceBundle bundle = ResourceBundle.getBundle("bind-messages"); 49 | private Messages messages = (key) -> bundle.getString(key); 50 | 51 | static Mapping user = $(mapping( 52 | field("id", $(longv()).desc("user id").$$), 53 | field("username", $(text(required())).desc("user name").$$), 54 | field("firstName", $(text()).desc("user's first name").$$), 55 | field("lastName", $(text()).desc("user's last name").$$), 56 | field("email", $(text(email())).desc("user's email").$$), 57 | field("password", $(text()).format("password").desc("password").$$), 58 | field("phone", $(text(pattern("[\\d]{3}-[\\d]{4}-[\\d]{2}"))).desc("phone number").$$), 59 | field("status", $(intv(oneOf(Arrays.asList("1", "2", "3")))).desc("user's status").$$) 60 | )).refName("User").desc("user info").$$; 61 | 62 | static SharingHolder sharing = sharing().pathPrefix("/user").tag("user"); 63 | 64 | /// 65 | static { 66 | sharing.operation(POST, "/") 67 | .summary("create a user") 68 | .parameter(param(user).in("body")) 69 | .response(200, response()) 70 | ; 71 | } 72 | @POST 73 | public Response createUser(String data) throws BadRequestException, SQLException { 74 | BindObject bindObj = new FormBinder(messages).bind( 75 | attach(expandJson()).to(user), 76 | hashmap(entry("", data))); 77 | if (bindObj.errors().isPresent()) { 78 | throw new BadRequestException(400, "invalid pet"); 79 | } else { 80 | userData.addUser(bindObj); 81 | return Response.ok().entity("").build(); 82 | } 83 | } 84 | 85 | static { 86 | sharing.operation(POST, "/createWithArray") 87 | .summary("create multiple users") 88 | .parameter(param(list(user)).in("body")) 89 | .response(200, response()) 90 | ; 91 | } 92 | @POST 93 | @Path("/createWithArray") 94 | public Response createUsersWithArrayInput(String data) throws BadRequestException, SQLException { 95 | BindObject bindObj = new FormBinder(messages).bind( 96 | attach(expandJson()).to(list(user)), 97 | hashmap(entry("", data))); 98 | if (bindObj.errors().isPresent()) { 99 | throw new BadRequestException(400, "invalid pet"); 100 | } else { 101 | for(BindObject u : (List) bindObj.get()) { 102 | userData.addUser(u); 103 | } 104 | } 105 | return Response.ok().entity("").build(); 106 | } 107 | 108 | static { 109 | sharing.operation(POST, "/createWithList") 110 | .summary("create multiple users") 111 | .parameter(param(list(user)).in("body")) 112 | .response(200, response()) 113 | ; 114 | } 115 | @POST 116 | @Path("/createWithList") 117 | public Response createUsersWithListInput(String data) throws BadRequestException, SQLException { 118 | BindObject bindObj = new FormBinder(messages).bind( 119 | attach(expandJson()).to(list(user)), 120 | hashmap(entry("", data))); 121 | if (bindObj.errors().isPresent()) { 122 | throw new BadRequestException(400, "invalid pet"); 123 | } else { 124 | for(BindObject u : (List) bindObj.get()) { 125 | userData.addUser(u); 126 | } 127 | } 128 | return Response.ok().entity("").build(); 129 | } 130 | 131 | static { 132 | sharing.operation(PUT, "/{username}") 133 | .summary("update user") 134 | .parameter(param(text()).in("path").name("username").desc("user name")) 135 | .parameter(param(user).in("body")) 136 | .response(200, response()) 137 | ; 138 | } 139 | @PUT 140 | @Path("/{username}") 141 | public Response updateUser(@PathParam("username") String username, String data) throws BadRequestException, SQLException { 142 | BindObject bindObj = new FormBinder(messages).bind( 143 | attach(expandJson()).to(user), 144 | hashmap(entry("", data))); 145 | if (bindObj.errors().isPresent()) { 146 | throw new BadRequestException(400, "invalid pet"); 147 | } else { 148 | userData.removeUser(username); 149 | userData.addUser(bindObj); 150 | return Response.ok().entity("").build(); 151 | } 152 | } 153 | 154 | static { 155 | sharing.operation(DELETE, "/{username}") 156 | .summary("delete user") 157 | .parameter(param(text()).in("path").name("username").desc("user name")) 158 | .response(200, response()) 159 | ; 160 | } 161 | @DELETE 162 | @Path("/{username}") 163 | public Response deleteUser(String username) throws SQLException { 164 | userData.removeUser(username); 165 | return Response.ok().entity("").build(); 166 | } 167 | 168 | static { 169 | sharing.operation(GET, "/{username}") 170 | .summary("get specified user") 171 | .parameter(param(text()).in("path").name("username").desc("user name")) 172 | .response(200, response(user)) 173 | .response(404, response().description("user not found")) 174 | ; 175 | } 176 | @GET 177 | @Path("/{username}") 178 | public Response getUserByName(@PathParam("username") String username) 179 | throws ApiException, SQLException { 180 | Map user = userData.findUserByName(username); 181 | if (null != user) { 182 | return Response.ok().entity(user).build(); 183 | } else { 184 | throw new NotFoundException(404, "User not found"); 185 | } 186 | } 187 | 188 | static { 189 | sharing.operation(POST, "/login") 190 | .summary("login user") 191 | .parameter(param(text(required())).in("form").name("username")) 192 | .parameter(param(text(required())).in("form").name("password")) 193 | .response(200, response()) 194 | ; 195 | } 196 | @POST 197 | @Path("/login") 198 | public Response loginUser(@FormParam("username") String username, @FormParam("password") String password) { 199 | return Response.ok() 200 | .entity("logged in user session:" + System.currentTimeMillis()) 201 | .build(); 202 | } 203 | 204 | static { 205 | sharing.operation(GET, "/logout") 206 | .summary("logout user") 207 | .response(200, response()) 208 | ; 209 | } 210 | @GET 211 | @Path("/logout") 212 | public Response logoutUser() { 213 | return Response.ok().entity("").build(); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/test/java/com/github/tminglei/swagger/fake/DataProvidersTest.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger.fake; 2 | 3 | import com.github.tminglei.bind.Framework; 4 | import com.github.tminglei.swagger.SwaggerContext; 5 | import com.github.tminglei.swagger.bind.MappingConverter; 6 | import com.github.tminglei.swagger.bind.MappingConverterImpl; 7 | import io.swagger.models.ArrayModel; 8 | import io.swagger.models.Model; 9 | import io.swagger.models.ModelImpl; 10 | import io.swagger.models.Swagger; 11 | import io.swagger.models.properties.*; 12 | import org.junit.Test; 13 | 14 | import java.util.*; 15 | 16 | import static com.github.tminglei.bind.Constraints.*; 17 | import static com.github.tminglei.bind.Mappings.*; 18 | import static com.github.tminglei.bind.Simple.*; 19 | import static com.github.tminglei.swagger.SwaggerContext.*; 20 | import static org.junit.Assert.*; 21 | 22 | /** 23 | * Created by minglei on 4/18/17. 24 | */ 25 | public class DataProvidersTest { 26 | private Random random = new Random(); 27 | 28 | private List statuses = Arrays.asList("available", "pending", "sold"); 29 | private Framework.Mapping petStatus = 30 | $(text(oneOf(statuses))).desc("pet status in the store") 31 | .example(gen(() -> statuses.get(random.nextInt(3)))).$$; 32 | 33 | private Framework.Mapping pet = 34 | mapping( 35 | field("id", $(longv(required())).desc("pet id").example(gen("petId")).$$), 36 | field("name", $(text(required())).desc("pet name").$$), 37 | field("category", $(mapping( 38 | field("id", longv(required())), 39 | field("name", text(required())) 40 | )).desc("category belonged to").$$), 41 | field("photoUrls", $(list(text())).desc("pet's photo urls").example(Arrays.asList("http://example.com/photo1")).$$), 42 | field("tags", $(list(text())).desc("tags for the pet").example(Arrays.asList("tag1", "tag2")).$$), 43 | field("status", petStatus) 44 | ); 45 | private Framework.Mapping pet1 = 46 | $(pet).refName("Pet").desc("pet info").$$; 47 | 48 | private List optionals = Arrays.asList("category", "photoUrls", "tags", "status"); 49 | private List paramVals = Arrays.asList("id"); 50 | 51 | private Map expectedSample = 52 | hashmap( 53 | entry("id", 101L), 54 | entry("name", "kitty"), 55 | entry("category", hashmap( 56 | entry("id", 202L), 57 | entry("name", "cat1") 58 | )), 59 | entry("photoUrls", Arrays.asList("http://example.com/photo1")), 60 | entry("tags", Arrays.asList("tag1", "tag2")), 61 | entry("status", "available") 62 | ); 63 | private Map exampleVals = 64 | hashmap( 65 | entry("id", null), 66 | entry("name", null), 67 | entry("category", null), 68 | entry("photoUrls", Arrays.asList("http://example.com/photo1")), 69 | entry("tags", Arrays.asList("tag1", "tag2")), 70 | entry("status", "available") 71 | ); 72 | 73 | 74 | private MappingConverter converter = new MappingConverterImpl(); 75 | 76 | ///--- 77 | 78 | @Test 79 | public void testCollectDataProviders() { 80 | Property property = converter.mToProperty(pet); 81 | DataProvider dataProvider = DataProviders.getInstance().collect(new Swagger(), property, true); 82 | 83 | dataProvider.setRequired(true); 84 | dataProvider.setRequestParams(hashmap(entry("petId", String.valueOf(101L)))); 85 | Object generated = dataProvider.get(); 86 | checkEquals(expectedSample, generated, "", optionals, paramVals); 87 | 88 | Object collected = collectData(new Swagger(), property); 89 | checkEquals(exampleVals, collected, "", optionals, paramVals); 90 | } 91 | 92 | @Test 93 | public void testCollectDataProviders_withRefModels() { 94 | SwaggerContext.getInstance().scanAndRegisterNamedModels(pet1); 95 | Swagger swagger = SwaggerContext.getInstance().getSwagger(); 96 | 97 | Property property = converter.mToProperty(pet1); 98 | DataProvider dataProvider = DataProviders.getInstance().collect(swagger, property, true); 99 | 100 | dataProvider.setRequired(true); 101 | dataProvider.setRequestParams(hashmap(entry("petId", String.valueOf(101L)))); 102 | Object generated = dataProvider.get(); 103 | checkEquals(expectedSample, generated, "", optionals, paramVals); 104 | 105 | Object collected = collectData(swagger, property); 106 | checkEquals(exampleVals, collected, "", optionals, paramVals); 107 | } 108 | 109 | @Test 110 | public void testCollectDataProviders_clean() { 111 | Property property1 = converter.mToProperty(pet); 112 | DataProviders.getInstance().collect(new Swagger(), property1, false); 113 | assertTrue("id's example object should be type of `DataProvider`", 114 | ((ObjectProperty) property1).getProperties().get("id").getExample() instanceof DataProvider); 115 | assertTrue("status's example object shoudl be type of `DataProvider`", 116 | ((ObjectProperty) property1).getProperties().get("status").getExample() instanceof DataProvider); 117 | 118 | Property property2 = converter.mToProperty(pet); 119 | DataProviders.getInstance().collect(new Swagger(), property2, true); 120 | assertTrue("id's example object should be null", 121 | ((ObjectProperty) property2).getProperties().get("id").getExample() == null); 122 | assertTrue("status's example object should be type of `String`", 123 | ((ObjectProperty) property2).getProperties().get("status").getExample() instanceof String); 124 | } 125 | 126 | ///--- 127 | 128 | private Object collectData(Swagger swagger, Property property) { 129 | if (property.getExample() != null) { 130 | return property.getExample(); 131 | } else if (property instanceof RefProperty) { 132 | Model model = swagger.getDefinitions().get(((RefProperty) property).getSimpleRef()); 133 | if (model instanceof ArrayModel) { 134 | Property itemProperty = ((ArrayModel) model).getItems(); 135 | return Arrays.asList(collectData(swagger, itemProperty)); 136 | } else if (model instanceof ModelImpl) { 137 | Map map = new HashMap(); 138 | Map fields = model.getProperties(); 139 | for (String field : fields.keySet()) { 140 | map.put(field, collectData(swagger, fields.get(field))); 141 | } 142 | return map; 143 | } 144 | throw new IllegalArgumentException("Unsupported model type: " + model.getClass()); 145 | } else if (property instanceof ObjectProperty) { 146 | Map map = new HashMap(); 147 | Map fields = ((ObjectProperty) property).getProperties(); 148 | for (String field : fields.keySet()) { 149 | map.put(field, collectData(swagger, fields.get(field))); 150 | } 151 | return map; 152 | } else if (property instanceof MapProperty) { 153 | Property valueProperty = ((MapProperty) property).getAdditionalProperties(); 154 | return hashmap(entry("k1", collectData(swagger, valueProperty))); 155 | } else if (property instanceof ArrayProperty) { 156 | Property itemProperty = ((ArrayProperty) property).getItems(); 157 | return Arrays.asList(collectData(swagger, itemProperty)); 158 | } else { 159 | return property.getExample(); 160 | } 161 | } 162 | 163 | private void checkEquals(Object obj1, Object obj2, String path, List optionals, List paramVals) { 164 | if (obj1 == null || obj2 == null) { 165 | if (!optionals.contains(path)) 166 | assertEquals(obj1, obj2); 167 | } else if (obj1.getClass() != obj2.getClass()) { 168 | if (paramVals.contains(path) && (obj1 instanceof String || obj2 instanceof String)) ; 169 | else assertEquals(obj1.getClass(), obj2.getClass()); 170 | } else if (obj1 instanceof Map) { 171 | Map map1 = (Map) obj1; 172 | Map map2 = (Map) obj2; 173 | 174 | assertEquals(map1.size(), map2.size()); 175 | for (Object key : map1.keySet()) { 176 | String path1 = path + (path.isEmpty() ? "" : ".") +key; 177 | checkEquals(map1.get(key), map2.get(key), path1, optionals, paramVals); 178 | } 179 | } else if (obj1 instanceof List) { 180 | List list1 = new ArrayList((List) obj1); 181 | List list2 = new ArrayList((List) obj2); 182 | 183 | assertEquals(list1.size(), list2.size()); 184 | Collections.sort(list1); 185 | Collections.sort(list2); 186 | for (int i = 0; i < list1.size(); i++) { 187 | String path1 = path + "[" + i + "]"; 188 | checkEquals(list1.get(i), list2.get(i), path1, optionals, paramVals); 189 | } 190 | } else { 191 | assertEquals(obj1.getClass(), obj2.getClass()); 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/route/RouteImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger.route; 2 | 3 | import com.github.tminglei.swagger.fake.DataProvider; 4 | import io.swagger.models.HttpMethod; 5 | 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.regex.Matcher; 11 | 12 | import static com.github.tminglei.swagger.SimpleUtils.*; 13 | 14 | /** 15 | * Created by minglei on 4/17/17. 16 | */ 17 | public class RouteImpl implements Route { 18 | private final HttpMethod method; 19 | private final String path; 20 | private final boolean implemented; 21 | private final DataProvider dataProvider; 22 | 23 | private final List allPathElements; 24 | private final List namedParamElements; 25 | private final List splatParamElements; 26 | private final List staticPathElements; 27 | 28 | public RouteImpl(HttpMethod method, String pathPattern, 29 | boolean implemented, DataProvider dataProvider) { 30 | this.method = notEmpty(method, "method cannot be null"); 31 | this.path = notEmpty(pathPattern, "pathPattern cannot be null"); 32 | this.implemented = implemented; 33 | this.dataProvider = notEmpty(dataProvider, "dataProvider cannot be null"); 34 | 35 | this.allPathElements = new ArrayList<>(); 36 | this.splatParamElements = new ArrayList<>(); 37 | this.staticPathElements = new ArrayList<>(); 38 | this.namedParamElements = new ArrayList<>(); 39 | 40 | extractPathElements(pathPattern); 41 | } 42 | 43 | @Override 44 | public HttpMethod getMethod() { 45 | return method; 46 | } 47 | 48 | @Override 49 | public String getPath() { 50 | return path; 51 | } 52 | 53 | @Override 54 | public Map getPathParams(String path) { 55 | Map params = new HashMap<>(); 56 | String[] pathTokens = RouteHelper.getPathElements(path); 57 | for (PathNamedParamElement pathParam : namedParamElements) { 58 | String value = RouteHelper.urlDecodeForPathParams(pathTokens[pathParam.index()]); 59 | params.put(pathParam.name(), value); 60 | } 61 | return params; 62 | } 63 | 64 | @Override 65 | public boolean isImplemented() { 66 | return implemented; 67 | } 68 | 69 | @Override 70 | public DataProvider getDataProvider() { 71 | return dataProvider; 72 | } 73 | 74 | private void extractPathElements(String pathPattern) { 75 | pathPattern = pathPattern.matches("<[^>]+>") ? pathPattern 76 | : pathPattern.replaceAll("/\\{([^\\}]+)\\}", "/:$1"); // replace `/{id}` with `/:id` 77 | Matcher m = RouteHelper.CUSTOM_REGEX_PATTERN.matcher(pathPattern); 78 | Map regexMap = getRegexMap(pathPattern, m); 79 | String path = m.replaceAll(""); 80 | 81 | String[] pathElements = RouteHelper.getPathElements(path); 82 | for (int i = 0; i < pathElements.length; i++) { 83 | String currentElement = pathElements[i]; 84 | if (currentElement.startsWith(RouteHelper.PARAM_PREFIX)) { 85 | currentElement = currentElement.substring(1); 86 | PathNamedParamElement named = 87 | new PathNamedParamElement(currentElement, i, regexMap.get(currentElement)); 88 | namedParamElements.add(named); 89 | allPathElements.add(named); 90 | } else if (currentElement.equals(RouteHelper.WILDCARD)) { 91 | PathSplatParamElement splat = new PathSplatParamElement(i); 92 | splatParamElements.add(splat); 93 | allPathElements.add(splat); 94 | } else { 95 | if (currentElement.trim().length() < 1) continue; 96 | PathStaticElement staticElem = new PathStaticElement(currentElement, i); 97 | staticPathElements.add(staticElem); 98 | allPathElements.add(staticElem); 99 | } 100 | } 101 | } 102 | 103 | /* 104 | * Returns a map of named param names to their regex, for 105 | * named params that have a regex. 106 | * e.g. {"name" -> "[a-z]+"} 107 | */ 108 | private Map getRegexMap(String path, Matcher m) { 109 | Map regexMap = new HashMap<>(); 110 | while (m.find()) { 111 | String regex = path.substring(m.start() + 1, m.end() - 1); 112 | int namedParamStart = m.start() - 1; 113 | int namedParamEnd = m.start(); 114 | String namedParamName = path.substring(namedParamStart, namedParamEnd); 115 | while (!namedParamName.startsWith(RouteHelper.PARAM_PREFIX)) { 116 | namedParamStart--; 117 | namedParamName = path.substring(namedParamStart, namedParamEnd); 118 | } 119 | namedParamName = path.substring(namedParamStart + 1, namedParamEnd); 120 | regexMap.put(namedParamName, regex); 121 | } 122 | return regexMap; 123 | } 124 | 125 | public List getPathElements() { 126 | return new ArrayList<>(allPathElements); 127 | } 128 | 129 | public List getNamedParameterElements() { 130 | return new ArrayList<>(namedParamElements); 131 | } 132 | 133 | public List getSplatParameterElements() { 134 | return new ArrayList<>(splatParamElements); 135 | } 136 | 137 | public List getStaticPathElements() { 138 | return new ArrayList<>(staticPathElements); 139 | } 140 | 141 | /** 142 | * Use of this method assumes the path given matches this Route. 143 | * 144 | * @param paramName param name 145 | * @param path path 146 | * @return the value of the named parameter in the path, or null if 147 | * no named parameter exists with the given name 148 | */ 149 | public String getNamedParameter(String paramName, String path) { 150 | List pathParams = getNamedParameterElements(); 151 | String[] pathTokens = RouteHelper.getPathElements(path); 152 | 153 | for (PathNamedParamElement pathParam : pathParams) { 154 | if (pathParam.name().equals(paramName)) { 155 | return RouteHelper.urlDecodeForPathParams(pathTokens[pathParam.index()]); 156 | } 157 | } 158 | return null; 159 | } 160 | 161 | /** 162 | * Use of this method assumes the path given matches this Route. 163 | * 164 | * @param index the param index 165 | * @param path path 166 | * @return the value of the splat parameter at the given index, 167 | * or null if the splat parameter index does not exist 168 | */ 169 | public String getSplatParameter(int index, String path) { 170 | String[] splat = splat(path); 171 | if (index > splat.length - 1) { 172 | return null; 173 | } 174 | return splat[index]; 175 | } 176 | 177 | /** 178 | * Use of this method assumes the path given matches this Route. 179 | * 180 | * @param path path 181 | * @return splat params' values 182 | */ 183 | public String[] splat(String path) { 184 | List splatParams = getSplatParameterElements(); 185 | String[] pathTokens = RouteHelper.getPathElements(path, false); 186 | String[] splat = new String[splatParams.size()]; 187 | 188 | for (int i = 0; i < splatParams.size(); i++) { 189 | PathSplatParamElement splatParam = splatParams.get(i); 190 | splat[i] = RouteHelper.urlDecodeForPathParams(pathTokens[splatParam.index()]); 191 | 192 | if (i + 1 == splatParams.size() && endsWithSplat()) { 193 | /* this is the last splat param and the route ends with splat */ 194 | for (int j = splatParam.index() + 1; j < pathTokens.length; j++) { 195 | splat[i] = splat[i] + RouteHelper.PATH_ELEMENT_SEPARATOR + RouteHelper.urlDecodeForPathParams(pathTokens[j]); 196 | } 197 | } 198 | } 199 | return splat; 200 | } 201 | 202 | private boolean endsWithSplat() { 203 | return path.endsWith(RouteHelper.WILDCARD); 204 | } 205 | 206 | public boolean endsWithPathSeparator() { 207 | return path.endsWith(RouteHelper.PATH_ELEMENT_SEPARATOR); 208 | } 209 | 210 | public boolean hasPathElements() { 211 | return !allPathElements.isEmpty(); 212 | } 213 | 214 | public String toString() { 215 | return "Route(method=" + method 216 | + ", path=" + path 217 | + ", implemented=" + implemented 218 | + ")"; 219 | } 220 | 221 | public int hashCode() { 222 | int hash = 1; 223 | hash = hash * 13 + (path == null ? 0 : path.hashCode()); 224 | return hash; 225 | } 226 | 227 | public boolean equals(Object o) { 228 | if (o == null) return false; 229 | if (o == this) return true; 230 | if (!(o instanceof RouteImpl)) return false; 231 | 232 | RouteImpl that = (RouteImpl) o; 233 | return this.path == null ? that.path == null 234 | : this.path.equals(that.path); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/test/java/com/github/tminglei/swagger/bind/MappingConverterImplTest.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger.bind; 2 | 3 | import io.swagger.models.ArrayModel; 4 | import io.swagger.models.Model; 5 | import io.swagger.models.ModelImpl; 6 | import io.swagger.models.parameters.BodyParameter; 7 | import io.swagger.models.parameters.Parameter; 8 | import io.swagger.models.parameters.PathParameter; 9 | import io.swagger.models.parameters.QueryParameter; 10 | import io.swagger.models.properties.*; 11 | import org.junit.Test; 12 | 13 | import java.util.Arrays; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | import static org.junit.Assert.*; 18 | import static com.github.tminglei.swagger.SwaggerContext.*; 19 | import static com.github.tminglei.bind.Simple.*; 20 | import static com.github.tminglei.bind.Mappings.*; 21 | import static com.github.tminglei.bind.Constraints.*; 22 | 23 | /** 24 | * Created by tminglei on 9/10/15. 25 | */ 26 | public class MappingConverterImplTest { 27 | private MappingConverterImpl converter = new MappingConverterImpl(); 28 | 29 | @Test 30 | public void testMtoParameters_Single() { 31 | List params = converter.mToParameters("id", $(longv(required())).in("query").desc("id").$$); 32 | 33 | assertEquals(params.size(), 1); 34 | 35 | assertTrue(params.get(0) instanceof QueryParameter); 36 | QueryParameter p = (QueryParameter) params.get(0); 37 | 38 | assertEquals(p.getType(), "integer"); 39 | assertEquals(p.getFormat(), "int64"); 40 | assertEquals(p.getDescription(), "id"); 41 | assertEquals(p.getRequired(), true); 42 | 43 | /// 'in' is required!!! 44 | try { 45 | List params1 = converter.mToParameters("id", $(longv(required())).desc("id").$$); 46 | assertTrue("shouldn't", false); 47 | } catch (Exception e) { 48 | assertEquals(e.getMessage(), "in is required!!!"); 49 | } 50 | } 51 | 52 | @Test 53 | public void testMtoParameters_Multiple() { 54 | List params = converter.mToParameters("", mapping( 55 | field("id", $(intv()).in("path").desc("id").$$), 56 | field("data", $(mapping( 57 | field("id", $(intv()).desc("id").$$), 58 | field("name", $(text(required())).desc("name").$$) 59 | )).in("body").$$) 60 | )); 61 | 62 | assertEquals(params.size(), 2); 63 | 64 | /// 65 | assertTrue(params.get(0) instanceof PathParameter); 66 | PathParameter p1 = (PathParameter) params.get(0); 67 | 68 | assertEquals(p1.getType(), "integer"); 69 | assertEquals(p1.getFormat(), "int32"); 70 | assertEquals(p1.getDescription(), "id"); 71 | assertEquals(p1.getRequired(), true); 72 | 73 | /// 74 | assertTrue(params.get(1) instanceof BodyParameter); 75 | BodyParameter p2 = (BodyParameter) params.get(1); 76 | ModelImpl model = (ModelImpl) p2.getSchema(); 77 | 78 | assertEquals(model.getType(), "object"); 79 | assertEquals(model.getRequired(), Arrays.asList("name")); 80 | assertTrue(model.getProperties() != null); 81 | assertEquals(model.getProperties().size(), 2); 82 | assertTrue(model.getProperties().get("id") instanceof IntegerProperty); 83 | assertTrue(model.getProperties().get("name") instanceof StringProperty); 84 | 85 | /// 'in' is required!!! 86 | try { 87 | List params1 = converter.mToParameters("", mapping( 88 | field("id", $(intv()).desc("id").$$), 89 | field("data", $(mapping( 90 | field("id", $(intv()).desc("id").$$), 91 | field("name", $(text(required())).desc("name").$$) 92 | )).in("body").$$) 93 | )); 94 | assertEquals(false, "shouldn't happen"); 95 | } catch (Exception e) { 96 | assertEquals(e.getMessage(), "in is required!!!"); 97 | } 98 | } 99 | 100 | @Test 101 | public void testMtoModel() { 102 | Model model = converter.mToModel(list(mapping( 103 | field("id", $(intv()).desc("id").$$), 104 | field("name", $(text(required())).desc("name").$$) 105 | ))); 106 | assertTrue(model instanceof ArrayModel); 107 | ArrayModel m = (ArrayModel) model; 108 | 109 | assertTrue(m.getItems() instanceof ObjectProperty); 110 | ObjectProperty p = (ObjectProperty) m.getItems(); 111 | 112 | assertTrue(p.getProperties() != null); 113 | assertEquals(p.getProperties().size(), 2); 114 | assertTrue(p.getProperties().get("id") instanceof IntegerProperty); 115 | assertTrue(p.getProperties().get("name") instanceof StringProperty); 116 | 117 | IntegerProperty p1 = (IntegerProperty) p.getProperties().get("id"); 118 | assertEquals(p1.getRequired(), false); 119 | assertEquals(p1.getFormat(), "int32"); 120 | assertEquals(p1.getDescription(), "id"); 121 | 122 | StringProperty p2 = (StringProperty) p.getProperties().get("name"); 123 | assertEquals(p2.getRequired(), true); 124 | assertEquals(p2.getDescription(), "name"); 125 | } 126 | 127 | @Test 128 | public void testMtoProperty() { 129 | Property prop = converter.mToProperty(list(mapping( 130 | field("id", $(intv()).desc("id").$$), 131 | field("name", $(text(required())).desc("name").$$) 132 | ))); 133 | assertTrue(prop instanceof ArrayProperty); 134 | ArrayProperty p = (ArrayProperty) prop; 135 | 136 | assertTrue(p.getItems() instanceof ObjectProperty); 137 | ObjectProperty p0 = (ObjectProperty) p.getItems(); 138 | 139 | assertTrue(p0.getProperties() != null); 140 | assertEquals(p0.getProperties().size(), 2); 141 | assertTrue(p0.getProperties().get("id") instanceof IntegerProperty); 142 | assertTrue(p0.getProperties().get("name") instanceof StringProperty); 143 | 144 | IntegerProperty p1 = (IntegerProperty) p0.getProperties().get("id"); 145 | assertEquals(p1.getRequired(), false); 146 | assertEquals(p1.getFormat(), "int32"); 147 | assertEquals(p1.getDescription(), "id"); 148 | 149 | StringProperty p2 = (StringProperty) p0.getProperties().get("name"); 150 | assertEquals(p2.getRequired(), true); 151 | assertEquals(p2.getDescription(), "name"); 152 | } 153 | 154 | @Test 155 | public void testScanModels() { 156 | List> models = converter.scanModels($(mapping( 157 | field("id", longv()), 158 | field("props1", $(mapping( 159 | field("id", longv()), 160 | field("name", text()) 161 | )).refName("props").$$), 162 | field("props2", $(mapping( 163 | field("id", longv()), 164 | field("name", text()), 165 | field("extra", text()) 166 | )).refName("props").$$), 167 | field("props3", $(mapping( 168 | field("id", longv()), 169 | field("name", text()) 170 | )).refName("props").$$) 171 | )).refName("test").$$); 172 | 173 | assertEquals(models.size(), 4); 174 | assertEquals(models.get(0).getKey(), "test"); 175 | assertTrue(models.get(0).getValue().getProperties().get("props1") instanceof RefProperty); 176 | assertEquals(((RefProperty) models.get(0).getValue().getProperties().get("props1")).get$ref(), "#/definitions/props"); 177 | assertTrue(models.get(0).getValue().getProperties().get("props2") instanceof RefProperty); 178 | assertEquals(((RefProperty) models.get(0).getValue().getProperties().get("props2")).get$ref(), "#/definitions/props"); 179 | assertTrue(models.get(0).getValue().getProperties().get("props3") instanceof RefProperty); 180 | assertEquals(((RefProperty) models.get(0).getValue().getProperties().get("props3")).get$ref(), "#/definitions/props"); 181 | 182 | assertEquals(models.get(1).getKey(), "props"); 183 | assertEquals(models.get(2).getKey(), "props"); 184 | assertEquals(models.get(3).getKey(), "props"); 185 | assertFalse(models.get(1).getValue().equals(models.get(2).getValue())); 186 | assertTrue(models.get(1).getValue().equals(models.get(3).getValue())); 187 | } 188 | 189 | /// 190 | @Test 191 | public void testIsPrimitive() { 192 | assertEquals(converter.isPrimitive(longv(), true), true); 193 | assertEquals(converter.isPrimitive(list(longv()), true), true); 194 | assertEquals(converter.isPrimitive(list(longv()), false), false); 195 | assertEquals(converter.isPrimitive(mapping(), true), false); 196 | } 197 | 198 | @Test 199 | public void testTargetType() { 200 | assertEquals(converter.targetType(longv()), "integer"); 201 | assertEquals(converter.targetType(list(intv())), "array"); 202 | assertEquals(converter.targetType(mapping()), "object"); 203 | assertEquals(converter.targetType(uuid()), "string"); 204 | assertEquals(converter.targetType(bigDecimal()), "number"); 205 | } 206 | 207 | @Test 208 | public void testFormat() { 209 | assertEquals(converter.format(datetime()), "date-time"); 210 | assertEquals(converter.format(text(email())), "email"); 211 | assertEquals(converter.format(longv(email())), "int64"); 212 | assertEquals(converter.format($(text()).format("json").$$), "json"); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/SwaggerContext.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger; 2 | 3 | import com.github.tminglei.bind.Framework; 4 | import com.github.tminglei.swagger.bind.Attachment; 5 | import com.github.tminglei.swagger.bind.MParamBuilder; 6 | import com.github.tminglei.swagger.bind.MappingConverterImpl; 7 | import com.github.tminglei.swagger.bind.MappingConverter; 8 | import com.github.tminglei.swagger.fake.*; 9 | import com.github.tminglei.swagger.route.RouteFactory; 10 | import com.github.tminglei.swagger.route.Router; 11 | import com.github.tminglei.swagger.route.RouteFactoryImpl; 12 | import com.github.tminglei.swagger.route.TreeRouterImpl; 13 | import io.swagger.models.*; 14 | import io.swagger.models.auth.ApiKeyAuthDefinition; 15 | import io.swagger.models.auth.BasicAuthDefinition; 16 | import io.swagger.models.auth.In; 17 | import io.swagger.models.auth.OAuth2Definition; 18 | import io.swagger.models.properties.Property; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | 22 | import java.util.AbstractMap; 23 | import java.util.HashMap; 24 | import java.util.Map; 25 | import java.util.function.Supplier; 26 | 27 | import static com.github.tminglei.swagger.SimpleUtils.*; 28 | 29 | /** 30 | * Context class to hold swagger instance and related helper methods 31 | */ 32 | public class SwaggerContext { 33 | private static final Logger logger = LoggerFactory.getLogger(SwaggerContext.class); 34 | 35 | private static SwaggerContext INSTANCE = new SwaggerContext( 36 | new Swagger(), new MappingConverterImpl(), new TreeRouterImpl(), new RouteFactoryImpl(), new DataWriterImpl()); 37 | 38 | private Swagger swagger; 39 | private MappingConverter mConverter; 40 | private Router router; 41 | private RouteFactory routeFactory; 42 | private DataWriter dataWriter; 43 | 44 | private Map, Boolean> implemented; 45 | private Map, String> origPaths; 46 | 47 | public SwaggerContext(Swagger swagger, MappingConverter mConverter, 48 | Router router, RouteFactory routeFactory, DataWriter dataWriter) { 49 | this.swagger = swagger; 50 | this.mConverter = mConverter; 51 | this.router = router; 52 | this.routeFactory = routeFactory; 53 | this.dataWriter = dataWriter; 54 | this.implemented = new HashMap<>(); 55 | this.origPaths = new HashMap<>(); 56 | } 57 | 58 | public static SwaggerContext getInstance() { 59 | return SwaggerContext.INSTANCE; 60 | } 61 | public static void setInstance(SwaggerContext instance) { 62 | SwaggerContext.INSTANCE = instance; 63 | } 64 | 65 | ///--- 66 | 67 | public Swagger getSwagger() { 68 | return this.swagger; 69 | } 70 | 71 | public MappingConverter getMappingConverter() { 72 | return this.mConverter; 73 | } 74 | public void setMappingConverter(MappingConverter mConverter) { 75 | this.mConverter = mConverter; 76 | } 77 | 78 | public Router getRouter() { 79 | return this.router; 80 | } 81 | public void setRouter(Router router) { 82 | this.router = router; 83 | } 84 | 85 | public RouteFactory getRouteFactory() { 86 | return this.routeFactory; 87 | } 88 | public void setRouteFactory(RouteFactory routeFactory) { 89 | this.routeFactory = routeFactory; 90 | } 91 | 92 | public DataWriter getDataWriter() { 93 | return this.dataWriter; 94 | } 95 | public void setDataWriter(DataWriter dataWriter) { 96 | this.dataWriter = dataWriter; 97 | } 98 | 99 | ///--- 100 | 101 | public SharingHolder mkSharing() { 102 | return new SharingHolder(this); 103 | } 104 | 105 | public ExOperation mkOperation(HttpMethod method, String path) { 106 | notEmpty(path, "'path' CAN'T be null or empty!!!"); 107 | notEmpty(method, "'method' CAN'T be null or empty!!!"); 108 | 109 | synchronized (swagger) { 110 | String origPath = path; 111 | path = path.replaceAll("<[^>]+>", "").replaceAll("/:([^/]+)", "/{$1}"); // replace `/:id<[0-9]+>` with `/{id}` 112 | if (swagger.getPath(path) == null) { 113 | logger.info(">>> adding path - '" + path + "'"); 114 | swagger.path(path, new Path()); 115 | } 116 | 117 | Path pathObj = swagger.getPath(path); 118 | if (pathObj.getOperationMap().get(method) != null) { 119 | throw new IllegalArgumentException("DUPLICATED operation - " + method + " '" + path + "'"); 120 | } 121 | 122 | logger.info(">>> adding operation - " + method + " '" + path + "'"); 123 | pathObj.set(method.name().toLowerCase(), new ExOperation(this, method, path)); 124 | implemented.put(entry(method, path), true); // set implemented by default 125 | String prevPath = origPaths.put(entry(method, path), origPath); 126 | if (prevPath != null) throw new IllegalArgumentException( 127 | "`" + path + "` was repeatedly defined by `" + prevPath + "` and `" + origPath + "`!!!"); 128 | return (ExOperation) pathObj.getOperationMap().get(method); 129 | } 130 | } 131 | 132 | public MParamBuilder mkParamBuilder(Framework.Mapping mapping) { 133 | return new MParamBuilder(this, mapping); 134 | } 135 | 136 | public Property mkProperty(Framework.Mapping mapping) { 137 | scanAndRegisterNamedModels(mapping); 138 | return mConverter.mToProperty(mapping); 139 | } 140 | 141 | public Model mkModel(Framework.Mapping mapping) { 142 | scanAndRegisterNamedModels(mapping); 143 | return mConverter.mToModel(mapping); 144 | } 145 | 146 | public Response mkResponse(Framework.Mapping mapping) { 147 | scanAndRegisterNamedModels(mapping); 148 | return mConverter.mToResponse(mapping); 149 | } 150 | 151 | public void scanAndRegisterNamedModels(Framework.Mapping mapping) { 152 | synchronized (swagger) { 153 | mConverter.scanModels(mapping).forEach(p -> { 154 | Model existed = swagger.getDefinitions() == null ? null : swagger.getDefinitions().get(p.getKey()); 155 | if (existed == null) swagger.model(p.getKey(), p.getValue()); 156 | else if (!existed.equals(p.getValue())) { 157 | throw new IllegalArgumentException("CONFLICTED model definitions for '" + p.getKey() + "'!!!"); 158 | } 159 | }); 160 | } 161 | } 162 | 163 | public void markNotImplemented(HttpMethod method, String path) { 164 | Boolean prevValue = implemented.put(entry(method, path), false); 165 | if (prevValue == null) throw new IllegalStateException(method + " " + path + "NOT defined!!!"); 166 | } 167 | 168 | public Boolean isImplemented(HttpMethod method, String path, boolean errIfAbsent) { 169 | Boolean value = implemented.get(entry(method, path)); 170 | if (value == null && errIfAbsent) throw new IllegalStateException(method + " " + path + "NOT defined!!!"); 171 | return value; 172 | } 173 | 174 | public String getOriginalPath(HttpMethod method, String path, boolean errIfAbsent) { 175 | String value = origPaths.get(entry(method, path)); 176 | if (value == null && errIfAbsent) throw new IllegalStateException(method + " " + path + "NOT defined!!!"); 177 | return value; 178 | } 179 | 180 | /////////////////////////////////////////////////////////////////////////// 181 | /// static convenient methods 182 | /////////////////////////////////////////////////////////////////////////// 183 | 184 | public static Swagger swagger() { 185 | return INSTANCE.getSwagger(); 186 | } 187 | 188 | public static SharingHolder sharing() { 189 | return INSTANCE.mkSharing(); 190 | } 191 | 192 | public static ExOperation operation(HttpMethod method, String path) { 193 | return INSTANCE.mkOperation(method, path); 194 | } 195 | 196 | public static MParamBuilder param(Framework.Mapping mapping) { 197 | return INSTANCE.mkParamBuilder(mapping); 198 | } 199 | public static Property prop(Framework.Mapping mapping) { 200 | return INSTANCE.mkProperty(mapping); 201 | } 202 | public static Model model(Framework.Mapping mapping) { 203 | return INSTANCE.mkModel(mapping); 204 | } 205 | public static Response response(Framework.Mapping mapping) { 206 | return INSTANCE.mkResponse(mapping); 207 | } 208 | public static Response response() { 209 | return new Response(); 210 | } 211 | 212 | public static Info info() { 213 | return new Info(); 214 | } 215 | public static Tag tag(String name) { 216 | return new Tag().name(name); 217 | } 218 | public static Contact contact() { 219 | return new Contact(); 220 | } 221 | public static License license() { 222 | return new License(); 223 | } 224 | public static BasicAuthDefinition basicAuth() { 225 | return new BasicAuthDefinition(); 226 | } 227 | public static ApiKeyAuthDefinition apiKeyAuth(String name, In in) { 228 | return new ApiKeyAuthDefinition(name, in); 229 | } 230 | public static OAuth2Definition oAuth2() { 231 | return new OAuth2Definition(); 232 | } 233 | public static ExternalDocs externalDocs() { 234 | return new ExternalDocs(); 235 | } 236 | 237 | ///--- 238 | 239 | public static Attachment.Builder $(Framework.Mapping mapping) { 240 | return new Attachment.Builder<>(mapping); 241 | } 242 | 243 | public static DataProvider gen(Supplier provider) { 244 | return new AbstractDataProvider("root") { 245 | @Override 246 | protected Object create() { 247 | return provider.get(); 248 | } 249 | }; 250 | } 251 | 252 | public static DataProvider gen(String paramKey) { 253 | return new ParamDataProvider(paramKey); 254 | } 255 | 256 | public static Map.Entry entry(K key, V value) { 257 | return new AbstractMap.SimpleImmutableEntry(key, value); 258 | } 259 | 260 | public static Map hashmap(Map.Entry... entries) { 261 | Map result = new HashMap<>(); 262 | for(Map.Entry entry : entries) { 263 | result.put(entry.getKey(), entry.getValue()); 264 | } 265 | return result; 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/fake/DataProviders.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger.fake; 2 | 3 | import com.github.javafaker.Faker; 4 | import com.mifmif.common.regex.Generex; 5 | import io.swagger.models.ArrayModel; 6 | import io.swagger.models.Model; 7 | import io.swagger.models.ModelImpl; 8 | import io.swagger.models.Swagger; 9 | import io.swagger.models.properties.*; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import java.time.LocalDateTime; 14 | import java.time.ZoneId; 15 | import java.util.Collections; 16 | import java.util.Date; 17 | import java.util.Map; 18 | import java.util.UUID; 19 | import java.util.concurrent.TimeUnit; 20 | import java.util.stream.Collectors; 21 | 22 | import static com.github.tminglei.swagger.SimpleUtils.isEmpty; 23 | import static com.github.tminglei.swagger.SwaggerContext.entry; 24 | import static com.github.tminglei.swagger.SwaggerContext.gen; 25 | 26 | /** 27 | * Created by minglei on 4/15/17. 28 | */ 29 | public class DataProviders { 30 | private static final Logger logger = LoggerFactory.getLogger(DataProviders.class); 31 | 32 | private static DataProviders INSTANCE = new DataProviders(); 33 | 34 | private Faker faker = new Faker(); 35 | 36 | public static DataProviders getInstance() { 37 | return DataProviders.INSTANCE; 38 | } 39 | public static void setInstance(DataProviders instance) { 40 | DataProviders.INSTANCE = instance; 41 | } 42 | 43 | /** 44 | * collect data providers from schema 45 | * 46 | * @param swagger the hosting swagger object 47 | * @param schema the schema object 48 | * @param clean whether to clean data providers in the schema 49 | * @return organized data provider 50 | */ 51 | public DataProvider collect(Swagger swagger, Property schema, boolean clean) { 52 | if (schema == null) return new ConstDataProvider(null); 53 | 54 | Object example = schema.getExample(); 55 | if (example instanceof DataProvider) { 56 | DataProvider provider = (DataProvider) example; 57 | if (clean) { 58 | provider.setRequired(true); 59 | schema.setExample(provider.get()); 60 | } 61 | provider.setRequired(schema.getRequired()); 62 | return provider; 63 | } else if (example != null) { 64 | DataProvider dataProvider = new ConstDataProvider(example); 65 | dataProvider.setRequired(schema.getRequired()); 66 | return dataProvider; 67 | } 68 | 69 | ///--- 70 | DataProvider dataProvider = null; 71 | if (schema instanceof RefProperty) { 72 | dataProvider = collectRefProperty(swagger, (RefProperty) schema, clean); 73 | } 74 | else if (schema instanceof ObjectProperty) { 75 | dataProvider = collectObjectProperty(swagger, (ObjectProperty) schema, clean); 76 | } 77 | else if (schema instanceof MapProperty) { 78 | dataProvider = collectMapProperty(swagger, (MapProperty) schema, clean); 79 | } 80 | else if (schema instanceof ArrayProperty) { 81 | dataProvider = collectArrayProperty(swagger, (ArrayProperty) schema, clean); 82 | } 83 | else if (schema instanceof AbstractNumericProperty) { 84 | dataProvider = collectNumericProperty(swagger, (AbstractNumericProperty) schema, clean); 85 | } 86 | else if (schema instanceof ByteArrayProperty || schema instanceof BinaryProperty) { 87 | dataProvider = collectByteProperty(swagger, schema, clean); 88 | } 89 | else if (schema instanceof DateProperty || schema instanceof DateTimeProperty) { 90 | dataProvider = collectDateProperty(swagger, schema, clean); 91 | } 92 | else if (schema instanceof EmailProperty) { 93 | dataProvider = collectEmailProperty(swagger, (EmailProperty) schema, clean); 94 | } 95 | else if (schema instanceof FileProperty) { 96 | dataProvider = collectFileProperty(swagger, (FileProperty) schema, clean); 97 | } 98 | else if (schema instanceof UUIDProperty) { 99 | dataProvider = collectUUIDProperty(swagger, (UUIDProperty) schema, clean); 100 | } 101 | else if (schema instanceof BooleanProperty) { 102 | dataProvider = collectBooleanProperty(swagger, (BooleanProperty) schema, clean); 103 | } 104 | else if (schema instanceof StringProperty) { 105 | dataProvider = collectStringProperty(swagger, (StringProperty) schema, clean); 106 | } 107 | else if (schema instanceof PasswordProperty) { 108 | dataProvider = collectPasswordProperty(swagger, (PasswordProperty) schema, clean); 109 | } 110 | 111 | if (dataProvider != null) { 112 | dataProvider.setRequired(schema.getRequired()); 113 | return dataProvider; 114 | } 115 | 116 | throw new IllegalArgumentException("Unsupported property type: " + schema.getClass()); 117 | } 118 | 119 | protected DataProvider collectRefProperty(Swagger swagger, RefProperty schema, boolean clean) { 120 | Model model = swagger.getDefinitions() != null ? swagger.getDefinitions().get(schema.getSimpleRef()) : null; 121 | if (model == null) throw new IllegalArgumentException("CAN'T find model for " + schema.getSimpleRef()); 122 | 123 | if (model instanceof ArrayModel) { 124 | DataProvider itemProvider = collect(swagger, ((ArrayModel) model).getItems(), clean); 125 | return new ListDataProvider(itemProvider, schema.getSimpleRef()); 126 | } else if (model instanceof ModelImpl) { 127 | Map fields = 128 | (model.getProperties() != null ? model.getProperties() : Collections.emptyMap()).entrySet().stream() 129 | .map(e -> entry(e.getKey(), collect(swagger, e.getValue(), clean))) 130 | .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 131 | 132 | return new ObjectDataProvider(fields, schema.getSimpleRef()); 133 | } 134 | 135 | throw new IllegalArgumentException("Unsupported model type: " + model.getClass()); 136 | } 137 | 138 | protected DataProvider collectObjectProperty(Swagger swagger, ObjectProperty schema, boolean clean) { 139 | Map fields = 140 | (schema.getProperties() != null ? schema.getProperties() : Collections.emptyMap()).entrySet().stream() 141 | .map(e -> entry(e.getKey(), collect(swagger, e.getValue(), clean))) 142 | .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 143 | 144 | return new ObjectDataProvider(fields); 145 | } 146 | 147 | protected DataProvider collectMapProperty(Swagger swagger, MapProperty schema, boolean clean) { 148 | DataProvider valueProvider = collect(swagger, schema.getAdditionalProperties(), clean); 149 | return new MapDataProvider(valueProvider); 150 | } 151 | 152 | protected DataProvider collectArrayProperty(Swagger swagger, ArrayProperty schema, boolean clean) { 153 | DataProvider itemProvider = collect(swagger, schema.getItems(), clean); 154 | return new ListDataProvider(itemProvider); 155 | } 156 | 157 | protected DataProvider collectNumericProperty(Swagger swagger, AbstractNumericProperty schema, boolean clean) { 158 | long min = schema.getMinimum() != null ? schema.getMinimum().longValue() : Long.MIN_VALUE; 159 | long max = schema.getMaximum() != null ? schema.getMaximum().longValue() : Long.MAX_VALUE; 160 | boolean noBetween = (min == Long.MIN_VALUE && max == Long.MAX_VALUE); 161 | 162 | if (schema instanceof BaseIntegerProperty) 163 | return gen(() -> noBetween ? faker.number().randomNumber() : faker.number().numberBetween(min, max)); 164 | else if (schema instanceof DecimalProperty) 165 | return gen(() -> faker.number().randomDouble(10, min, max)); 166 | 167 | throw new IllegalArgumentException("Unsupported property type: " + schema.getClass()); 168 | } 169 | 170 | protected DataProvider collectByteProperty(Swagger swagger, Property schema, boolean clean) { 171 | if (schema instanceof ByteArrayProperty) 172 | return new ConstDataProvider("[ByteArray]"); 173 | else if (schema instanceof BinaryProperty) 174 | return new ConstDataProvider("[Binary]"); 175 | 176 | throw new IllegalArgumentException("Unsupported property type: " + schema.getClass()); 177 | } 178 | 179 | protected DataProvider collectDateProperty(Swagger swagger, Property schema, boolean clean) { 180 | if (schema instanceof DateTimeProperty) 181 | return gen(() -> { 182 | Date dateTime = faker.date().future((int) faker.random().nextDouble(), TimeUnit.DAYS); 183 | return LocalDateTime.ofInstant(dateTime.toInstant(), ZoneId.of("UTC")); 184 | }); 185 | else if (schema instanceof DateProperty) 186 | return gen(() -> { 187 | Date dateTime = faker.date().future((int) faker.random().nextDouble(), TimeUnit.DAYS); 188 | return LocalDateTime.ofInstant(dateTime.toInstant(), ZoneId.of("UTC")).toLocalDate(); 189 | }); 190 | 191 | throw new IllegalArgumentException("Unsupported property type: " + schema.getClass()); 192 | } 193 | 194 | protected DataProvider collectEmailProperty(Swagger swagger, EmailProperty schema, boolean clean) { 195 | return gen(() -> faker.internet().emailAddress()); 196 | } 197 | 198 | protected DataProvider collectFileProperty(Swagger swagger, FileProperty schema, boolean clean) { 199 | return gen(() -> faker.file().fileName()); 200 | } 201 | 202 | protected DataProvider collectUUIDProperty(Swagger swagger, UUIDProperty schema, boolean clean) { 203 | return gen(() -> UUID.randomUUID()); 204 | } 205 | 206 | protected DataProvider collectBooleanProperty(Swagger swagger, BooleanProperty schema, boolean clean) { 207 | return gen(() -> faker.random().nextBoolean()); 208 | } 209 | 210 | protected DataProvider collectStringProperty(Swagger swagger, StringProperty schema, boolean clean) { 211 | return gen(() -> { 212 | StringProperty.Format uriFormat = StringProperty.Format.fromName(schema.getFormat()); 213 | if (uriFormat == StringProperty.Format.URI || uriFormat == StringProperty.Format.URL) 214 | return faker.internet().url(); 215 | else if (!isEmpty(schema.getPattern())) { 216 | Generex generex = new Generex(schema.getPattern()); 217 | return generex.random(); 218 | } else 219 | return faker.lorem().word(); 220 | }); 221 | } 222 | 223 | protected DataProvider collectPasswordProperty(Swagger swagger, PasswordProperty schema, boolean clean) { 224 | return new ConstDataProvider("*********"); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/main/java/com/github/tminglei/swagger/SwaggerFilter.java: -------------------------------------------------------------------------------- 1 | package com.github.tminglei.swagger; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.github.tminglei.bind.Simple; 6 | import com.github.tminglei.swagger.fake.ConstDataProvider; 7 | import com.github.tminglei.swagger.fake.DataProvider; 8 | import com.github.tminglei.swagger.fake.DataProviders; 9 | import com.github.tminglei.swagger.route.Route; 10 | import io.swagger.models.*; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import javax.servlet.*; 15 | import javax.servlet.http.HttpServletRequest; 16 | import javax.servlet.http.HttpServletResponse; 17 | import java.io.File; 18 | import java.io.IOException; 19 | import java.net.URISyntaxException; 20 | import java.net.URL; 21 | import java.net.URLDecoder; 22 | import java.util.*; 23 | import java.util.jar.JarEntry; 24 | import java.util.jar.JarFile; 25 | import java.util.stream.Collectors; 26 | 27 | import static com.github.tminglei.swagger.SimpleUtils.*; 28 | 29 | /** 30 | * Filter used to init/scan swagger registering info and serv swagger json 31 | */ 32 | public class SwaggerFilter implements Filter { 33 | private boolean enabled = true; 34 | private boolean fakeEnabled = true; 35 | private String swaggerUri = "/swagger.json"; 36 | 37 | private static final Logger logger = LoggerFactory.getLogger(SwaggerFilter.class); 38 | 39 | @Override 40 | public void init(FilterConfig filterConfig) throws ServletException { 41 | enabled = Optional.ofNullable(filterConfig.getInitParameter("enabled")) 42 | .map(Boolean::parseBoolean).orElse(enabled); 43 | fakeEnabled = Optional.ofNullable(filterConfig.getInitParameter("fake-enabled")) 44 | .map(Boolean::parseBoolean).orElse(fakeEnabled); 45 | 46 | if (enabled) { 47 | swaggerUri = Optional.ofNullable(filterConfig.getInitParameter("swagger-uri")) 48 | .orElse(swaggerUri); 49 | 50 | SwaggerContext swaggerContext = SwaggerContext.getInstance(); 51 | 52 | // step 1. setup custom components 53 | 54 | // set user custom mapping converter 55 | String mappingConverter = filterConfig.getInitParameter("mapping-converter"); 56 | logger.info("swagger config - mapping-converter: {}", mappingConverter); 57 | if (!isEmpty(mappingConverter)) { 58 | swaggerContext.setMappingConverter(newInstance(mappingConverter)); 59 | } 60 | 61 | // set user custom url router 62 | String router = filterConfig.getInitParameter("url-router"); 63 | logger.info("swagger config - url-router: {}", router); 64 | if (!isEmpty(router)) { 65 | swaggerContext.setRouter(newInstance(router)); 66 | } 67 | 68 | // set user custom data writer 69 | String dataWriter = filterConfig.getInitParameter("data-writer"); 70 | logger.info("swagger config - data-writer: {}", dataWriter); 71 | if (!isEmpty(dataWriter)) { 72 | swaggerContext.setDataWriter(newInstance(dataWriter)); 73 | } 74 | 75 | // step 2: scan and register swagger api info 76 | String scanPkgAndClasses = filterConfig.getInitParameter("scan-packages-and-classes"); 77 | if (isEmpty(scanPkgAndClasses)) throw new IllegalArgumentException("`scan-packages-and-classes` NOT configured!!!"); 78 | logger.info("swagger config - scan-packages-and-classes: {}", scanPkgAndClasses); 79 | String[] scanPkgClasses = scanPkgAndClasses.split(",|;"); 80 | for (String pkgClazz : scanPkgClasses) { 81 | if (!isEmpty(pkgClazz)) { 82 | scan(this.getClass(), pkgClazz.trim()).forEach(clz -> { 83 | logger.info("found class: {}", clz); 84 | try { 85 | Class.forName(clz); 86 | } catch (ClassNotFoundException e) { 87 | throw new RuntimeException(e); 88 | } 89 | }); 90 | } 91 | } 92 | 93 | // step 3: tidy swagger and prepare fake generators 94 | Swagger swagger = swaggerContext.getSwagger(); 95 | Map paths = swagger.getPaths(); 96 | if (paths == null) paths = Collections.emptyMap(); 97 | for (String path : paths.keySet()) { 98 | Map operations = paths.get(path).getOperationMap(); 99 | for (HttpMethod method : operations.keySet()) { 100 | Operation operation = operations.get(method); 101 | if (isEmpty(operation.getConsumes())) 102 | operation.setConsumes(swagger.getConsumes()); 103 | if (isEmpty(operation.getProduces())) 104 | operation.setProduces(swagger.getProduces()); 105 | 106 | if (fakeEnabled) { 107 | Map responses = operation.getResponses(); 108 | Response response = responses != null ? responses.get("200") : null; 109 | DataProvider dataProvider = response != null && response.getSchema() != null 110 | ? DataProviders.getInstance().collect(swagger, response.getSchema(), true) 111 | : new ConstDataProvider(null); 112 | dataProvider.setRequired(true); 113 | boolean implemented = swaggerContext.isImplemented(method, path, true); 114 | String origPath = swaggerContext.getOriginalPath(method, path, true); 115 | Route route = swaggerContext.getRouteFactory() 116 | .create(method, origPath, implemented, dataProvider); 117 | swaggerContext.getRouter() 118 | .add(route); 119 | } 120 | } 121 | } 122 | } 123 | } 124 | 125 | @Override 126 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { 127 | if (enabled) { 128 | HttpServletRequest req = (HttpServletRequest) request; 129 | HttpServletResponse resp = (HttpServletResponse) response; 130 | SwaggerContext swaggerContext = SwaggerContext.getInstance(); 131 | 132 | if (req.getPathInfo().equals(swaggerUri) && "GET".equalsIgnoreCase(req.getMethod())) { 133 | // enable cross-origin resource sharing 134 | resp.addHeader("Access-Control-Allow-Origin", "*"); 135 | resp.addHeader("Access-Control-Allow-Methods", "POST, GET, PUT, PATCH, DELETE, HEAD, OPTIONS"); 136 | resp.addHeader("Access-Control-Max-Age", "43200"); // half a day 137 | 138 | String json = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL) 139 | .writer().writeValueAsString(swaggerContext.getSwagger()); 140 | resp.getWriter().write(json); 141 | resp.flushBuffer(); 142 | return; 143 | } 144 | 145 | if (fakeEnabled) { 146 | HttpMethod method = HttpMethod.valueOf(req.getMethod().toUpperCase()); 147 | Route route = swaggerContext.getRouter().route(method, req.getPathInfo()); 148 | if (route != null && ! route.isImplemented()) { 149 | Map params = Simple.data(req.getParameterMap()); 150 | params.putAll(route.getPathParams(req.getPathInfo())); 151 | DataProvider dataProvider = route.getDataProvider(); 152 | dataProvider.setRequestParams(params); 153 | String format = req.getHeader("accept"); 154 | swaggerContext.getDataWriter().write(resp.getWriter(), format, dataProvider); 155 | resp.flushBuffer(); 156 | return; 157 | } 158 | } 159 | } 160 | 161 | filterChain.doFilter(request, response); 162 | } 163 | 164 | @Override 165 | public void destroy() { 166 | // nothing to do 167 | } 168 | 169 | ///--- 170 | // (recursively) find class names under specified base package 171 | // inspired by: http://www.uofr.net/~greg/java/get-resource-listing.html 172 | List scan(Class loaderClazz, String pkgOrClassName) { 173 | pkgOrClassName = pkgOrClassName.replace(".", "/").replaceAll("^/", "").replaceAll("/$", "") + "/"; 174 | 175 | try { 176 | Set result = new HashSet<>(); 177 | boolean found = false; 178 | 179 | //1. first, let's try to treat it as package 180 | Enumeration dirURLs = loaderClazz.getClassLoader().getResources(pkgOrClassName); 181 | while (dirURLs.hasMoreElements()) { 182 | found = true; 183 | URL dirURL = dirURLs.nextElement(); 184 | 185 | if (dirURL.getProtocol().equals("file")) { 186 | /* A file path: easy enough */ 187 | String[] names = new File(dirURL.toURI()).list(); 188 | for (String name : names) { 189 | if (name.endsWith(".class") && !name.contains("$")) { //filter out inner classes 190 | result.add(pkgOrClassName + name); 191 | } else { 192 | File f = new File(dirURL.getPath() + name); 193 | if (f.isDirectory()) { //recursively finding 194 | result.addAll(scan(loaderClazz, pkgOrClassName + name)); 195 | } 196 | } 197 | } 198 | } 199 | 200 | if (dirURL.getProtocol().equals("jar")) { 201 | /* A JAR path */ 202 | String jarPath = dirURL.getPath().substring(5, dirURL.getPath().indexOf("!")); //strip out only the JAR file 203 | JarFile jar = new JarFile(URLDecoder.decode(jarPath, "UTF-8")); 204 | Enumeration entries = jar.entries(); //gives ALL entries in jar 205 | while(entries.hasMoreElements()) { 206 | String name = entries.nextElement().getName(); 207 | if (name.startsWith(pkgOrClassName) && name.endsWith(".class") && !name.contains("$")) { 208 | result.add(name); 209 | } 210 | } 211 | } 212 | } 213 | 214 | //2. if not found, let's try to treat it as class 215 | if (!found) { 216 | pkgOrClassName = pkgOrClassName.replaceAll("/$", "").replaceAll("/class", ".class"); 217 | if (!pkgOrClassName.endsWith(".class")) pkgOrClassName = pkgOrClassName + ".class"; 218 | URL clsURL = loaderClazz.getClassLoader().getResource(pkgOrClassName); 219 | if (clsURL != null) result.add(pkgOrClassName); 220 | } 221 | 222 | return result.stream().map( 223 | n -> n.replaceAll("\\.class$", "").replace(File.separator, ".").replace("/", ".") 224 | ).collect(Collectors.toList()); 225 | 226 | } catch (URISyntaxException e) { 227 | throw new RuntimeException("INVALID package or class name: '" + pkgOrClassName + "'!!!"); 228 | } catch (IOException e) { 229 | throw new RuntimeException(e); 230 | } 231 | } 232 | 233 | } 234 | -------------------------------------------------------------------------------- /src/test/java/com/github/tminglei/swagger/route/RouterContractTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 BigTesting.org 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.tminglei.swagger.route; 17 | 18 | import io.swagger.models.HttpMethod; 19 | import org.junit.Before; 20 | import org.junit.Test; 21 | 22 | import static org.junit.Assert.*; 23 | 24 | /** 25 | * 26 | * @author Luis Antunes 27 | */ 28 | public abstract class RouterContractTest { 29 | 30 | protected R router; 31 | 32 | @Before 33 | public void beforeEachTest() { 34 | router = newRouter(); 35 | } 36 | 37 | protected abstract R newRouter(); 38 | 39 | protected abstract Route newRoute(String path); 40 | 41 | protected Route route(String path) { 42 | return router.route(HttpMethod.GET, path); 43 | } 44 | 45 | ///--- 46 | 47 | @Test 48 | // matches the root route 49 | public void routeTest1() { 50 | Route r1 = newRoute("/"); 51 | router.add(r1); 52 | assertEquals(r1, route("/")); 53 | } 54 | 55 | @Test 56 | // distinguishes between similar static and named param routes; case 1 57 | public void routeTest2() { 58 | Route r1 = newRoute("/clients/all"); 59 | Route r2 = newRoute("/clients/:id"); 60 | 61 | router.add(r1); 62 | router.add(r2); 63 | 64 | assertEquals(r1, route("/clients/all")); 65 | } 66 | 67 | @Test 68 | // distinguishes between similar static and named param routes; case 2 69 | public void routeTest3() { 70 | Route r1 = newRoute("/clients/all"); 71 | Route r2 = newRoute("/clients/:id"); 72 | 73 | router.add(r1); 74 | router.add(r2); 75 | 76 | assertEquals(r2, route("/clients/123")); 77 | } 78 | 79 | @Test 80 | // distinguishes between dissimilar static and named param routes; case 1 81 | public void routeTest4() { 82 | Route r1 = newRoute("/cntrl"); 83 | Route r2 = newRoute("/cntrl/clients/:id"); 84 | 85 | router.add(r1); 86 | router.add(r2); 87 | 88 | assertEquals(r1, route("/cntrl")); 89 | } 90 | 91 | @Test 92 | // distinguishes between dissimilar static and named param routes; case 2 93 | public void routeTest5() { 94 | Route r1 = newRoute("/cntrl"); 95 | Route r2 = newRoute("/cntrl/clients/:id"); 96 | 97 | router.add(r1); 98 | router.add(r2); 99 | 100 | assertEquals(r2, route("/cntrl/clients/23455")); 101 | } 102 | 103 | @Test 104 | // distinguishes between two different static routes 105 | public void routeTest6() { 106 | Route r1 = newRoute("/cntrl"); 107 | Route r2 = newRoute("/actn"); 108 | 109 | router.add(r1); 110 | router.add(r2); 111 | 112 | assertEquals(r2, route("/actn")); 113 | } 114 | 115 | @Test 116 | // returns null when no route is found 117 | public void routeTest7() { 118 | Route r1 = newRoute("/cntrl"); 119 | Route r2 = newRoute("/actn"); 120 | 121 | router.add(r1); 122 | router.add(r2); 123 | 124 | assertNull(route("/test")); 125 | } 126 | 127 | @Test 128 | // distinguishes between routes with multiple named path parameters; case 1 129 | public void routeTest8() { 130 | Route r1 = newRoute("/cntrl/actn/:id"); 131 | Route r2 = newRoute("/cntrl/actn/:id/:name"); 132 | 133 | router.add(r1); 134 | router.add(r2); 135 | 136 | assertEquals(r2, route("/cntrl/actn/123/bob")); 137 | } 138 | 139 | @Test 140 | // distinguishes between routes with multiple named path parameters; case 2 141 | public void routeTest9() { 142 | Route r1 = newRoute("/cntrl/actn/:id"); 143 | Route r2 = newRoute("/cntrl/actn/:id/:name"); 144 | 145 | router.add(r1); 146 | router.add(r2); 147 | 148 | assertEquals(r1, route("/cntrl/actn/123")); 149 | } 150 | 151 | @Test 152 | // distinguishes between named parameters with custom regex; alpha case 153 | public void routeTest10() { 154 | Route r1 = newRoute("/cntrl/actn/:id<[0-9]+>"); 155 | Route r2 = newRoute("/cntrl/actn/:id<[a-z]+>"); 156 | 157 | router.add(r1); 158 | router.add(r2); 159 | 160 | assertEquals(r2, route("/cntrl/actn/bob")); 161 | assertNull(route("/cntrl/actn/bob/")); 162 | } 163 | 164 | @Test 165 | // distinguishes between named parameters with custom regex; numeric case 166 | public void routeTest11() { 167 | Route r1 = newRoute("/cntrl/actn/:id<[0-9]+>"); 168 | Route r2 = newRoute("/cntrl/actn/:id<[a-z]+>"); 169 | 170 | router.add(r1); 171 | router.add(r2); 172 | 173 | assertEquals(r1, route("/cntrl/actn/123")); 174 | assertNull(route("/cntrl/actn/123/")); 175 | } 176 | 177 | @Test 178 | // matches a named parameter, but not if there is an extra path element after it 179 | public void routeTest12() { 180 | Route r1 = newRoute("/cntrl/:name"); 181 | 182 | router.add(r1); 183 | 184 | assertEquals(r1, route("/cntrl/Tim")); 185 | assertNull(route("/cntrl/Tim/blah")); 186 | } 187 | 188 | @Test 189 | // handles the splat path parameter for all requests 190 | public void routeTest13() { 191 | Route r1 = newRoute("/*"); 192 | Route r2 = newRoute("/specific"); 193 | Route r3 = newRoute("/:id<[0-9]+>"); 194 | 195 | router.add(r1); 196 | router.add(r2); 197 | router.add(r3); 198 | 199 | assertEquals(r1, route("/")); 200 | assertEquals(r1, route("/*")); 201 | assertEquals(r1, route("/cntrl")); 202 | assertEquals(r1, route("/actn")); 203 | assertEquals(r1, route("/cntrl/actn")); 204 | assertEquals(r1, route("/specific")); 205 | assertEquals(r1, route("/123")); 206 | assertEquals(r1, route("/hello/")); 207 | } 208 | 209 | @Test 210 | // handles splat parameter for all requests with root route present 211 | public void routeTest14() { 212 | Route r0 = newRoute("/"); 213 | Route r1 = newRoute("/*"); 214 | 215 | router.add(r0); 216 | router.add(r1); 217 | 218 | assertEquals(r0, route("/")); 219 | assertEquals(r1, route("/blah")); 220 | } 221 | 222 | @Test 223 | // handles splat parameters with a preceding resource 224 | public void routeTest15() { 225 | Route r1 = newRoute("/protected/*"); 226 | Route r2 = newRoute("/protected/:id<[0-9]+>"); 227 | Route r3 = newRoute("/:name<[a-z]+>"); 228 | 229 | router.add(r1); 230 | router.add(r2); 231 | router.add(r3); 232 | 233 | assertEquals(r1, route("/protected/content")); 234 | assertEquals(r1, route("/protected/123")); 235 | assertEquals(r3, route("/john")); 236 | assertEquals(r1, route("/protected/")); 237 | } 238 | 239 | @Test 240 | // handles splat parameters interjected between resources 241 | public void routeTest16() { 242 | Route r1 = newRoute("/protected/*/content"); 243 | Route r2 = newRoute("/protected/user/content"); 244 | 245 | router.add(r1); 246 | router.add(r2); 247 | 248 | assertNull(route("/hello")); 249 | assertEquals(r1, route("/protected/1/content")); 250 | assertEquals(r1, route("/protected/blah/content")); 251 | assertNull(route("/protected/blah/content/")); 252 | assertNull(route("/protected/1/blah/content")); 253 | assertEquals(r1, route("/protected/user/content")); 254 | } 255 | 256 | @Test 257 | // handles paths with splat parameters occurring multiple times 258 | public void routeTest17() { 259 | Route r1 = newRoute("/say/*/to/*"); 260 | 261 | router.add(r1); 262 | 263 | assertNull(route("/hello")); 264 | assertEquals(r1, route("/say/hello/to/world")); 265 | assertEquals(r1, route("/say/bye/to/Tim")); 266 | assertNull(route("/say/bye/bye/to/Tim")); 267 | assertEquals(r1, route("/say/bye/to/John/Doe")); 268 | assertNull(route("/say/hello/to")); 269 | assertEquals(r1, route("/say/hello/to/")); 270 | } 271 | 272 | @Test 273 | // handles splat path params that are part of paths with various path params 274 | public void routeTest18() { 275 | Route r1 = newRoute("/say/*/to/:name/:times<[0-9]+>/*"); 276 | 277 | router.add(r1); 278 | 279 | assertNull(route("/hello")); 280 | assertNull(route("/say/hello/to/John")); 281 | assertNull(route("/say/hello/to/John/1")); 282 | assertEquals(r1, route("/say/hello/to/John/1/")); 283 | assertEquals(r1, route("/say/hello/to/Tim/1/time")); 284 | assertEquals(r1, route("/say/hello/to/Tim/1/time/thanks")); 285 | } 286 | 287 | @Test 288 | // handles paths containing regex symbols 289 | public void routeTest19() { 290 | Route r = newRoute("/hello$.html"); 291 | router.add(r); 292 | assertEquals(r, route("/hello$.html")); 293 | } 294 | 295 | @Test 296 | // allows using unicode 297 | public void routeTest20() { 298 | Route r = newRoute("/föö"); 299 | router.add(r); 300 | assertEquals(r, route("/f%C3%B6%C3%B6")); 301 | } 302 | 303 | @Test 304 | // handles encoded '/' correctly 305 | public void routeTest21() { 306 | Route r = newRoute("/foo/bar"); 307 | router.add(r); 308 | assertNull(route("/foo%2Fbar")); 309 | } 310 | 311 | @Test 312 | // handles encoded '/' correctly with named params 313 | public void routeTest22() { 314 | Route r = newRoute("/:test"); 315 | router.add(r); 316 | assertEquals(r, route("/foo%2Fbar")); 317 | } 318 | 319 | @Test 320 | // handles encoded '/' correctly with splat 321 | public void routeTest22b() { 322 | Route r = newRoute("/*"); 323 | router.add(r); 324 | assertEquals(r, route("/foo%2Fbar")); 325 | } 326 | 327 | @Test 328 | // handles encoded '/' correctly with named params if it is interjected between resources 329 | public void routeTest22c() { 330 | Route r = newRoute("/hello/:test/there"); 331 | router.add(r); 332 | assertEquals(r, route("/hello/foo%2Fbar/there")); 333 | } 334 | 335 | @Test 336 | // handles encoded '/' correctly with splat if it is interjected between resources 337 | public void routeTest22d() { 338 | Route r = newRoute("/hello/*/there"); 339 | router.add(r); 340 | assertEquals(r, route("/hello/foo%2Fbar/there")); 341 | } 342 | 343 | @Test 344 | // literally matches '+' in path 345 | public void routeTest23() { 346 | Route r = newRoute("/foo+bar"); 347 | router.add(r); 348 | assertEquals(r, route("/foo%2Bbar")); 349 | } 350 | 351 | @Test 352 | // literally matches '$' in path 353 | public void routeTest24() { 354 | Route r = newRoute("/test$/"); 355 | router.add(r); 356 | assertEquals(r, route("/test$/")); 357 | } 358 | 359 | @Test 360 | // literally matches '.' in path 361 | public void routeTest25() { 362 | Route r = newRoute("/test.bar"); 363 | router.add(r); 364 | assertEquals(r, route("/test.bar")); 365 | } 366 | 367 | @Test 368 | // matches paths that include spaces encoded with '%20' 369 | public void routeTest26() { 370 | Route r = newRoute("/path with spaces"); 371 | router.add(r); 372 | assertEquals(r, route("/path%20with%20spaces")); 373 | } 374 | 375 | @Test 376 | // matches paths that include spaces encoded with '+' 377 | public void routeTest27() { 378 | Route r = newRoute("/path with spaces"); 379 | router.add(r); 380 | assertEquals(r, route("/path+with+spaces")); 381 | } 382 | 383 | @Test 384 | // matches paths that include ampersands 385 | public void routeTest28() { 386 | Route r = newRoute("/:name"); 387 | router.add(r); 388 | assertEquals(r, route("/foo&bar")); 389 | } 390 | 391 | @Test 392 | // matches a dot as part of a named param 393 | public void routeTest29() { 394 | Route r = newRoute("/:foo/:bar"); 395 | router.add(r); 396 | assertEquals(r, route("/user@example.com/name")); 397 | } 398 | 399 | @Test 400 | // literally matches parens in path 401 | public void routeTest30() { 402 | Route r = newRoute("/test(bar)/"); 403 | router.add(r); 404 | assertEquals(r, route("/test(bar)/")); 405 | } 406 | 407 | @Test 408 | // matches paths that end with '/' that occur within route paths 409 | public void routeTest31() { 410 | Route r1 = newRoute("/hello"); 411 | Route r2 = newRoute("/hello/"); 412 | Route r3 = newRoute("/hello/world"); 413 | 414 | router.add(r1); 415 | router.add(r2); 416 | router.add(r3); 417 | 418 | assertEquals(r1, route("/hello")); 419 | assertEquals(r2, route("/hello/")); 420 | assertEquals(r3, route("/hello/world")); 421 | } 422 | } 423 | --------------------------------------------------------------------------------