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}