| // 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.logging.log4j.Logger; | |
| import org.apache.logging.log4j.LogManager; | |
| 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 { | |
| protected static Logger LOGGER = LogManager.getLogger(ApiXmlDocWriter.class); | |
| 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", | |
| "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); | |
| boolean isDeprecated = clas.getAnnotation(Deprecated.class) != null; | |
| apiCommand.setDeprecated(isDeprecated); | |
| 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 { | |
| 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) | |
| { | |
| 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) { | |
| LOGGER.error("Failed to create output stream to write an alert types ", e); | |
| } catch (IllegalAccessException e) { | |
| 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); | |
| } | |
| } | |
| } |