// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package com.cloud.api.doc; import com.cloud.alert.AlertManager; import com.cloud.serializer.Param; import com.cloud.utils.IteratorUtil; import com.cloud.utils.ReflectUtil; import com.google.gson.annotations.SerializedName; import com.thoughtworks.xstream.XStream; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.BaseAsyncCmd; import org.apache.cloudstack.api.BaseAsyncCreateCmd; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.BaseResponse; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.response.AsyncJobResponse; import org.apache.cloudstack.api.response.HostResponse; import org.apache.cloudstack.api.response.IPAddressResponse; import org.apache.cloudstack.api.response.SecurityGroupResponse; import org.apache.cloudstack.api.response.SnapshotResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.cloudstack.api.response.TemplateResponse; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.api.response.VolumeResponse; import org.apache.log4j.Logger; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.TreeMap; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; public class ApiXmlDocWriter { public static final Logger s_logger = Logger.getLogger(ApiXmlDocWriter.class.getName()); private static String s_dirName = ""; private static Map<String, Class<?>> s_apiNameCmdClassMap = new HashMap<String, Class<?>>(); private static LinkedHashMap<Object, String> s_allApiCommands = new LinkedHashMap<Object, String>(); private static TreeMap<Object, String> s_allApiCommandsSorted = new TreeMap<Object, String>(); private static final List<String> AsyncResponses = setAsyncResponses(); private static List<String> setAsyncResponses() { List<String> asyncResponses = new ArrayList<String>(); asyncResponses.add(TemplateResponse.class.getName()); asyncResponses.add(VolumeResponse.class.getName()); //asyncResponses.add(LoadBalancerResponse.class.getName()); asyncResponses.add(HostResponse.class.getName()); asyncResponses.add(IPAddressResponse.class.getName()); asyncResponses.add(StoragePoolResponse.class.getName()); asyncResponses.add(UserVmResponse.class.getName()); asyncResponses.add(SecurityGroupResponse.class.getName()); //asyncResponses.add(ExternalLoadBalancerResponse.class.getName()); asyncResponses.add(SnapshotResponse.class.getName()); return asyncResponses; } public static void main(String[] args) { Set<Class<?>> cmdClasses = ReflectUtil.getClassesWithAnnotation(APICommand.class, new String[] {"org.apache.cloudstack.api", "com.cloud.api", "com.cloud.api.commands", "com.globo.globodns.cloudstack.api", "org.apache.cloudstack.network.opendaylight.api", "com.cloud.api.commands.netapp", "org.apache.cloudstack.api.command.admin.zone", "org.apache.cloudstack.network.contrail.api.command"}); for (Class<?> cmdClass : cmdClasses) { if(cmdClass.getAnnotation(APICommand.class)==null){ System.out.println("Warning, API Cmd class " + cmdClass.getName() + " has no APICommand annotation "); continue; } String apiName = cmdClass.getAnnotation(APICommand.class).name(); if (s_apiNameCmdClassMap.containsKey(apiName)) { // handle API cmd separation into admin cmd and user cmd with the common api name Class<?> curCmd = s_apiNameCmdClassMap.get(apiName); if (curCmd.isAssignableFrom(cmdClass)) { // api_cmd map always keep the admin cmd class to get full response and parameters s_apiNameCmdClassMap.put(apiName, cmdClass); } else if (cmdClass.isAssignableFrom(curCmd)) { // just skip this one without warning continue; } else { System.out.println("Warning, API Cmd class " + cmdClass.getName() + " has non-unique apiname " + apiName); continue; } } else { s_apiNameCmdClassMap.put(apiName, cmdClass); } } System.out.printf("Scanned and found %d APIs\n", s_apiNameCmdClassMap.size()); List<String> argsList = Arrays.asList(args); Iterator<String> iter = argsList.iterator(); while (iter.hasNext()) { String arg = iter.next(); if (arg.equals("-d")) { s_dirName = iter.next(); } } for (Map.Entry<String, Class<?>> entry: s_apiNameCmdClassMap.entrySet()) { Class<?> cls = entry.getValue(); s_allApiCommands.put(entry.getKey(), cls.getName()); } s_allApiCommandsSorted.putAll(s_allApiCommands); try { // Create object writer XStream xs = new XStream(); xs.alias("command", Command.class); xs.alias("arg", Argument.class); String xmlDocDir = s_dirName + "/xmldoc"; String rootAdminDirName = xmlDocDir + "/apis"; (new File(rootAdminDirName)).mkdirs(); ObjectOutputStream out = xs.createObjectOutputStream(new FileWriter(s_dirName + "/commands.xml"), "commands"); ObjectOutputStream rootAdmin = xs.createObjectOutputStream(new FileWriter(rootAdminDirName + "/" + "apiSummary.xml"), "commands"); ObjectOutputStream rootAdminSorted = xs.createObjectOutputStream(new FileWriter(rootAdminDirName + "/" + "apiSummarySorted.xml"), "commands"); Iterator<?> it = s_allApiCommands.keySet().iterator(); while (it.hasNext()) { String key = (String)it.next(); // Write admin commands writeCommand(out, key); writeCommand(rootAdmin, key); // Write single commands to separate xml files ObjectOutputStream singleRootAdminCommandOs = xs.createObjectOutputStream(new FileWriter(rootAdminDirName + "/" + key + ".xml"), "command"); writeCommand(singleRootAdminCommandOs, key); singleRootAdminCommandOs.close(); } // Write sorted commands it = s_allApiCommandsSorted.keySet().iterator(); while (it.hasNext()) { String key = (String)it.next(); writeCommand(rootAdminSorted, key); } out.close(); rootAdmin.close(); rootAdminSorted.close(); // write alerttypes to xml writeAlertTypes(xmlDocDir); } catch (Exception ex) { ex.printStackTrace(); System.exit(2); } } private static void writeCommand(ObjectOutputStream out, String command) throws ClassNotFoundException, IOException { Class<?> clas = Class.forName(s_allApiCommands.get(command)); ArrayList<Argument> request = new ArrayList<Argument>(); ArrayList<Argument> response = new ArrayList<Argument>(); // Create a new command, set name/description/usage Command apiCommand = new Command(); apiCommand.setName(command); APICommand impl = clas.getAnnotation(APICommand.class); if (impl == null) { impl = clas.getSuperclass().getAnnotation(APICommand.class); } if (impl == null) { throw new IllegalStateException(String.format("An %1$s annotation is required for class %2$s.", APICommand.class.getCanonicalName(), clas.getCanonicalName())); } if (impl.includeInApiDoc()) { String commandDescription = impl.description(); if (commandDescription != null && !commandDescription.isEmpty()) { apiCommand.setDescription(commandDescription); } else { System.out.println("Command " + apiCommand.getName() + " misses description"); } String commandUsage = impl.usage(); if (commandUsage != null && !commandUsage.isEmpty()) { apiCommand.setUsage(commandUsage); } //Set version when the API is added if (!impl.since().isEmpty()) { apiCommand.setSinceVersion(impl.since()); } boolean isAsync = ReflectUtil.isCmdClassAsync(clas, new Class<?>[] {BaseAsyncCmd.class, BaseAsyncCreateCmd.class}); apiCommand.setAsync(isAsync); Set<Field> fields = ReflectUtil.getAllFieldsForClass(clas, new Class<?>[] {BaseCmd.class, BaseAsyncCmd.class, BaseAsyncCreateCmd.class}); request = setRequestFields(fields); // Get response parameters Class<?> responseClas = impl.responseObject(); Field[] responseFields = responseClas.getDeclaredFields(); response = setResponseFields(responseFields, responseClas); apiCommand.setRequest(request); apiCommand.setResponse(response); out.writeObject(apiCommand); } else { s_logger.debug("Command " + command + " is not exposed in api doc"); } } private static ArrayList<Argument> setRequestFields(Set<Field> fields) { ArrayList<Argument> arguments = new ArrayList<Argument>(); Set<Argument> requiredArguments = new HashSet<Argument>(); Set<Argument> optionalArguments = new HashSet<Argument>(); Argument id = null; for (Field f : fields) { Parameter parameterAnnotation = f.getAnnotation(Parameter.class); if (parameterAnnotation != null && parameterAnnotation.expose() && parameterAnnotation.includeInApiDoc()) { Argument reqArg = new Argument(parameterAnnotation.name()); reqArg.setRequired(parameterAnnotation.required()); if (!parameterAnnotation.description().isEmpty()) { reqArg.setDescription(parameterAnnotation.description()); } if (parameterAnnotation.type() == BaseCmd.CommandType.LIST || parameterAnnotation.type() == BaseCmd.CommandType.MAP) { reqArg.setType(parameterAnnotation.type().toString().toLowerCase()); } reqArg.setDataType(parameterAnnotation.type().toString().toLowerCase()); if (!parameterAnnotation.since().isEmpty()) { reqArg.setSinceVersion(parameterAnnotation.since()); } if (reqArg.isRequired()) { if (parameterAnnotation.name().equals("id")) { id = reqArg; } else { requiredArguments.add(reqArg); } } else { optionalArguments.add(reqArg); } } } // sort required and optional arguments here if (id != null) { arguments.add(id); } arguments.addAll(IteratorUtil.asSortedList(requiredArguments)); arguments.addAll(IteratorUtil.asSortedList(optionalArguments)); return arguments; } private static ArrayList<Argument> setResponseFields(Field[] responseFields, Class<?> responseClas) { ArrayList<Argument> arguments = new ArrayList<Argument>(); ArrayList<Argument> sortedChildlessArguments = new ArrayList<Argument>(); ArrayList<Argument> sortedArguments = new ArrayList<Argument>(); Argument id = null; for (Field responseField : responseFields) { SerializedName nameAnnotation = responseField.getAnnotation(SerializedName.class); if (nameAnnotation != null) { Param paramAnnotation = responseField.getAnnotation(Param.class); Argument respArg = new Argument(nameAnnotation.value()); boolean hasChildren = false; if (paramAnnotation != null && paramAnnotation.includeInApiDoc()) { String description = paramAnnotation.description(); Class fieldClass = paramAnnotation.responseObject(); if (description != null && !description.isEmpty()) { respArg.setDescription(description); } respArg.setDataType(responseField.getType().getSimpleName().toLowerCase()); if (!paramAnnotation.since().isEmpty()) { respArg.setSinceVersion(paramAnnotation.since()); } if (fieldClass != null) { Class<?> superClass = fieldClass.getSuperclass(); if (superClass != null) { String superName = superClass.getName(); if (superName.equals(BaseResponse.class.getName())) { ArrayList<Argument> fieldArguments = new ArrayList<Argument>(); Field[] fields = fieldClass.getDeclaredFields(); fieldArguments = setResponseFields(fields, fieldClass); respArg.setArguments(fieldArguments); hasChildren = true; } } } } if (paramAnnotation != null && paramAnnotation.includeInApiDoc()) { if (nameAnnotation.value().equals("id")) { id = respArg; } else { if (hasChildren) { respArg.setName(nameAnnotation.value() + "(*)"); sortedArguments.add(respArg); } else { sortedChildlessArguments.add(respArg); } } } } } Collections.sort(sortedArguments); Collections.sort(sortedChildlessArguments); if (id != null) { arguments.add(id); } arguments.addAll(sortedChildlessArguments); arguments.addAll(sortedArguments); if (responseClas.getName().equalsIgnoreCase(AsyncJobResponse.class.getName())) { Argument jobIdArg = new Argument("jobid", "the ID of the async job"); arguments.add(jobIdArg); } else if (AsyncResponses.contains(responseClas.getName())) { Argument jobIdArg = new Argument("jobid", "the ID of the latest async job acting on this object"); Argument jobStatusArg = new Argument("jobstatus", "the current status of the latest async job acting on this object"); arguments.add(jobIdArg); arguments.add(jobStatusArg); } return arguments; } private static void zipDir(String zipFileName, String dir) throws Exception { File dirObj = new File(dir); ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipFileName)); addDir(dirObj, out); out.close(); } static void addDir(File dirObj, ZipOutputStream out) throws IOException { File[] files = dirObj.listFiles(); byte[] tmpBuf = new byte[1024]; String pathToDir = s_dirName; for (int i = 0; i < files.length; i++) { if (files[i].isDirectory()) { addDir(files[i], out); continue; } try(FileInputStream in = new FileInputStream(files[i].getPath());) { out.putNextEntry(new ZipEntry(files[i].getPath().substring(pathToDir.length()))); int len; while ((len = in.read(tmpBuf)) > 0) { out.write(tmpBuf, 0, len); } out.closeEntry(); }catch(IOException ex) { s_logger.error("addDir:Exception:"+ ex.getMessage(),ex); } } } private static void deleteDir(File dir) { if (dir.isDirectory()) { String[] children = dir.list(); if (children != null) { for (int i = 0; i < children.length; i++) { deleteDir(new File(dir, children[i])); } } } dir.delete(); } private static void writeAlertTypes(String dirName) { XStream xs = new XStream(); xs.alias("alert", Alert.class); try(ObjectOutputStream out = xs.createObjectOutputStream(new FileWriter(dirName + "/alert_types.xml"), "alerts");) { for (Field f : AlertManager.class.getFields()) { if (f.getClass().isAssignableFrom(Number.class)) { String name = f.getName().substring(11); Alert alert = new Alert(name, f.getInt(null)); out.writeObject(alert); } } } catch (IOException e) { s_logger.error("Failed to create output stream to write an alert types ", e); } catch (IllegalAccessException e) { s_logger.error("Failed to read alert fields ", e); } } private static class LinkedProperties extends Properties { private final LinkedList<Object> keys = new LinkedList<Object>(); @Override public Enumeration<Object> keys() { return Collections.<Object> enumeration(keys); } @Override public Object put(Object key, Object value) { // System.out.println("Adding key" + key); keys.add(key); return super.put(key, value); } } }