Finished OpenAPIWriter

This commit is contained in:
Ziver Koc 2021-03-02 20:34:51 +01:00
parent 02e91a38e9
commit bb8d6a93b4
10 changed files with 215 additions and 107 deletions

View file

@ -72,14 +72,14 @@ public class ClassUtil {
* @return if the given class is a wrapper for a primitive
*/
public static boolean isWrapper(Class<?> type){
return wrappers.contains( type );
return wrappers.contains(type);
}
/**
* @return if the given class is a primitive including String
*/
public static boolean isPrimitive(Class<?> type){
return primitives.contains( type );
return primitives.contains(type);
}
/**
@ -184,13 +184,26 @@ public class ClassUtil {
return null;
}
/**
* @param c a array class
* @return the base class the array is based on, if the input is not an array then the input is returned.
*/
public static Class<?> getArrayClass(Class<?> c) {
return getArrayClass(c, Integer.MAX_VALUE);
}
/**
* @param c a array class
* @param depth the number of times the method should recurse into array. Value of zero will return the input value.
* @return the base class the array is based on, if the input is not an array then the input is returned.
*/
public static Class<?> getArrayClass(Class<?> c, int depth) {
if (depth <= 0) // Stop recursion
return c;
if (c != null && c.isArray()) {
return getArrayClass(c.getComponentType());
return getArrayClass(c.getComponentType(), depth-1);
}
return c;
}

View file

@ -141,13 +141,9 @@ public class UPnPContentDirectory implements UPnPService, HttpPage, WSInterface
return ret;
}
public class BrowseRetObj extends WSReturnObject{
@WSParamName("Result")
public String Result;
@WSParamName("NumberReturned")
public int NumberReturned;
@WSParamName("TotalMatches")
public int TotalMatches;
@WSParamName("UpdateID")
public int UpdateID;
}

View file

