001    /**
002     * Copyright (c) 2010 Yahoo! Inc. All rights reserved.
003     * Licensed under the Apache License, Version 2.0 (the "License");
004     * you may not use this file except in compliance with the License.
005     * You may obtain a copy of the License at
006     *
007     *   http://www.apache.org/licenses/LICENSE-2.0
008     *
009     *  Unless required by applicable law or agreed to in writing, software
010     *  distributed under the License is distributed on an "AS IS" BASIS,
011     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012     *  See the License for the specific language governing permissions and
013     *  limitations under the License. See accompanying LICENSE file.
014     */
015    package org.apache.oozie.workflow.lite;
016    
017    import org.apache.oozie.workflow.WorkflowException;
018    import org.apache.oozie.util.IOUtils;
019    import org.apache.oozie.util.XmlUtils;
020    import org.apache.oozie.util.ParamChecker;
021    import org.apache.oozie.ErrorCode;
022    import org.apache.oozie.service.Services;
023    import org.apache.oozie.service.ActionService;
024    import org.jdom.Element;
025    import org.jdom.JDOMException;
026    import org.jdom.Namespace;
027    import org.xml.sax.SAXException;
028    
029    import javax.xml.transform.stream.StreamSource;
030    import javax.xml.validation.Schema;
031    import javax.xml.validation.Validator;
032    import java.io.IOException;
033    import java.io.Reader;
034    import java.io.StringReader;
035    import java.io.StringWriter;
036    import java.util.ArrayList;
037    import java.util.HashMap;
038    import java.util.List;
039    import java.util.Map;
040    
041    /**
042     * Class to parse and validate workflow xml
043     */
044    public class LiteWorkflowAppParser {
045    
046        private static final String DECISION_E = "decision";
047        private static final String ACTION_E = "action";
048        private static final String END_E = "end";
049        private static final String START_E = "start";
050        private static final String JOIN_E = "join";
051        private static final String FORK_E = "fork";
052        private static final Object KILL_E = "kill";
053    
054        private static final String SLA_INFO = "info";
055    
056        private static final String NAME_A = "name";
057        private static final String TO_A = "to";
058    
059        private static final String FORK_PATH_E = "path";
060        private static final String FORK_START_A = "start";
061    
062        private static final String ACTION_OK_E = "ok";
063        private static final String ACTION_ERROR_E = "error";
064    
065        private static final String DECISION_SWITCH_E = "switch";
066        private static final String DECISION_CASE_E = "case";
067        private static final String DECISION_DEFAULT_E = "default";
068    
069        private static final String KILL_MESSAGE_E = "message";
070    
071        private Schema schema;
072        private Class<? extends DecisionNodeHandler> decisionHandlerClass;
073        private Class<? extends ActionNodeHandler> actionHandlerClass;
074    
075        private static enum VisitStatus {
076            VISITING, VISITED
077        }
078    
079        ;
080    
081    
082        public LiteWorkflowAppParser(Schema schema, Class<? extends DecisionNodeHandler> decisionHandlerClass,
083                                     Class<? extends ActionNodeHandler> actionHandlerClass) throws WorkflowException {
084            this.schema = schema;
085            this.decisionHandlerClass = decisionHandlerClass;
086            this.actionHandlerClass = actionHandlerClass;
087        }
088    
089        /**
090         * Parse and validate xml to {@link LiteWorkflowApp}
091         *
092         * @param reader
093         * @return LiteWorkflowApp
094         * @throws WorkflowException
095         */
096        public LiteWorkflowApp validateAndParse(Reader reader) throws WorkflowException {
097            try {
098                StringWriter writer = new StringWriter();
099                IOUtils.copyCharStream(reader, writer);
100                String strDef = writer.toString();
101    
102                if (schema != null) {
103                    Validator validator = schema.newValidator();
104                    validator.validate(new StreamSource(new StringReader(strDef)));
105                }
106    
107                Element wfDefElement = XmlUtils.parseXml(strDef);
108                LiteWorkflowApp app = parse(strDef, wfDefElement);
109                Map<String, VisitStatus> traversed = new HashMap<String, VisitStatus>();
110                traversed.put(app.getNode(StartNodeDef.START).getName(), VisitStatus.VISITING);
111                validate(app, app.getNode(StartNodeDef.START), traversed);
112                return app;
113            }
114            catch (JDOMException ex) {
115                throw new WorkflowException(ErrorCode.E0700, ex.getMessage(), ex);
116            }
117            catch (SAXException ex) {
118                throw new WorkflowException(ErrorCode.E0701, ex.getMessage(), ex);
119            }
120            catch (IOException ex) {
121                throw new WorkflowException(ErrorCode.E0702, ex.getMessage(), ex);
122            }
123        }
124    
125        /**
126         * Parse xml to {@link LiteWorkflowApp}
127         *
128         * @param strDef
129         * @param root
130         * @return LiteWorkflowApp
131         * @throws WorkflowException
132         */
133        @SuppressWarnings({"unchecked", "ConstantConditions"})
134        private LiteWorkflowApp parse(String strDef, Element root) throws WorkflowException {
135            Namespace ns = root.getNamespace();
136            LiteWorkflowApp def = null;
137            for (Element eNode : (List<Element>) root.getChildren()) {
138                if (eNode.getName().equals(START_E)) {
139                    def = new LiteWorkflowApp(root.getAttributeValue(NAME_A), strDef,
140                                              new StartNodeDef(eNode.getAttributeValue(TO_A)));
141                }
142                else {
143                    if (eNode.getName().equals(END_E)) {
144                        def.addNode(new EndNodeDef(eNode.getAttributeValue(NAME_A)));
145                    }
146                    else {
147                        if (eNode.getName().equals(KILL_E)) {
148                            def.addNode(new KillNodeDef(eNode.getAttributeValue(NAME_A), eNode.getChildText(KILL_MESSAGE_E, ns)));
149                        }
150                        else {
151                            if (eNode.getName().equals(FORK_E)) {
152                                List<String> paths = new ArrayList<String>();
153                                for (Element tran : (List<Element>) eNode.getChildren(FORK_PATH_E, ns)) {
154                                    paths.add(tran.getAttributeValue(FORK_START_A));
155                                }
156                                def.addNode(new ForkNodeDef(eNode.getAttributeValue(NAME_A), paths));
157                            }
158                            else {
159                                if (eNode.getName().equals(JOIN_E)) {
160                                    def.addNode(new JoinNodeDef(eNode.getAttributeValue(NAME_A), eNode.getAttributeValue(TO_A)));
161                                }
162                                else {
163                                    if (eNode.getName().equals(DECISION_E)) {
164                                        Element eSwitch = eNode.getChild(DECISION_SWITCH_E, ns);
165                                        List<String> transitions = new ArrayList<String>();
166                                        for (Element e : (List<Element>) eSwitch.getChildren(DECISION_CASE_E, ns)) {
167                                            transitions.add(e.getAttributeValue(TO_A));
168                                        }
169                                        transitions.add(eSwitch.getChild(DECISION_DEFAULT_E, ns).getAttributeValue(TO_A));
170    
171                                        String switchStatement = XmlUtils.prettyPrint(eSwitch).toString();
172                                        def.addNode(new DecisionNodeDef(eNode.getAttributeValue(NAME_A), switchStatement, decisionHandlerClass,
173                                                                        transitions));
174                                    }
175                                    else {
176                                        if (ACTION_E.equals(eNode.getName())) {
177                                            String[] transitions = new String[2];
178                                            Element eActionConf = null;
179                                            for (Element elem : (List<Element>) eNode.getChildren()) {
180                                                if (ACTION_OK_E.equals(elem.getName())) {
181                                                    transitions[0] = elem.getAttributeValue(TO_A);
182                                                }
183                                                else {
184                                                    if (ACTION_ERROR_E.equals(elem.getName())) {
185                                                        transitions[1] = elem.getAttributeValue(TO_A);
186                                                    }
187                                                    else {
188                                                        if (SLA_INFO.equals(elem.getName())) {
189                                                            continue;
190                                                        }
191                                                        else {
192                                                            eActionConf = elem;
193                                                        }
194                                                    }
195                                                }
196                                            }
197                                            String actionConf = XmlUtils.prettyPrint(eActionConf).toString();
198                                            def.addNode(new ActionNodeDef(eNode.getAttributeValue(NAME_A), actionConf, actionHandlerClass,
199                                                                          transitions[0], transitions[1]));
200                                        }
201                                        else {
202                                            if (SLA_INFO.equals(eNode.getName())) {
203                                                // No operation is required
204                                            }
205                                            else {
206                                                throw new WorkflowException(ErrorCode.E0703, eNode.getName());
207                                            }
208                                        }
209                                    }
210                                }
211                            }
212                        }
213                    }
214                }
215            }
216            return def;
217        }
218    
219        /**
220         * Validate workflow xml
221         *
222         * @param app
223         * @param node
224         * @param traversed
225         * @throws WorkflowException
226         */
227        private void validate(LiteWorkflowApp app, NodeDef node, Map<String, VisitStatus> traversed) throws WorkflowException {
228            if (!(node instanceof StartNodeDef)) {
229                try {
230                    ParamChecker.validateActionName(node.getName());
231                }
232                catch (IllegalArgumentException ex) {
233                    throw new WorkflowException(ErrorCode.E0724, ex.getMessage());
234                }
235            }
236            if (node instanceof ActionNodeDef) {
237                try {
238                    Element action = XmlUtils.parseXml(node.getConf());
239                    boolean supportedAction = Services.get().get(ActionService.class).getExecutor(action.getName()) != null;
240                    if (!supportedAction) {
241                        throw new WorkflowException(ErrorCode.E0723, node.getName(), action.getName());
242                    }
243                }
244                catch (JDOMException ex) {
245                    throw new RuntimeException("It should never happen, " + ex.getMessage(), ex);
246                }
247            }
248    
249            if (node instanceof EndNodeDef) {
250                traversed.put(node.getName(), VisitStatus.VISITED);
251                return;
252            }
253            if (node instanceof KillNodeDef) {
254                traversed.put(node.getName(), VisitStatus.VISITED);
255                return;
256            }
257            for (String transition : node.getTransitions()) {
258    
259                if (app.getNode(transition) == null) {
260                    throw new WorkflowException(ErrorCode.E0708, node.getName(), transition);
261                }
262    
263                //check if it is a cycle
264                if (traversed.get(app.getNode(transition).getName()) == VisitStatus.VISITING) {
265                    throw new WorkflowException(ErrorCode.E0707, app.getNode(transition).getName());
266                }
267                //ignore validated one
268                if (traversed.get(app.getNode(transition).getName()) == VisitStatus.VISITED) {
269                    continue;
270                }
271    
272                traversed.put(app.getNode(transition).getName(), VisitStatus.VISITING);
273                validate(app, app.getNode(transition), traversed);
274            }
275            traversed.put(node.getName(), VisitStatus.VISITED);
276        }
277    }