001package org.unix4j.codegen.command; 002 003import java.io.BufferedReader; 004import java.io.FileNotFoundException; 005import java.io.IOException; 006import java.io.InputStream; 007import java.io.InputStreamReader; 008import java.net.URL; 009import java.util.Arrays; 010import java.util.LinkedHashMap; 011import java.util.LinkedHashSet; 012import java.util.List; 013import java.util.Map; 014import java.util.Set; 015 016import javax.xml.parsers.DocumentBuilder; 017import javax.xml.parsers.DocumentBuilderFactory; 018 019import org.unix4j.codegen.command.def.CommandDef; 020import org.unix4j.codegen.command.def.MethodDef; 021import org.unix4j.codegen.command.def.OperandDef; 022import org.unix4j.codegen.command.def.OptionDef; 023import org.unix4j.codegen.xml.XmlUtil; 024import org.w3c.dom.Document; 025import org.w3c.dom.Element; 026 027/** 028 * Loads the XML command definition file and returns the contents as an fmpp 029 * data model. 030 */ 031public class CommandDefinitionLoader { 032 private static enum XmlElement { 033 command_def, command, 034 name, synopsis, description, 035 notes, note, 036 methods, method, 037 options, option, 038 operands, operand 039 } 040 private static enum XmlAttribtue { 041 class_, package_, ref, 042 name, args, usesStandardInput, acronym, type, _default, 043 exclusiveGroup, enabledBy, redirection 044 } 045 public CommandDef load(URL commandDefinition) { 046 try { 047 final DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); 048 final Document doc = builder.parse(commandDefinition.openStream()); 049 return load(commandDefinition, doc); 050 } catch (Exception e) { 051 throw new RuntimeException(e); 052 } 053 } 054 055 public CommandDef load(URL commandDefinitionURL, Document commandDefinition) throws IOException { 056 final Element elCommandDef = commandDefinition.getDocumentElement(); 057 final Element elCommand = XmlUtil.getSingleChildElement(elCommandDef, XmlElement.command); 058 final String commandName = XmlUtil.getRequiredAttribute(elCommand, XmlAttribtue.name); 059 final String className = XmlUtil.getRequiredAttribute(elCommand, XmlAttribtue.class_); 060 final String packageName = XmlUtil.getRequiredAttribute(elCommand, XmlAttribtue.package_); 061 final String name = XmlUtil.getRequiredElementText(elCommandDef, XmlElement.name); 062 final String synopsis = XmlUtil.getRequiredElementText(elCommandDef, XmlElement.synopsis); 063 final String description = loadDescription(commandDefinitionURL, elCommandDef); 064 final CommandDef def = new CommandDef(commandName, className, packageName, name, synopsis, description); 065 loadNotes(def, elCommandDef); 066 loadOptions(def, elCommandDef); 067 loadOperands(def, elCommandDef); 068 loadMethods(def, elCommandDef); 069 return def; 070 } 071 072 private String loadDescription(URL commandDefinitionURL, Element elCommand) throws IOException { 073 final String path = commandDefinitionURL.toString(); 074 final int lastDash = path == null ? -1 : path.lastIndexOf('/'); 075 final String parentPath = lastDash < 0 ? "" : path.substring(0, lastDash + 1); 076 final Element elDescription = XmlUtil.getSingleChildElement(elCommand, XmlElement.description); 077 final String ref = XmlUtil.getRequiredAttribute(elDescription, XmlAttribtue.ref); 078 final InputStream descFile = new URL(parentPath + ref).openStream(); 079 if (descFile != null) { 080 return readDescriptionFile(descFile); 081 } 082 throw new FileNotFoundException("cannot find description file '" + ref + "' for command " + elCommand.getNodeName()); 083 } 084 private void loadNotes(CommandDef def, Element elCommand) { 085 final Element elNotes = XmlUtil.getSingleChildElement(elCommand, XmlElement.notes); 086 final List<Element> list = XmlUtil.getChildElements(elNotes, XmlElement.note); 087 for (final Element elNote : list) { 088 final String desc = formatDesc(XmlUtil.getRequiredElementText(elNote)); 089 def.notes.add(desc); 090 } 091 } 092 private void loadOptions(CommandDef def, Element elCommand) { 093 final Element elOptions = XmlUtil.getSingleChildElement(elCommand, XmlElement.options); 094 final List<Element> list = XmlUtil.getChildElements(elOptions, XmlElement.option); 095 final Map<String, Set<OptionDef>> exclusiveGroupByName = new LinkedHashMap<String, Set<OptionDef>>(); 096 for (final Element elOption : list) { 097 final String name = XmlUtil.getRequiredAttribute(elOption, XmlAttribtue.name); 098 final String acronym = XmlUtil.getRequiredAttribute(elOption, XmlAttribtue.acronym); 099 final String desc = formatDesc(XmlUtil.getRequiredElementText(elOption)); 100 final OptionDef optDef = new OptionDef(name, acronym, desc); 101 def.options.put(name, optDef); 102 103 //the enabled-by constraint 104 final String enabledBy = XmlUtil.getAttribute(elOption, XmlAttribtue.enabledBy); 105 if (enabledBy != null) { 106 final String[] enablers = enabledBy.split(","); 107 optDef.enabledBy.addAll(Arrays.asList(enablers));//verify option names later when we have all options 108 } 109 110 //the exclusive group 111 final String exclusiveGroup = XmlUtil.getAttribute(elOption, XmlAttribtue.exclusiveGroup); 112 if (exclusiveGroup != null) { 113 Set<OptionDef> members = exclusiveGroupByName.get(exclusiveGroup); 114 if (members == null) { 115 members = new LinkedHashSet<OptionDef>(); 116 exclusiveGroupByName.put(exclusiveGroup, members); 117 } 118 members.add(optDef); 119 } 120 } 121 122 //check enabler options now 123 for (final OptionDef opt: def.options.values()) { 124 for (final String enabler : opt.enabledBy) { 125 if (!def.options.containsKey(enabler)) { 126 throw new IllegalArgumentException("enabler option '" + enabler + "' for option '" + opt.name + "' is not defined for the '" + def.commandName + "' command (hint: use name, not acronym)"); 127 } 128 } 129 } 130 131 //add exclusive groups to option defs 132 for (final Map.Entry<String, Set<OptionDef>> e : exclusiveGroupByName.entrySet()) { 133 for (final OptionDef opt : e.getValue()) { 134 for (final OptionDef excl : e.getValue()) { 135 if (opt != excl) { 136 opt.excludes.add(excl.name); 137 } 138 } 139 } 140 } 141 } 142 143 private void loadOperands(CommandDef def, Element elCommand) { 144 final Element elOperands = XmlUtil.getSingleChildElement(elCommand, XmlElement.operands); 145 final List<Element> list = XmlUtil.getChildElements(elOperands, XmlElement.operand); 146 final String[] defaultOperands = XmlUtil.getRequiredAttribute(elOperands, XmlAttribtue._default).split(","); 147 final boolean[] defaultOperandExists = new boolean[defaultOperands.length]; 148 for (final Element elOperand : list) { 149 final String name = XmlUtil.getRequiredAttribute(elOperand, XmlAttribtue.name); 150 final String type = XmlUtil.getRequiredAttribute(elOperand, XmlAttribtue.type); 151 final String desc = formatDesc(XmlUtil.getRequiredElementText(elOperand)); 152 final String redirection = XmlUtil.getAttribute(elOperand, XmlAttribtue.redirection, ""); 153 final OperandDef opDef = new OperandDef(name, type, desc, redirection); 154 def.operands.put(name, opDef); 155 for (int i = 0; i < defaultOperands.length; i++) { 156 defaultOperandExists[i] |= name.equals(defaultOperands[i]); 157 } 158 } 159 if (defaultOperands.length == 0) { 160 throw new IllegalArgumentException("default operand cannot be empty for command " + elCommand.getNodeName()); 161 } 162 for (int i = 0; i < defaultOperands.length; i++) { 163 if (!defaultOperandExists[i]) { 164 throw new IllegalArgumentException("default operand '" + defaultOperands[i] + "' is not defined for command " + elCommand.getNodeName()); 165 } 166 def.defaultOperands.add(defaultOperands[i]); 167 } 168 } 169 170 private void loadMethods(CommandDef def, Element elCommand) { 171 final Element elMethods = XmlUtil.getSingleChildElement(elCommand, XmlElement.methods); 172 final List<Element> list = XmlUtil.getChildElements(elMethods, XmlElement.method); 173 for (final Element elMethod : list) { 174 final String name = XmlUtil.getAttribute(elMethod, XmlAttribtue.name, def.commandName); 175 final String args = XmlUtil.getAttribute(elMethod, XmlAttribtue.args); 176 final String input = XmlUtil.getRequiredAttribute(elMethod, XmlAttribtue.usesStandardInput); 177 final String desc = formatDesc(XmlUtil.getRequiredElementText(elMethod)); 178 final MethodDef methodDef; 179 if (args == null) { 180 methodDef = new MethodDef(name, desc, Boolean.parseBoolean(input)); 181 } else { 182 final String[] splitArgs = args.split(","); 183 methodDef = new MethodDef(name, desc, Boolean.parseBoolean(input), splitArgs); 184 validateMethodArgs(def, methodDef); 185 } 186 def.methods.add(methodDef); 187 } 188 } 189 190 private void validateMethodArgs(CommandDef def, MethodDef methodDef) { 191 for (final String arg : methodDef.args) { 192 if (!def.operands.containsKey(arg)) { 193 throw new IllegalArgumentException("method argument '" + arg + "' is missing in the operands list of the '" + def.commandName + "' command"); 194 } 195 } 196 } 197 198 private static String formatDesc(String desc) { 199 return desc.replaceAll("\n(\\s*)\n", "\n$1<p>\n"); 200 } 201 202 private static final String BODY_TAG_START = "<body>"; 203 private static final String BODY_TAG_END = "</body>"; 204 private String readDescriptionFile(InputStream commandDescription) throws IOException { 205 final BufferedReader reader = new BufferedReader(new InputStreamReader(commandDescription)); 206 final StringBuilder description = new StringBuilder(); 207 String line = reader.readLine(); 208 while (line != null) { 209 description.append(line); 210 line = reader.readLine(); 211 } 212 reader.close(); 213 final int start = description.indexOf(BODY_TAG_START); 214 if (start < 0) { 215 throw new IllegalArgumentException("body start tag " + BODY_TAG_START + " not found in html command description file"); 216 } 217 final int end = description.indexOf(BODY_TAG_END, start); 218 if (end < 0) { 219 throw new IllegalArgumentException("body end tag " + BODY_TAG_END + " not found in html command description file"); 220 } 221 return formatDesc(description.substring(start + BODY_TAG_START.length(), end)); 222 } 223 224 225}