package org.jaxygen.apibrowser.pages; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import javax.naming.NamingException; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import org.jaxygen.annotations.NetAPI; import org.jaxygen.converters.json.JsonHRResponseConverter; import org.jaxygen.converters.properties.PropertiesToBeanConverter; import org.jaxygen.dto.Downloadable; import org.jaxygen.dto.Uploadable; import org.jaxygen.netservice.html.*; import org.jaxygen.url.UrlQuery; import org.jaxygen.util.ClassTypeUtil; import org.jaxygen.util.MethodNameComparator; /** * * @author artur */ public class MethodInvokerPage extends Page { public static final String NAME = "MethodInvokerPage"; public MethodInvokerPage(ServletContext context, HttpServletRequest request, String classRegistry, String beansPath) throws NamingException, IllegalArgumentException, SecurityException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, ClassNotFoundException, IOException, ServletException, NoSuchFieldException { super(context, request, classRegistry, beansPath); final String className = request.getParameter("className"); final String method = request.getParameter("methodName"); renderClassForm(request, className, method); } private void debug(final String message) { System.out.println(message); } private boolean isSimpleResultType(final Class<?> returnType) { return returnType.isPrimitive() || returnType.equals(Integer.class) || returnType.equals(Double.class) || returnType.equals(Float.class) || returnType.equals(String.class) || returnType.equals(double.class) || returnType.equals(float.class); } private static String removePathContextFromClassName(final String className, final String beansPath) { String simpleClassname = className.substring(beansPath.length()); if (simpleClassname.startsWith(".")) { simpleClassname = simpleClassname.substring(1); } return simpleClassname; } private void renderClassForm(HttpServletRequest request, final String classFilter, final String methodFilter) throws NamingException, IllegalArgumentException, SecurityException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, ClassNotFoundException, IOException, NoSuchFieldException { final String simpleClassname = removePathContextFromClassName(classFilter, beansPath); HTMLTable exceptionsTable = new HTMLTable(); exceptionsTable.getHeader().addColumn(new HTMLTable.HeadColumn(new HTMLLabel("Exception name"))); exceptionsTable.getHeader().addColumn(new HTMLTable.HeadColumn(new HTMLLabel("Description"))); exceptionsTable.setAttribute("border", "1"); HTMLDiv pointer = new HTMLDiv(); pointer.append(new HTMLLabel("className=" + classFilter), new HTMLLabel(" "), new HTMLLabel("methodName=" + methodFilter)); HTMLForm propertiesInputForm = new HTMLForm("submitForm"); //propertiesInputForm.setMethod(HTMLForm.Action.post); //propertiesInputForm.setAction(invokerPath + "/" + simpleClassname + "/" + methodFilter); propertiesInputForm.setEnctype("multipart/form-data"); propertiesInputForm.appendInput(HTMLInput.Type.hidden, "className", classFilter); propertiesInputForm.appendInput(HTMLInput.Type.hidden, "methodName", methodFilter); propertiesInputForm.appendInput(HTMLInput.Type.hidden, "inputType", "PROPERTIES"); propertiesInputForm.appendInput(HTMLInput.Type.hidden, "outputType", JsonHRResponseConverter.NAME); Class handerClass = Thread.currentThread().getContextClassLoader().loadClass(classFilter); Class resultType = null; for (Method method : handerClass.getMethods()) { if (method.getName().equals(methodFilter)) { resultType = method.getReturnType(); Type[] parameters = method.getParameterTypes(); HTMLTable table = new HTMLTable(); for (Type type : parameters) { HTMLParagraph p = new HTMLParagraph(); p.append(new HTMLLabel("inputClass=" + ((Class) type).getCanonicalName())); pointer.append(p); if (type instanceof Class<?>) { Class<?> paramClass = (Class<?>) type; addInputClassParameters(request, table, paramClass, ""); } } propertiesInputForm.append(table); Type[] exceptions = method.getExceptionTypes(); for (Type exceptionType : exceptions) { addExceptionHelp(exceptionType, exceptionsTable); } } } final Class resultClass = resultType; propertiesInputForm.appendInput(HTMLInput.Type.submit, "submit", null); HTMLDiv mainDiv = new HTMLDiv("mainDiv"); mainDiv.append(pointer); mainDiv.append(propertiesInputForm); mainDiv.append((HTMLElement) () -> { StringBuilder sb = new StringBuilder("<script type=\"text/javascript\">"); sb.append("window.addEventListener(\"load\", function () {\n") .append(" var form = document.getElementById(\"submitForm\");\n") .append("\n") .append(" form.addEventListener(\"submit\", function (event) {\n") .append(" event.preventDefault();\n") .append(" sendData();\n") .append(" });\n") .append("\n") .append(" handleAnchors(form);\n") .append(" registerCopyButtonEventListener(form);\n") .append(" registerUpdateShareLink(form);\n") .append("\n") .append("});\n"); sb.append("</script>"); return sb.toString(); }); mainDiv.append(new HTMLHeading(HTMLHeading.Level.H2, new HTMLLabel("Return type"))); mainDiv.append(renderOutputObject(resultType)); mainDiv.append(new HTMLHeading(HTMLHeading.Level.H2, new HTMLLabel("Exceptions thrown by the method"))); mainDiv.append(exceptionsTable); Page page = this; // append script responsible for saving files page.append((HTMLElement) () -> "<script type=\"application/ecmascript\" async src=\"js/FileSaver.js\"></script>"); page.append((HTMLElement) () -> "<script type=\"application/ecmascript\" async src=\"js/AnchorUpdater.js\"></script>"); page.append((HTMLElement) () -> "<script type=\"application/ecmascript\" async src=\"js/shareThisPage.js\"></script>"); // append script responsible for sending data to service page.append((HTMLElement) () -> { StringBuilder sb = new StringBuilder("<script type=\"text/javascript\">"); sb.append(" function sendData() {\n") .append(" document.getElementById(\"queryResult\").innerHTML='Wait...';\n") .append(" document.getElementById(\"responseDiv\").style.display='block'\n") .append(" document.getElementById(\"mainDiv\").style.display='none';\n") .append(" var form = document.getElementById(\"submitForm\");\n") .append(" var XHR = new XMLHttpRequest();\n") .append(" var FD = new FormData(form);\n") .append("\n") .append(" XHR.addEventListener(\"load\", function(event) {\n") .append(" if (event.target.getResponseHeader('tabid')) {\n") .append(" sessionStorage.setItem('tabid', event.target.getResponseHeader('tabid'));\n") .append(" }\n"); if (resultClass.isAssignableFrom(Downloadable.class)) { sb.append(" var blob=new Blob([event.target.response], {type:event.target.getResponseHeader('Content-Type')});\n") .append(" var regex = /.*filename=\"(.*)\".*/g;\n") .append(" saveAs(blob, regex.exec(event.target.getResponseHeader('Content-Disposition'))[1]);\n"); } else { sb.append(" document.getElementById(\"queryResult\").innerHTML=JSON.stringify(event.target.response, null, 2);\n"); } sb.append(" });\n"); if (resultClass.isAssignableFrom(Downloadable.class)) { sb.append(" XHR.addEventListener(\"progress\", updateProgress, false);\n"); } sb.append("\n") .append(" XHR.open(\"POST\", \"").append(invokerPath).append("/").append(simpleClassname).append("/").append(methodFilter).append("\");\n"); if (resultClass.isAssignableFrom(Downloadable.class)) { sb.append(" XHR.responseType='arraybuffer';\n"); } else { sb.append(" XHR.responseType='json';\n"); } sb.append("\n") .append(" if (sessionStorage.getItem('tabid')) {\n") .append(" XHR.setRequestHeader('tabid', sessionStorage.getItem('tabid'));\n") .append(" }\n") .append(" XHR.send(FD);\n") .append(" }\n"); sb.append(" function updateProgress(event) {\n") .append(" if (event.lengthComputable) {\n") .append(" var percentageComplete = event.loaded*100 / event.total;\n") .append(" document.getElementById(\"queryResult\").innerHTML='Downloading: ' + percentageComplete + '%';\n") .append(" } else {\n") .append(" document.getElementById(\"queryResult\").innerHTML='Downloading file...';\n") .append(" }\n") .append(" }"); sb.append("</script>"); return sb.toString(); }); String[] codes = getJSCode(methodFilter, handerClass, methodFilter); mainDiv.append(new HTMLHeading(HTMLHeading.Level.H2, new HTMLLabel("JS API code"))); mainDiv.append(new HTMLPre("js1", codes[0])); mainDiv.append(new HTMLPre("js2", codes[1])); mainDiv.append(new HTMLPre("js3", codes[2])); mainDiv.append(new HTMLHeading(HTMLHeading.Level.H2, new HTMLLabel("Share this page"))); mainDiv.append(new HTMLInput(HTMLInput.Type.text, "share it", "share_it", "share_it", "value")); mainDiv.append(new HTMLInput(HTMLInput.Type.button, "copyButton", "copyButton", "copyButton", "Copy")); page.append(mainDiv); HTMLDiv responseDiv = new HTMLDiv("responseDiv"); responseDiv.setAttribute("style", "display:none"); responseDiv.append((HTMLElement) () -> { StringBuilder sb = new StringBuilder("<script type=\"text/javascript\">"); sb.append("function goBack() {\n") .append(" document.getElementById(\"mainDiv\").style.display='block';\n") .append(" document.getElementById(\"responseDiv\").style.display='none'\n") .append("}"); sb.append("</script>"); return sb.toString(); }); HTMLInput backButton = new HTMLInput(); backButton.setType(HTMLInput.Type.button); backButton.setValue("Back"); backButton.setAttribute("onClick", "goBack()"); responseDiv.append(backButton); HTMLInput retryButton = new HTMLInput(); retryButton.setType(HTMLInput.Type.button); retryButton.setValue("Resend request"); retryButton.setAttribute("onClick", "sendData()"); responseDiv.append(retryButton); HTMLParagraph responseQueryResult = new HTMLParagraph("queryResult"); responseQueryResult.setAttribute("style", "white-space:pre-wrap;font-family:monospace"); responseDiv.append(responseQueryResult); page.append(responseDiv); } private String[] getJSCode(String methodName, Class handerClass, String methodFilter) { String[] result = new String[4]; result[3] = ""; String fields = ""; String fieldsInput = ""; for (Method method : handerClass.getMethods()) { if (method.getName().equals(methodFilter)) { Type[] parameters = method.getParameterTypes(); for (Type type : parameters) { if (type instanceof Class<?>) { Class<?> paramClass = (Class<?>) type; for (Method setter : paramClass.getMethods()) { if (setter.getName().startsWith("set") && !"set".equals(setter.getName())) { final String fieldName = setter.getName().substring(3); if (fieldName != null) { result[3] = "ok"; } String propertyName = fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1); fields += propertyName + ", "; fieldsInput += propertyName + ": " + propertyName + ", "; } } } } } } if (result[3].equals("ok")) { fieldsInput = fieldsInput.substring(0, fieldsInput.length() - 2); String function = "this." + methodName + " = function(" + fields + "onSuccess, onException){"; String call = " this.call(\"" + handerClass.getSimpleName() + "\", \"" + methodName + "\",{"; String input = "inputType: \"PROPERTIES\", " + fieldsInput + " } , onSuccess, onException)}"; result[0] = function; result[1] = call; result[2] = input; } else { String function = "this." + methodName + " = function(onSuccess, onException){"; String call = " this.call(\"" + handerClass.getSimpleName() + "\", \"" + methodName + "\",{} , onSuccess, onException);};"; result[0] = function; result[1] = call; result[2] = ""; } return result; } private HTMLElement renderOutputObject(Class<?> paramClass) { HTMLElement rc; if (paramClass != null) { HTMLTable table = new HTMLTable(); HTMLTable gettersTale = new HTMLTable(); table.addRow().addColumn(new HTMLHeading(HTMLHeading.Level.H3, new HTMLLabel(paramClass.getCanonicalName()))); table.addRow().addColumn(gettersTale); for (Method getter : paramClass.getMethods()) { if (getter.getName().startsWith("get") || getter.getName().startsWith("is")) { final Class<?> returnType = getter.getReturnType(); if (isSimpleResultType(returnType)) { gettersTale.addRow().addColumns(new HTMLLabel(getter.getName()), new HTMLLabel(returnType.getSimpleName())); } else if (ClassTypeUtil.isBoolType(returnType)) { gettersTale.addRow().addColumns(new HTMLLabel(getter.getName()), new HTMLLabel(returnType.getSimpleName()), enumBoolValues()); } else if (ClassTypeUtil.isEnumType(returnType)) { gettersTale.addRow().addColumns(new HTMLLabel(getter.getName()), new HTMLLabel(returnType.getSimpleName()), enumValues(returnType)); } else if (ClassTypeUtil.isArrayType(paramClass)) { gettersTale.addRow().addColumns(new HTMLLabel(getter.getName() + "[]"), new HTMLLabel(returnType.getSimpleName())); } else if (returnType.equals(List.class)) { gettersTale.addRow().addColumns(new HTMLLabel(getter.getName()), new HTMLLabel(returnType.getSimpleName())); } } } rc = table; } else { rc = new HTMLLabel("void"); } return rc; } private static HTMLElement enumValues(Class enumeration) { HTMLTable table = new HTMLTable(); HTMLTable.Row row = table.addRow(); row.addColumn(new HTMLLabel("ENUMS:")); for (Object o : enumeration.getEnumConstants()) { row.addColumn(new HTMLLabel(o.toString())); } return table; } private static HTMLElement enumBoolValues() { HTMLTable table = new HTMLTable(); HTMLTable.Row row = table.addRow(); row.addColumn(new HTMLLabel("Boolean:")); row.addColumn(new HTMLLabel("TRUE")); row.addColumn(new HTMLLabel("FALSE")); return table; } /** * Add list of parameters from bean class passed in the parameter paramClass * as a list of rows to the table. * * @param request * @param table * @param paramClass * @throws InstantiationException * @throws IllegalAccessException * @throws InvocationTargetException * @throws NoSuchMethodException */ private void addInputClassParameters(HttpServletRequest request, HTMLTable table, Class<?> paramClass, final String parentFieldName) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException { Object inputObject = paramClass.getConstructor().newInstance(); List<Method> methods = new ArrayList(Arrays.asList(paramClass.getMethods())); Collections.sort(methods, new MethodNameComparator()); for (Method setter : methods) { String setterName = setter.getName(); if (!"set".equals(setterName) && setter.getName().startsWith("set")) { final String fieldName = setter.getName().substring(3); Method getter = paramClass.getMethod("get" + fieldName); Object defaultValue = ""; Class<?> paramTypes[] = setter.getParameterTypes(); Class<?> paramType = null; String propertyName = fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1); if (paramTypes.length > 0) { paramType = paramTypes[0]; } if (getter != null) { defaultValue = getter.invoke(inputObject); } if ((HashMap.class.isAssignableFrom(paramType)) || paramType.isAssignableFrom(HashMap.class)) { final String counterName = parentFieldName + propertyName + "Size"; int multiplicity = 0; if (request.getParameter(counterName) != null) { multiplicity = Integer.parseInt(request.getParameter(counterName)); } Class<?>[] componentsTypes = ClassTypeUtil.retrieveMapTypes(paramClass, propertyName); if (multiplicity == 0) { renderMapFieldInputRow(request, table, parentFieldName + propertyName + "[]", counterName, null, componentsTypes, 0); } else { for (int i = 0; i < multiplicity; i++) { String newKeyFieldName = parentFieldName + propertyName + "[" + i + "]"; renderMapFieldInputRow(request, table, newKeyFieldName, counterName, null, componentsTypes, multiplicity); //TODO: add heredefault value object } } } else if (paramType.isAssignableFrom(ArrayList.class) || paramType.isAssignableFrom(LinkedList.class) || (List.class).isAssignableFrom(paramType)) { final String counterName = parentFieldName + propertyName + "Size"; int multiplicity = 0; if (request.getParameter(counterName) != null) { multiplicity = Integer.parseInt(request.getParameter(counterName)); } Class<?> componentType = ClassTypeUtil.retrieveListType(paramClass, propertyName); if (multiplicity == 0) { renderFieldInputRow(request, table, parentFieldName + propertyName + "[]", counterName, null, componentType, 0); } else { for (int i = 0; i < multiplicity; i++) { String newFieldName = parentFieldName + propertyName + "[" + i + "]"; renderFieldInputRow(request, table, newFieldName, counterName, null, componentType, multiplicity); //TODO: add heredefault value object } } } else if (paramType.isArray()) { final String counterName = parentFieldName + propertyName + "Size"; int multiplicity = 0; if (request.getParameter(counterName) != null) { multiplicity = Integer.parseInt(request.getParameter(counterName)); } Class<?> componentType = paramType.getComponentType(); if (paramType.equals(List.class)) { Type t = paramType.getTypeParameters()[0]; componentType = (Class<?>) t; } if (multiplicity == 0) { renderFieldInputRow(request, table, parentFieldName + propertyName + "[]", counterName, null, componentType, 0); } else { for (int i = 0; i < multiplicity; i++) { renderFieldInputRow(request, table, parentFieldName + propertyName + "[" + i + "]", counterName, null, componentType, multiplicity); } } } else { renderFieldInputRow(request, table, parentFieldName + propertyName, parentFieldName + propertyName, defaultValue, paramType, -1); } } } } /** * Generate row with the parameter input and fill it with the default value * * @param table * @param fieldName * @param defaultValue * @param paramType * @throws NoSuchMethodException * @throws InvocationTargetException * @throws IllegalAccessException * @throws InstantiationException */ private void renderFieldInputRow(HttpServletRequest request, HTMLTable table, final String fieldName, final String counterName, Object defaultValue, Class<?> paramType, int multiplicity) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException { HTMLTable.Row row = new HTMLTable.Row(); table.addRow(row); String propertyName = fieldName; row.addColumn(new HTMLLabel(paramType.getSimpleName(), paramType.getCanonicalName())); row.addColumn(new HTMLLabel(propertyName)); addPlusAndMinusAnchors(request, row, counterName, multiplicity, propertyName); if (multiplicity > 0 || multiplicity == -1) { addSpecificInput(request, row, defaultValue, paramType, propertyName); } } private void renderMapFieldInputRow(HttpServletRequest request, HTMLTable table, final String fieldName, final String counterName, Object defaultValue, Class<?>[] keyValueTypes, int multiplicity) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException { Class<?> keyType = keyValueTypes[0]; Class<?> valueType = keyValueTypes[1]; HTMLTable.Row row = new HTMLTable.Row(); table.addRow(row); String keyInputName = fieldName + "<key>"; String valueInputName = fieldName + "<value>"; String propertyName = fieldName; row.addColumn(new HTMLLabel("<" + keyType.getSimpleName() + ", " + keyType.getSimpleName() + ">", "<" + keyType.getCanonicalName() + ", " + keyType.getCanonicalName() + ">")); row.addColumn(new HTMLLabel(propertyName)); addPlusAndMinusAnchors(request, row, counterName, multiplicity, propertyName); if (multiplicity > 0 || multiplicity == -1) { addSpecificInput(request, row, defaultValue, keyType, keyInputName); addSpecificInput(request, row, defaultValue, valueType, valueInputName); } } private void addSpecificInput(HttpServletRequest request, HTMLTable.Row row, Object defaultValue, final Class<?> paramType, final String propertyName) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException { if (ClassTypeUtil.isBoolType(paramType)) { HTMLSelect select = new HTMLSelect(propertyName); select.addOption(new HTMLOption("TRUE", new HTMLLabel("TRUE"))); select.addOption(new HTMLOption("FALSE", new HTMLLabel("FALSE"))); row.addColumn(select); } else if (paramType.isEnum()) { HTMLSelect select = new HTMLSelect(propertyName); String parameterName = propertyName + "_Value"; Object value = request.getParameter(parameterName); for (Object obj : paramType.getEnumConstants()) { String name = obj.toString(); HTMLOption htmlOptions = new HTMLOption(name, new HTMLLabel(name)); boolean isSelected = value != null && name.equals(value.toString()); htmlOptions.setSelected(isSelected); select.addOption(htmlOptions); } row.addColumn(select); } else if (PropertiesToBeanConverter.isCovertable(paramType)) { String parameterName = propertyName + "_Value"; Object value = request.getParameter(parameterName); Object v = value != null ? value : defaultValue; row.addColumn(new HTMLInput(propertyName, propertyName, "INPUT_FIELD", v)); } else if (paramType.isAssignableFrom(Uploadable.class)) { row.addColumn(new HTMLInput(HTMLInput.Type.file, propertyName)); } else { HTMLTable beanTable = new HTMLTable(); row.addColumn(beanTable); addInputClassParameters(request, beanTable, paramType, propertyName + "."); } } private void addExceptionHelp(Type exceptionType, HTMLTable table) { HTMLTable.Row row = table.addRow(); table.addRow(row); row.addColumn(new HTMLLabel(exceptionType.toString())); NetAPI annotation = (NetAPI) ((Class) exceptionType).getAnnotation(NetAPI.class); if (annotation != null) { row.addColumn(new HTMLLabel(annotation.description())); } } private void addPlusAndMinusAnchors(HttpServletRequest request, HTMLTable.Row row, final String counterName, int multiplicity, final String propertyName) { /* * Build the query which adds more inputs to the rendered array */ UrlQuery queryMultiplicityUp = new UrlQuery(); request.getParameterMap().keySet().stream().forEach((key) -> { if (key.toString().equals(counterName)) { queryMultiplicityUp.add(counterName, "" + (multiplicity + 1)); } else { queryMultiplicityUp.add(key.toString(), request.getParameter(key.toString())); } }); /* * Build the query which removes one input from the rendered array */ UrlQuery queryMultiplicityDown = new UrlQuery(); request.getParameterMap().keySet().stream().forEach((key) -> { if (key.toString().equals(counterName)) { queryMultiplicityDown.add(counterName, "" + (multiplicity - 1)); } else { queryMultiplicityDown.add(key.toString(), request.getParameter(key.toString())); } }); if (queryMultiplicityUp.getParameters().containsKey(counterName) == false) { queryMultiplicityUp.add(counterName, "" + (multiplicity + 1)); } if (multiplicity >= 0) { row.addColumn(new HTMAnchor("P" + propertyName, "anchor", "" + browserPath + "?" + queryMultiplicityUp.toString(), new HTMLLabel("+"))); } else { row.addColumn(new HTMLLabel("")); } if (multiplicity >= 1) { row.addColumn(new HTMAnchor("M" + propertyName, "anchor", "" + browserPath + "?" + queryMultiplicityDown.toString(), new HTMLLabel("-"))); } else { row.addColumn(new HTMLLabel("")); } } }