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        private static final String CREDENTIALS = "credentials";
056    
057        private static final String NAME_A = "name";
058        private static final String CRED_A = "cred";
059        private static final String TO_A = "to";
060    
061        private static final String FORK_PATH_E = "path";
062        private static final String FORK_START_A = "start";
063    
064        private static final String ACTION_OK_E = "ok";
065        private static final String ACTION_ERROR_E = "error";
066    
067        private static final String DECISION_SWITCH_E = "switch";
068        private static final String DECISION_CASE_E = "case";
069        private static final String DECISION_DEFAULT_E = "default";
070    
071        private static final String KILL_MESSAGE_E = "message";
072    
073        private Schema schema;
074        private Class<? extends DecisionNodeHandler> decisionHandlerClass;
075        private Class<? extends ActionNodeHandler> actionHandlerClass;
076    
077        private static enum VisitStatus {
078            VISITING, VISITED
079        }
080    
081        ;
082    
083    
084        public LiteWorkflowAppParser(Schema schema, Class<? extends DecisionNodeHandler> decisionHandlerClass,
085                                     Class<? extends ActionNodeHandler> actionHandlerClass) throws WorkflowException {
086            this.schema = schema;
087            this.decisionHandlerClass = decisionHandlerClass;
088            this.actionHandlerClass = actionHandlerClass;
089        }
090    
091        /**
092         * Parse and validate xml to {@link LiteWorkflowApp}
093         *
094         * @param reader
095         * @return LiteWorkflowApp
096         * @throws WorkflowException
097         */
098        public LiteWorkflowApp validateAndParse(Reader reader) throws WorkflowException {
099            try {
100                StringWriter writer = new StringWriter();
101                IOUtils.copyCharStream(reader, writer);
102                String strDef = writer.toString();
103    
104                if (schema != null) {
105                    Validator validator = schema.newValidator();
106                    validator.validate(new StreamSource(new StringReader(strDef)));
107                }
108    
109                Element wfDefElement = XmlUtils.parseXml(strDef);
110                LiteWorkflowApp app = parse(strDef, wfDefElement);
111                Map<String, VisitStatus> traversed = new HashMap<String, VisitStatus>();
112                traversed.put(app.getNode(StartNodeDef.START).getName(), VisitStatus.VISITING);
113                validate(app, app.getNode(StartNodeDef.START), traversed);
114                return app;
115            }
116            catch (JDOMException ex) {
117                throw new WorkflowException(ErrorCode.E0700, ex.getMessage(), ex);
118            }
119            catch (SAXException ex) {
120                throw new WorkflowException(ErrorCode.E0701, ex.getMessage(), ex);
121            }
122            catch (IOException ex) {
123                throw new WorkflowException(ErrorCode.E0702, ex.getMessage(), ex);
124            }
125        }
126    
127        /**
128         * Parse xml to {@link LiteWorkflowApp}
129         *
130         * @param strDef
131         * @param root
132         * @return LiteWorkflowApp
133         * @throws WorkflowException
134         */
135        @SuppressWarnings({"unchecked", "ConstantConditions"})
136        private LiteWorkflowApp parse(String strDef, Element root) throws WorkflowException {
137            Namespace ns = root.getNamespace();
138            LiteWorkflowApp def = null;
139            for (Element eNode : (List<Element>) root.getChildren()) {
140                if (eNode.getName().equals(START_E)) {
141                    def = new LiteWorkflowApp(root.getAttributeValue(NAME_A), strDef,
142                                              new StartNodeDef(eNode.getAttributeValue(TO_A)));
143                }
144                else {
145                    if (eNode.getName().equals(END_E)) {
146                        def.addNode(new EndNodeDef(eNode.getAttributeValue(NAME_A)));
147                    }
148                    else {
149                        if (eNode.getName().equals(KILL_E)) {
150                            def.addNode(new KillNodeDef(eNode.getAttributeValue(NAME_A), eNode.getChildText(KILL_MESSAGE_E, ns)));
151                        }
152                        else {
153                            if (eNode.getName().equals(FORK_E)) {
154                                List<String> paths = new ArrayList<String>();
155                                for (Element tran : (List<Element>) eNode.getChildren(FORK_PATH_E, ns)) {
156                                    paths.add(tran.getAttributeValue(FORK_START_A));
157                                }
158                                def.addNode(new ForkNodeDef(eNode.getAttributeValue(NAME_A), paths));
159                            }
160                            else {
161                                if (eNode.getName().equals(JOIN_E)) {
162                                    def.addNode(new JoinNodeDef(eNode.getAttributeValue(NAME_A), eNode.getAttributeValue(TO_A)));
163                                }
164                                else {
165                                    if (eNode.getName().equals(DECISION_E)) {
166                                        Element eSwitch = eNode.getChild(DECISION_SWITCH_E, ns);
167                                        List<String> transitions = new ArrayList<String>();
168                                        for (Element e : (List<Element>) eSwitch.getChildren(DECISION_CASE_E, ns)) {
169                                            transitions.add(e.getAttributeValue(TO_A));
170                                        }
171                                        transitions.add(eSwitch.getChild(DECISION_DEFAULT_E, ns).getAttributeValue(TO_A));
172    
173                                        String switchStatement = XmlUtils.prettyPrint(eSwitch).toString();
174                                        def.addNode(new DecisionNodeDef(eNode.getAttributeValue(NAME_A), switchStatement, decisionHandlerClass,
175                                                                        transitions));
176                                    }
177                                    else {
178                                        if (ACTION_E.equals(eNode.getName())) {
179                                            String[] transitions = new String[2];
180                                            Element eActionConf = null;
181                                            for (Element elem : (List<Element>) eNode.getChildren()) {
182                                                if (ACTION_OK_E.equals(elem.getName())) {
183                                                    transitions[0] = elem.getAttributeValue(TO_A);
184                                                }
185                                                else {
186                                                    if (ACTION_ERROR_E.equals(elem.getName())) {
187                                                        transitions[1] = elem.getAttributeValue(TO_A);
188                                                    }
189                                                    else {
190                                                        if (SLA_INFO.equals(elem.getName()) || CREDENTIALS.equals(elem.getName())) {
191                                                            continue;
192                                                        }
193                                                        else {
194                                                            eActionConf = elem;
195                                                        }
196                                                    }
197                                                }
198                                            }
199                                            String actionConf = XmlUtils.prettyPrint(eActionConf).toString();
200                                            def.addNode(new ActionNodeDef(eNode.getAttributeValue(NAME_A), actionConf, actionHandlerClass,
201                                                                          transitions[0], transitions[1], eNode.getAttributeValue(CRED_A)));
202                                        }
203                                        else {
204                                            if (SLA_INFO.equals(eNode.getName()) || CREDENTIALS.equals(eNode.getName())) {
205                                                // No operation is required
206                                            }
207                                            else {
208                                                throw new WorkflowException(ErrorCode.E0703, eNode.getName());
209                                            }
210                                        }
211                                    }
212                                }
213                            }
214                        }
215                    }
216                }
217            }
218            return def;
219        }
220    
221        /**
222         * Validate workflow xml
223         *
224         * @param app
225         * @param node
226         * @param traversed
227         * @throws WorkflowException
228         */
229        private void validate(LiteWorkflowApp app, NodeDef node, Map<String, VisitStatus> traversed) throws WorkflowException {
230            if (!(node instanceof StartNodeDef)) {
231                try {
232                    ParamChecker.validateActionName(node.getName());
233                }
234                catch (IllegalArgumentException ex) {
235                    throw new WorkflowException(ErrorCode.E0724, ex.getMessage());
236                }
237            }
238            if (node instanceof ActionNodeDef) {
239                try {
240                    Element action = XmlUtils.parseXml(node.getConf());
241                    boolean supportedAction = Services.get().get(ActionService.class).getExecutor(action.getName()) != null;
242                    if (!supportedAction) {
243                        throw new WorkflowException(ErrorCode.E0723, node.getName(), action.getName());
244                    }
245                }
246                catch (JDOMException ex) {
247                    throw new RuntimeException("It should never happen, " + ex.getMessage(), ex);
248                }
249            }
250    
251            if (node instanceof EndNodeDef) {
252                traversed.put(node.getName(), VisitStatus.VISITED);
253                return;
254            }
255            if (node instanceof KillNodeDef) {
256                traversed.put(node.getName(), VisitStatus.VISITED);
257                return;
258            }
259            for (String transition : node.getTransitions()) {
260    
261                if (app.getNode(transition) == null) {
262                    throw new WorkflowException(ErrorCode.E0708, node.getName(), transition);
263                }
264    
265                //check if it is a cycle
266                if (traversed.get(app.getNode(transition).getName()) == VisitStatus.VISITING) {
267                    throw new WorkflowException(ErrorCode.E0707, app.getNode(transition).getName());
268                }
269                //ignore validated one
270                if (traversed.get(app.getNode(transition).getName()) == VisitStatus.VISITED) {
271                    continue;
272                }
273    
274                traversed.put(app.getNode(transition).getName(), VisitStatus.VISITING);
275                validate(app, app.getNode(transition), traversed);
276            }
277            traversed.put(node.getName(), VisitStatus.VISITED);
278        }
279    }