Skip to content

Commit 198a077

Browse files
committed
Add support for documenting service APIs.
1 parent d2d5ab8 commit 198a077

File tree

7 files changed

+194
-26
lines changed

7 files changed

+194
-26
lines changed

httprpc-test/src/main/java/org/httprpc/test/CatalogService.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import java.util.ArrayList;
1818
import java.util.HashMap;
1919
import java.util.List;
20+
import java.util.Map;
2021

2122
import javax.servlet.ServletException;
2223
import javax.servlet.annotation.WebServlet;
@@ -32,7 +33,7 @@
3233
public class CatalogService extends WebService {
3334
private static final long serialVersionUID = 0;
3435

35-
private List<?> items = null;
36+
private List<? extends Map<String, ?>> items = null;
3637

3738
@Override
3839
public void init() throws ServletException {
@@ -66,13 +67,13 @@ public void init() throws ServletException {
6667

6768
@RequestMethod("GET")
6869
@ResourcePath("items")
69-
public List<?> getItems() {
70+
public List<? extends Map<String, ?>> getItems() {
7071
return items;
7172
}
7273

7374
@RequestMethod("GET")
7475
@ResourcePath("items/?:itemID")
75-
public Object getItem() {
76+
public Map<String, ?> getItem() {
7677
int itemID = Integer.parseInt(getKey("itemID"));
7778

7879
return (itemID > 0 && itemID <= items.size()) ? items.get(itemID - 1) : null;

httprpc-test/src/main/java/org/httprpc/test/mongodb/RestaurantService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.httprpc.CSVEncoder;
2727
import org.httprpc.JSONEncoder;
2828
import org.httprpc.RequestMethod;
29+
import org.httprpc.Response;
2930
import org.jtemplate.TemplateEncoder;
3031

3132
import com.mongodb.MongoClient;
@@ -59,6 +60,7 @@ public void destroy() {
5960
}
6061

6162
@RequestMethod("GET")
63+
@Response("[{string: any}]")
6264
public void getRestaurants(String zipCode, String format) throws IOException {
6365
MongoDatabase db = mongoClient.getDatabase("test");
6466

httprpc-test/src/main/java/org/httprpc/test/mysql/PetService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.httprpc.JSONEncoder;
3535
import org.httprpc.RequestMethod;
3636
import org.httprpc.ResourcePath;
37+
import org.httprpc.Response;
3738
import org.httprpc.sql.Parameters;
3839
import org.httprpc.sql.ResultSetAdapter;
3940
import org.jtemplate.TemplateEncoder;
@@ -70,6 +71,7 @@ public void init() throws ServletException {
7071
}
7172

7273
@RequestMethod("GET")
74+
@Response("[{name: string, owner: string, species: string, sex: string, birth: date}]")
7375
public void getPets(String owner, String format) throws SQLException, IOException {
7476
try (Connection connection = DriverManager.getConnection(DB_URL)) {
7577
Parameters parameters = Parameters.parse("SELECT name, species, sex, birth FROM pet WHERE owner = :owner");

httprpc-test/src/main/webapp/index.jsp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,26 @@
88
<body>
99

1010
<h2>Test</h2>
11+
<a href="${pageContext.request.contextPath}/math?description">Math (description)</a><br/>
12+
<br/>
13+
1114
<a href="${pageContext.request.contextPath}/math/sum?a=2&b=4">Sum</a><br/>
1215
<a href="${pageContext.request.contextPath}/math/sum?values=1&values=2&values=3">Sum Values</a><br/>
1316
<a href="${pageContext.request.contextPath}/echo?value=héllo">Echo</a><br/>
17+
18+
<hr/>
19+
20+
<a href="${pageContext.request.contextPath}/catalog?description">Catalog (description)</a><br/>
21+
<br/>
22+
1423
<a href="${pageContext.request.contextPath}/catalog/items">Items</a><br/>
1524
<a href="${pageContext.request.contextPath}/catalog/items/1">Item 1</a><br/>
1625

1726
<hr/>
1827

28+
<a href="${pageContext.request.contextPath}/upload?description">Upload (description)</a><br/>
29+
<br/>
30+
1931
<form action="${pageContext.request.contextPath}/upload" method="post" enctype="multipart/form-data">
2032
<table>
2133
<tr>
@@ -42,10 +54,16 @@
4254

4355
<hr>
4456

57+
<a href="${pageContext.request.contextPath}/tree?description">Tree (description)</a><br/>
58+
<br/>
59+
4560
<a href="${pageContext.request.contextPath}/tree">Tree</a><br/>
4661

4762
<hr>
4863

64+
<a href="${pageContext.request.contextPath}/test?description">Test (description)</a><br/>
65+
<br/>
66+
4967
<a href="${pageContext.request.contextPath}/test?string=héllo&strings=a&strings=b&strings=c&number=123&flag=true&date=0&localDate=2018-06-28&localTime=10:45&localDateTime=2018-06-28T10:45">GET</a><br/>
5068
<a href="${pageContext.request.contextPath}/test/a/123/b/héllo/c/456/d/göodbye">GET (Key List)</a><br/>
5169
<a href="${pageContext.request.contextPath}/test/fibonacci">GET (Fibonacci)</a><br/>
@@ -130,12 +148,20 @@
130148
</form>
131149

132150
<h2>MongoDB</h2>
151+
152+
<a href="${pageContext.request.contextPath}/restaurants?description">Restaurants (description)</a><br/>
153+
<br/>
154+
133155
<a href="${pageContext.request.contextPath}/restaurants?zipCode=10462">Restaurants</a><br/>
134156
<a href="${pageContext.request.contextPath}/restaurants?zipCode=10462&format=csv">Restaurants (CSV)</a><br/>
135157
<a href="${pageContext.request.contextPath}/restaurants?zipCode=10462&format=html">Restaurants (HTML)</a><br/>
136158
<a href="${pageContext.request.contextPath}/restaurants?zipCode=10462&format=xml">Restaurants (XML)</a><br/>
137159

138160
<h2>MySQL</h2>
161+
162+
<a href="${pageContext.request.contextPath}/pets?description">Pets (description)</a><br/>
163+
<br/>
164+
139165
<a href="${pageContext.request.contextPath}/pets?owner=Gwen">Pets</a><br/>
140166
<a href="${pageContext.request.contextPath}/pets?owner=Gwen&format=csv">Pets (CSV)</a><br/>
141167
<a href="${pageContext.request.contextPath}/pets?owner=Gwen&format=html">Pets (HTML)</a><br/>

httprpc/src/main/java/org/httprpc/WebService.java

Lines changed: 149 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,16 @@
3232
import java.util.LinkedList;
3333
import java.util.List;
3434
import java.util.Map;
35+
import java.util.TreeMap;
3536

3637
import javax.servlet.ServletException;
3738
import javax.servlet.http.HttpServlet;
3839
import javax.servlet.http.HttpServletRequest;
3940
import javax.servlet.http.HttpServletResponse;
4041
import javax.servlet.http.Part;
42+
import javax.xml.stream.XMLOutputFactory;
43+
import javax.xml.stream.XMLStreamException;
44+
import javax.xml.stream.XMLStreamWriter;
4145

4246
import org.httprpc.beans.BeanAdapter;
4347

@@ -48,15 +52,18 @@ public abstract class WebService extends HttpServlet {
4852
private static final long serialVersionUID = 0;
4953

5054
private static class Resource {
51-
public final HashMap<String, LinkedList<Method>> handlerMap = new HashMap<>();
52-
public final HashMap<String, Resource> resources = new HashMap<>();
55+
private static List<String> order = Arrays.asList("get", "post", "put", "patch", "delete");
5356

54-
public String name = null;
57+
public final TreeMap<String, LinkedList<Method>> handlerMap = new TreeMap<>((verb1, verb2) -> {
58+
int i1 = order.indexOf(verb1);
59+
int i2 = order.indexOf(verb2);
5560

56-
@Override
57-
public String toString() {
58-
return handlerMap.keySet().toString() + "; " + resources.toString();
59-
}
61+
return Integer.compare((i1 == -1) ? order.size() : i1, (i2 == -1) ? order.size() : i2);
62+
});
63+
64+
public final TreeMap<String, Resource> resources = new TreeMap<>();
65+
66+
public String key = null;
6067
}
6168

6269
private Resource root = null;
@@ -110,7 +117,7 @@ public void init() throws ServletException {
110117
throw new ServletException("Invalid path component.");
111118
}
112119

113-
child.name = component.substring(k);
120+
child.key = component.substring(k);
114121

115122
component = PATH_VARIABLE;
116123
}
@@ -141,13 +148,23 @@ public void init() throws ServletException {
141148
@Override
142149
@SuppressWarnings("unchecked")
143150
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
151+
String verb = request.getMethod().toLowerCase();
152+
String pathInfo = request.getPathInfo();
153+
154+
if (verb.equals("get") && pathInfo == null) {
155+
String queryString = request.getQueryString();
156+
157+
if (queryString != null && queryString.equals("description")) {
158+
describeService(request, response);
159+
return;
160+
}
161+
}
162+
144163
Resource resource = root;
145164

146165
ArrayList<String> keyList = new ArrayList<>();
147166
HashMap<String, String> keyMap = new HashMap<>();
148167

149-
String pathInfo = request.getPathInfo();
150-
151168
if (pathInfo != null) {
152169
String[] components = pathInfo.split("/");
153170

@@ -170,16 +187,16 @@ protected void service(HttpServletRequest request, HttpServletResponse response)
170187

171188
keyList.add(component);
172189

173-
if (child.name != null) {
174-
keyMap.put(child.name, component);
190+
if (child.key != null) {
191+
keyMap.put(child.key, component);
175192
}
176193
}
177194

178195
resource = child;
179196
}
180197
}
181198

182-
List<Method> handlerList = resource.handlerMap.get(request.getMethod().toLowerCase());
199+
List<Method> handlerList = resource.handlerMap.get(verb);
183200

184201
if (handlerList == null) {
185202
super.service(request, response);
@@ -454,5 +471,124 @@ protected String getKey(int index) {
454471
protected String getKey(String name) {
455472
return keyMap.get().get(name);
456473
}
474+
475+
private void describeService(HttpServletRequest request, HttpServletResponse response) throws IOException {
476+
response.setContentType(String.format("text/html;charset=%s", UTF_8));
477+
478+
XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newInstance();
479+
480+
try {
481+
XMLStreamWriter xmlStreamWriter = xmlOutputFactory.createXMLStreamWriter(response.getWriter());
482+
483+
xmlStreamWriter.writeStartElement("html");
484+
xmlStreamWriter.writeStartElement("body");
485+
486+
TreeMap<Class<?>, String> structures = new TreeMap<>((type1, type2) -> {
487+
return type1.getSimpleName().compareTo(type2.getSimpleName());
488+
});
489+
490+
describeResource(request.getServletPath(), root, structures, xmlStreamWriter);
491+
492+
for (Map.Entry<Class<?>, String> entry : structures.entrySet()) {
493+
Class<?> type = entry.getKey();
494+
495+
if (type == URL.class) {
496+
continue;
497+
}
498+
499+
String name = type.getSimpleName();
500+
501+
xmlStreamWriter.writeStartElement("h3");
502+
xmlStreamWriter.writeAttribute("id", name);
503+
xmlStreamWriter.writeCharacters(name);
504+
xmlStreamWriter.writeEndElement();
505+
506+
xmlStreamWriter.writeStartElement("pre");
507+
xmlStreamWriter.writeCharacters(entry.getValue());
508+
xmlStreamWriter.writeEndElement();
509+
}
510+
511+
xmlStreamWriter.writeEndElement();
512+
xmlStreamWriter.writeEndElement();
513+
514+
xmlStreamWriter.close();
515+
} catch (XMLStreamException exception) {
516+
throw new IOException(exception);
517+
}
518+
}
519+
520+
private void describeResource(String path, Resource resource, TreeMap<Class<?>, String> structures, XMLStreamWriter xmlStreamWriter) throws XMLStreamException {
521+
if (!resource.handlerMap.isEmpty()) {
522+
xmlStreamWriter.writeStartElement("h2");
523+
xmlStreamWriter.writeCharacters(path);
524+
xmlStreamWriter.writeEndElement();
525+
526+
for (Map.Entry<String, LinkedList<Method>> entry : resource.handlerMap.entrySet()) {
527+
for (Method method : entry.getValue()) {
528+
xmlStreamWriter.writeStartElement("pre");
529+
530+
xmlStreamWriter.writeCharacters(entry.getKey().toUpperCase() + " (");
531+
532+
Parameter[] parameters = method.getParameters();
533+
534+
for (int i = 0; i < parameters.length; i++) {
535+
Parameter parameter = parameters[i];
536+
537+
if (i > 0) {
538+
xmlStreamWriter.writeCharacters(", ");
539+
}
540+
541+
xmlStreamWriter.writeCharacters(getName(parameter) + ": ");
542+
543+
Type type = parameter.getParameterizedType();
544+
545+
if (type == URL.class) {
546+
xmlStreamWriter.writeCharacters("file");
547+
} else if (type instanceof ParameterizedType
548+
&& ((ParameterizedType)type).getRawType() == List.class
549+
&& ((ParameterizedType)type).getActualTypeArguments()[0] == URL.class) {
550+
xmlStreamWriter.writeCharacters("[file]");
551+
} else {
552+
xmlStreamWriter.writeCharacters(BeanAdapter.describe(type, structures));
553+
}
554+
}
555+
556+
xmlStreamWriter.writeCharacters(") -> ");
557+
558+
Type type = method.getGenericReturnType();
559+
Response response = method.getAnnotation(Response.class);
560+
561+
if ((type == Void.class || type == Void.TYPE) && response != null) {
562+
xmlStreamWriter.writeCharacters(response.value());
563+
} else {
564+
String description = BeanAdapter.describe(type, structures);
565+
566+
if (structures.containsKey(type)) {
567+
xmlStreamWriter.writeStartElement("a");
568+
xmlStreamWriter.writeAttribute("href", "#" + description);
569+
xmlStreamWriter.writeCharacters(description);
570+
xmlStreamWriter.writeEndElement();
571+
} else {
572+
xmlStreamWriter.writeCharacters(description);
573+
}
574+
}
575+
576+
xmlStreamWriter.writeEndElement();
577+
}
578+
}
579+
}
580+
581+
for (Map.Entry<String, Resource> entry : resource.resources.entrySet()) {
582+
String component = entry.getKey();
583+
584+
Resource child = entry.getValue();
585+
586+
if (child.key != null) {
587+
component += ":" + child.key;
588+
}
589+
590+
describeResource(path + "/" + component, child, structures, xmlStreamWriter);
591+
}
592+
}
457593
}
458594

httprpc/src/main/java/org/httprpc/beans/BeanAdapter.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -767,10 +767,9 @@ public V setValue(V value) {
767767
* <li>Any type that implements {@link CharSequence}: "string"</li>
768768
* <li>Any {@link Enum} type: "enum"</li>
769769
* <li>Any type that extends {@link Date}: "date"</li>
770-
* <li>{@link LocalDate}: "local-date"</li>
771-
* <li>{@link LocalTime}: "local-time"</li>
772-
* <li>{@link LocalDateTime}: "local-datetime"</li>
773-
* <li>{@link LocalDateTime}: "local-datetime"</li>
770+
* <li>{@link LocalDate}: "date-local"</li>
771+
* <li>{@link LocalTime}: "time-local"</li>
772+
* <li>{@link LocalDateTime}: "datetime-local"</li>
774773
* <li>{@link List}: "[<i>element description</i>]"</li>
775774
* <li>{@link Map}: "[<i>key description</i>: <i>value description</i>]"</li>
776775
* </ul>
@@ -848,13 +847,15 @@ private static String describe(Class<?> type, Map<Class<?>, String> structures)
848847
} else if (Date.class.isAssignableFrom(type)) {
849848
return "date";
850849
} else if (type == LocalDate.class) {
851-
return "local-date";
850+
return "date-local";
852851
} else if (type == LocalTime.class) {
853-
return "local-time";
852+
return "time-local";
854853
} else if (type == LocalDateTime.class) {
855-
return "local-datetime";
854+
return "datetime-local";
856855
} else {
857856
if (!structures.containsKey(type)) {
857+
structures.put(type, null);
858+
858859
Method[] methods = type.getMethods();
859860

860861
TreeMap<String, String> properties = new TreeMap<>();

httprpc/src/test/java/org/httprpc/beans/BeanAdapterTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,9 @@ public void testDescribe() {
151151
" double: double,\n" +
152152
" i: integer,\n" +
153153
" list: [any],\n" +
154-
" localDate: local-date,\n" +
155-
" localDateTime: local-datetime,\n" +
156-
" localTime: local-time,\n" +
154+
" localDate: date-local,\n" +
155+
" localDateTime: datetime-local,\n" +
156+
" localTime: time-local,\n" +
157157
" long: long,\n" +
158158
" map: [string: any],\n" +
159159
" nestedBean: NestedBean,\n" +

0 commit comments

Comments
 (0)