diff --git a/src/zutil/ClassUtil.java b/src/zutil/ClassUtil.java index c45524c..85cec90 100755 --- a/src/zutil/ClassUtil.java +++ b/src/zutil/ClassUtil.java @@ -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; } diff --git a/src/zutil/net/upnp/service/UPnPContentDirectory.java b/src/zutil/net/upnp/service/UPnPContentDirectory.java index da45890..09f17cf 100755 --- a/src/zutil/net/upnp/service/UPnPContentDirectory.java +++ b/src/zutil/net/upnp/service/UPnPContentDirectory.java @@ -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; } diff --git a/src/zutil/net/ws/WSInterface.java b/src/zutil/net/ws/WSInterface.java index beba110..c37b013 100755 --- a/src/zutil/net/ws/WSInterface.java +++ b/src/zutil/net/ws/WSInterface.java @@ -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) diff --git a/src/zutil/net/ws/WSMethodDef.java b/src/zutil/net/ws/WSMethodDef.java index e6e11e2..f246254 100755 --- a/src/zutil/net/ws/WSMethodDef.java +++ b/src/zutil/net/ws/WSMethodDef.java @@ -55,6 +55,10 @@ public class WSMethodDef { * A List of return parameters of the method **/ private ArrayList 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 */ diff --git a/src/zutil/net/ws/WSParameterDef.java b/src/zutil/net/ws/WSParameterDef.java index 93edee5..162fdde 100644 --- a/src/zutil/net/ws/WSParameterDef.java +++ b/src/zutil/net/ws/WSParameterDef.java @@ -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; diff --git a/src/zutil/net/ws/WSReturnObject.java b/src/zutil/net/ws/WSReturnObject.java index 69eda22..ed28c3d 100644 --- a/src/zutil/net/ws/WSReturnObject.java +++ b/src/zutil/net/ws/WSReturnObject.java @@ -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; /** diff --git a/src/zutil/net/ws/openapi/OpenAPIWriter.java b/src/zutil/net/ws/openapi/OpenAPIWriter.java index 84fe631..a7d33f6 100644 --- a/src/zutil/net/ws/openapi/OpenAPIWriter.java +++ b/src/zutil/net/ws/openapi/OpenAPIWriter.java @@ -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 OpenAPI Specification */ +@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> schemas = new HashMap<>(); + List 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> schemas) { + private DataNode generatePaths(List 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> schemas) { + private DataNode generateComponents(List 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 clazz, boolean reference, List 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; diff --git a/src/zutil/net/ws/soap/SOAPHttpPage.java b/src/zutil/net/ws/soap/SOAPHttpPage.java index 1cbc39d..c5b1e74 100755 --- a/src/zutil/net/ws/soap/SOAPHttpPage.java +++ b/src/zutil/net/ws/soap/SOAPHttpPage.java @@ -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)) { diff --git a/test/zutil/net/ws/openapi/OpenAPIWriterTest.java b/test/zutil/net/ws/openapi/OpenAPIWriterTest.java index 8efff8e..09fed54 100644 --- a/test/zutil/net/ws/openapi/OpenAPIWriterTest.java +++ b/test/zutil/net/ws/openapi/OpenAPIWriterTest.java @@ -37,6 +37,6 @@ public class OpenAPIWriterTest { OpenAPIWriter writer = new OpenAPIWriter(new WebServiceDef(SOAPTest.MainSOAPClass.class)); writer.addServer("example.com", "Main Server"); - assertEquals("", writer.write()); + assertEquals("{\"components\": {\"schemas\": {\"SpecialReturnClass\": {\"type\": \"object\", \"properties\": {\"b\": {\"type\": \"array\", \"items\": {\"format\": \"byte\", \"type\": \"string\"}}, \"otherValue1\": {\"type\": \"string\"}, \"otherName2\": {\"type\": \"string\"}, \"inner\": {\"type\": \"object\", \"properties\": {\"innerClassParam2\": {\"type\": \"string\"}, \"innerClassParam1\": {\"type\": \"string\"}}}}}, \"SimpleReturnClass\": {\"type\": \"object\", \"properties\": {\"otherParam1\": {\"type\": \"string\"}, \"param2\": {\"type\": \"string\"}}}}}, \"servers\": [{\"description\": \"Main Server\", \"url\": \"example.com\"}], \"openapi\": \"3.0.1\", \"paths\": {\"/simpleReturnClassMethod\": {\"get\": {\"responses\": {\"200\": {\"description\": \"A successful response.\", \"content\": {\"application/json\": {\"schema\": {\"type\": \"object\", \"$ref\": \"#/components/schemas/SimpleReturnClass\"}}}}}, \"parameters\": [{\"schema\": {\"type\": \"string\"}, \"in\": \"query\", \"name\": \"byte\", \"required\": true}]}}, \"/exceptionMethod\": {\"get\": {\"description\": \"Documentation of method exceptionMethod()\", \"responses\": {\"200\": {\"description\": \"A successful response.\"}}, \"parameters\": [{\"schema\": {\"type\": \"integer\"}, \"in\": \"query\", \"name\": \"otherParam1\", \"required\": false}, {\"schema\": {\"type\": \"integer\"}, \"in\": \"query\", \"name\": \"otherParam2\", \"required\": false}]}}, \"/specialReturnMethod\": {\"get\": {\"responses\": {\"200\": {\"description\": \"A successful response.\", \"content\": {\"application/json\": {\"schema\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"$ref\": \"#/components/schemas/SpecialReturnClass\"}}}}}}, \"parameters\": [{\"schema\": {\"type\": \"string\"}, \"in\": \"query\", \"name\": \"StringName2\", \"required\": true}]}}, \"/stringArrayMethod\": {\"get\": {\"responses\": {\"200\": {\"description\": \"A successful response.\", \"content\": {\"application/json\": {\"schema\": {\"type\": \"array\", \"items\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}}}}}}}, \"parameters\": [{\"schema\": {\"type\": \"string\"}, \"in\": \"query\", \"name\": \"StringName\", \"required\": true}]}}, \"/voidMethod\": {\"get\": {\"responses\": {\"200\": {\"description\": \"A successful response.\"}}, \"parameters\": []}}}, \"info\": {\"description\": \"\", \"title\": \"MainSOAPClass\", \"version\": \"\"}}", writer.write()); } } \ No newline at end of file diff --git a/test/zutil/net/ws/soap/SOAPTest.java b/test/zutil/net/ws/soap/SOAPTest.java index 421c35b..52c338c 100755 --- a/test/zutil/net/ws/soap/SOAPTest.java +++ b/test/zutil/net/ws/soap/SOAPTest.java @@ -84,29 +84,6 @@ public class SOAPTest { // TEST CLASSES // ---------------------------------------------------- - @SuppressWarnings("unused") - public static class SpecialReturnClass extends WSReturnObject{ - @WSParamName("otherValue1") - public String param1 = "otherValue1"; - @WSParamName("otherName2") - public String param2 = "otherValue2"; - public byte[] b = new byte[]{0x12, 0x23}; - public InnerClass inner = new InnerClass(); - } - - @SuppressWarnings("unused") - public static class InnerClass extends WSReturnObject{ - public String innerClassParam1 = "innerClass1"; - public String innerClassParam2 = "innerClass2"; - } - - @SuppressWarnings("unused") - public static class SimpleReturnClass extends WSReturnObject{ - @WSParamName("otherParam1") - public String param1 = "param1"; - public String param2 = "param2"; - } - @SuppressWarnings("unused") @WSNamespace("http://test.se:8080/") public static class MainSOAPClass implements WSInterface{ @@ -157,4 +134,30 @@ public class SOAPTest { private void privateMethod(){ } } + + + @SuppressWarnings("unused") + public static class SpecialReturnClass extends WSReturnObject{ + @WSParamName("otherValue1") + public String param1 = "otherValue1"; + @WSParamName("otherName2") + public String param2 = "otherValue2"; + public byte[] b = new byte[]{0x12, 0x23}; + public InnerClass inner = new InnerClass(); + } + + + @SuppressWarnings("unused") + public static class InnerClass extends WSReturnObject{ + public String innerClassParam1 = "innerClass1"; + public String innerClassParam2 = "innerClass2"; + } + + + @SuppressWarnings("unused") + public static class SimpleReturnClass extends WSReturnObject{ + @WSParamName("otherParam1") + public String param1 = "param1"; + public String param2 = "param2"; + } }