@ -122,7 +122,7 @@ public interface WSInterface {
}
/**
* This method will be used in the header.
* This method will be used in the SOAP header.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@ -146,7 +146,7 @@ public interface WSInterface {
}
/**
* Sets a specific path for the method overriding the auto generated path.
* Sets a specific URL path for the method overriding the auto generated path.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)

View file

@ -55,6 +55,10 @@ public class WSMethodDef {
* A List of return parameters of the method
**/
private ArrayList<WSParameterDef> outputs;
/**
* The object type of output object
**/
private Class<?> outputClass;
/**
* A List of exceptions that this method throws
**/
@ -112,68 +116,58 @@ public class WSMethodDef {
else
namespace = wsDef.getNamespace() + "?#" + name;
// Hnadle Exceptions
Collections.addAll(exceptions, method.getExceptionTypes());
// Handle input parameter names
// ------------------------------------------------
// Handle inputs
// ------------------------------------------------
Annotation[][] paramAnnotation = method.getParameterAnnotations();
Class<?>[] inputTypes = method.getParameterTypes();
for (int i = 0; i < paramAnnotation.length; i++) {
WSParameterDef param = new WSParameterDef(this);
for (Annotation annotation : paramAnnotation[i]) {
if (annotation instanceof WSInterface.WSParamName) {
WSInterface.WSParamName paramName = (WSInterface.WSParamName) annotation;
param.setName(paramName.value());
param.setOptional(paramName.optional());
}
}
param.setParamClass(inputTypes[i]);
// if no name was found then use default
WSParameterDef param = new WSParameterDef(this, inputTypes[i], paramAnnotation[i]);
// if no name was found then generate one
if (param.getName() == null)
param.setName("args" + i);
inputs.add(param);
}
// Handle return parameter names
// ------------------------------------------------
// Handle outputs
// ------------------------------------------------
WSInterface.WSReturnName returnNameAnnotation = method.getAnnotation(WSInterface.WSReturnName.class);
if (WSReturnObject.class.isAssignableFrom(method.getReturnType())) {
Class<?> retClass = method.getReturnType();
Field[] fields = retClass.getFields();
this.outputClass = method.getReturnType();
if (WSReturnObject.class.isAssignableFrom(outputClass)) {
Field[] fields = outputClass.getFields();
for (Field field : fields) {
WSParameterDef ret_param = new WSParameterDef(this);
WSParameterDef ret_param = new WSParameterDef(this, field.getType(), field.getAnnotations());
WSInterface.WSParamName paramNameAnnotation = field.getAnnotation(WSInterface.WSParamName.class);
if (paramNameAnnotation != null)
ret_param.setName(paramNameAnnotation.value());
else
if (ret_param.getName() == null)
ret_param.setName(field.getName());
WSInterface.WSDocumentation documentationAnnotation = field.getAnnotation(WSInterface.WSDocumentation.class);
if (documentationAnnotation != null)
ret_param.setDocumentation(documentationAnnotation.value());
ret_param.setParamClass(field.getType());
outputs.add(ret_param);
}
} else if (method.getReturnType() != void.class) {
WSParameterDef ret_param = new WSParameterDef(this);
} else if (outputClass != void.class) {
WSInterface.WSReturnName returnNameAnnotation = method.getAnnotation(WSInterface.WSReturnName.class);
WSParameterDef ret_param = new WSParameterDef(this, method.getReturnType(), new Annotation[]{returnNameAnnotation});
if (returnNameAnnotation != null)
ret_param.setName(returnNameAnnotation.value());
else
if (ret_param.getName() == null)
ret_param.setName("return");
ret_param.setParamClass(method.getReturnType());
outputs.add(ret_param);
}
// ------------------------------------------------
// Handle Exceptions
// ------------------------------------------------
Collections.addAll(exceptions, method.getExceptionTypes());
// ------------------------------------------------
// Handle the request type
// ------------------------------------------------
WSRequestType requestTypeAnnotation = method.getAnnotation(WSRequestType.class);
if (requestTypeAnnotation != null) {
@ -193,7 +187,9 @@ public class WSMethodDef {
this.requestType = WSInterface.RequestType.GET;
}
// ------------------------------------------------
// Handle endpoint path
// ------------------------------------------------
WSPath pathAnnotation = method.getAnnotation(WSPath.class);
if (pathAnnotation != null)
@ -240,6 +236,13 @@ public class WSMethodDef {
return outputs;
}
/**
* @return the class of the output object
*/
public Class<?> getOutputClass() {
return outputClass;
}
/**
* @return documentation of the method if one exists or else null
*/

View file

@ -24,6 +24,8 @@
package zutil.net.ws;
import java.lang.annotation.Annotation;
/**
* This is a web service parameter definition class
*
@ -39,21 +41,35 @@ public class WSParameterDef {
/** Developer documentation **/
private String documentation;
/** If this parameter is optional **/
private boolean optional;
private boolean optional = false;
protected WSParameterDef(WSMethodDef mDef){
protected WSParameterDef(WSMethodDef mDef, Class<?> paramClass, Annotation[] annotations){
this.mDef = mDef;
this.optional = false;
this.paramClass = paramClass;
for (Annotation annotation : annotations) {
if (annotation == null)
continue;
if (annotation instanceof WSInterface.WSParamName) {
WSInterface.WSParamName paramNameAnnotation = (WSInterface.WSParamName) annotation;
this.name = paramNameAnnotation.value();
this.optional = paramNameAnnotation.optional();
} else if (annotation instanceof WSInterface.WSReturnName) {
WSInterface.WSReturnName returnAnnotation = (WSInterface.WSReturnName) annotation;
this.name = returnAnnotation.value();
} else if (annotation instanceof WSInterface.WSDocumentation) {
WSInterface.WSDocumentation documentationAnnotation = (WSInterface.WSDocumentation) annotation;
this.documentation = documentationAnnotation.value();
}
}
}
public Class<?> getParamClass() {
return paramClass;
}
protected void setParamClass(Class<?> paramClass) {
this.paramClass = paramClass;
}
public String getName() {
return name;

View file

@ -24,10 +24,6 @@
package zutil.net.ws;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
/**

View file

@ -1,20 +1,18 @@
package zutil.net.ws.openapi;
import zutil.ClassUtil;
import zutil.log.LogUtil;
import zutil.net.ws.WSMethodDef;
import zutil.net.ws.WSParameterDef;
import zutil.net.ws.WebServiceDef;
import zutil.net.ws.*;
import zutil.parser.DataNode;
import zutil.parser.json.JSONWriter;
import javax.xml.crypto.Data;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.lang.reflect.Field;
import java.util.*;
import java.util.logging.Logger;
/**
@ -22,6 +20,7 @@ import java.util.logging.Logger;
*
* @see <a href="https://swagger.io/specification/">OpenAPI Specification</a>
*/
@SuppressWarnings("rawtypes")
public class OpenAPIWriter {
private static final Logger logger = LogUtil.getLogger();
@ -60,14 +59,14 @@ public class OpenAPIWriter {
public String write() {
if (cache == null) {
Map<String, List<WSParameterDef>> schemas = new HashMap<>();
List<Class> objSchemas = new ArrayList<>();
DataNode root = new DataNode(DataNode.DataType.Map);
root.set("openapi", OPENAPI_VERSION);
root.set("info", generateInfo());
root.set("servers", generateServers());
root.set("paths", generatePaths(schemas));
root.set("components", generateComponents(schemas));
root.set("paths", generatePaths(objSchemas));
root.set("components", generateComponents(objSchemas));
this.cache = JSONWriter.toString(root);
}
@ -77,9 +76,11 @@ public class OpenAPIWriter {
private DataNode generateInfo() {
DataNode infoRoot = new DataNode(DataNode.DataType.Map);
infoRoot.set("title", ws.getName());
infoRoot.set("description", ws.getDocumentation());
infoRoot.set("version", "");
if (ws.getDocumentation() != null)
infoRoot.set("description", ws.getDocumentation());
// Not implemented properties
// "termsOfService": xxx,
// "contact": {"name": xxx,"url": xxx,"email": xxx},
@ -99,27 +100,33 @@ public class OpenAPIWriter {
return serversRoot;
}
private DataNode generatePaths(Map<String, List<WSParameterDef>> schemas) {
private DataNode generatePaths(List<Class> objSchemas) {
DataNode pathsRoot = new DataNode(DataNode.DataType.Map);
for (WSMethodDef methodDef : ws.getMethods()) {
DataNode pathNode = pathsRoot.set(methodDef.getPath(), DataNode.DataType.Map);
DataNode typeNode = pathNode.set(methodDef.getRequestType().toString().toLowerCase(), DataNode.DataType.Map);
typeNode.set("description", methodDef.getDocumentation());
if (methodDef.getDocumentation() != null)
typeNode.set("description", methodDef.getDocumentation());
// --------------------------------------------
// Inputs
// --------------------------------------------
DataNode parameterNode = typeNode.set("parameters", DataNode.DataType.Map);
DataNode parametersNode = typeNode.set("parameters", DataNode.DataType.List);
for (WSParameterDef parameterDef : methodDef.getInputs()) {
DataNode parameterNode = parametersNode.add(DataNode.DataType.Map);
parameterNode.set("name", parameterDef.getName());
parameterNode.set("description", parameterDef.getDocumentation());
parameterNode.set("in", "query");
parameterNode.set("required", parameterDef.isOptional());
parameterNode.set("required", !parameterDef.isOptional());
parameterNode.set("schema", "");
if (parameterDef.getDocumentation() != null)
parameterNode.set("description", parameterDef.getDocumentation());
DataNode schemaNode = parameterNode.set("schema", DataNode.DataType.Map);
generateSchema(schemaNode, parameterDef.getParamClass(), true, objSchemas);
}
// --------------------------------------------
@ -127,31 +134,104 @@ public class OpenAPIWriter {
// --------------------------------------------
DataNode responseNode = typeNode.set("responses", DataNode.DataType.Map);
DataNode schemaNode = responseNode.set("200", DataNode.DataType.Map)
.set("content", DataNode.DataType.Map)
.set("application/json", DataNode.DataType.Map)
.set("schema", DataNode.DataType.Map);
String retName = methodDef.getName() + "Return";
schemas.put("retName", methodDef.getOutputs());
schemaNode.set("$ref", "#/components/schemas/" + retName);
DataNode successNode = responseNode.set("200", DataNode.DataType.Map);
successNode.set("description", "A successful response.");
if (methodDef.getOutputClass() != void.class) {
DataNode schemaNode = successNode.set("content", DataNode.DataType.Map)
.set("application/json", DataNode.DataType.Map)
.set("schema", DataNode.DataType.Map);
generateSchema(schemaNode, methodDef.getOutputClass(), true, objSchemas);
}
}
return pathsRoot;
}
private DataNode generateComponents(Map<String, List<WSParameterDef>> schemas) {
private DataNode generateComponents(List<Class> objSchemas) {
DataNode componentsRoot = new DataNode(DataNode.DataType.Map);
DataNode schemasNode = new DataNode(DataNode.DataType.Map);
componentsRoot.set("schemas", schemasNode);
DataNode schemasNode = componentsRoot.set("schemas", DataNode.DataType.Map);
// Generate schemas
for (int i=0; i<objSchemas.size(); i++) {
Class clazz = objSchemas.get(i);
DataNode objectNode = schemasNode.set(clazz.getSimpleName(), DataNode.DataType.Map);
generateSchema(objectNode, clazz, false, objSchemas);
}
return componentsRoot;
}
private void generateSchema(DataNode parent, Class<?> clazz, boolean reference, List<Class> objSchemas) {
if (clazz == void.class)
return;
if (ClassUtil.isPrimitive(clazz) || ClassUtil.isWrapper(clazz)) {
parent.set("type", getOpenAPIType(clazz));
if (clazz == byte.class || clazz == Byte.class)
parent.set("format", "byte");
} else if (clazz.isArray() || Collection.class.isAssignableFrom(clazz)) {
parent.set("type", "array");
DataNode itemsNode = parent.set("items", DataNode.DataType.Map);
generateSchema(itemsNode, ClassUtil.getArrayClass(clazz, 1), reference, objSchemas);
} else {
parent.set("type", "object");
if (reference) {
if (!objSchemas.contains(clazz))
objSchemas.add(clazz);
parent.set("$ref", "#/components/schemas/" + clazz.getSimpleName());
} else {
if (WSReturnObject.class.isAssignableFrom(clazz)) {
DataNode propertiesNode = parent.set("properties", DataNode.DataType.Map);
for (Field field : clazz.getFields()) {
String fieldName = field.getName();
WSInterface.WSParamName paramNameAnnotation = field.getAnnotation(WSInterface.WSParamName.class);
if (paramNameAnnotation != null)
fieldName = paramNameAnnotation.value();
DataNode parameterNode = propertiesNode.set(fieldName, DataNode.DataType.Map);
generateSchema(parameterNode, field.getType(), false, objSchemas);
}
}
}
}
}
private String getOpenAPIType(Class<?> clazz) {
switch (clazz.getName()) {
case "int":
case "java.lang.Integer":
return "integer";
case "float":
case "java.lang.Float":
case "double":
case "java.lang.Double":
return "number";
case "byte":
case "java.lang.Byte":
case "java.lang.String":
return "string";
case "boolean":
case "java.lang.Boolean":
return "boolean";
}
return null;
}
/**
* Class containing Target API server information.
*/
protected static class ServerData {
String url;
String description;

View file

@ -373,6 +373,7 @@ public class SOAPHttpPage implements HttpPage{
*/
public static String getSOAPClassName(Class<?> c) {
Class<?> cTmp = ClassUtil.getArrayClass(c);
if (byte[].class.isAssignableFrom(c)) {
return "base64Binary";
} else if (WSReturnObject.class.isAssignableFrom(cTmp)) {