/** * Copyright (c) 2010 Yahoo! Inc. All rights reserved. * Licensed 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. See accompanying LICENSE file. */ package org.apache.oozie.workflow.lite; import org.apache.oozie.workflow.WorkflowException; import org.apache.oozie.util.IOUtils; import org.apache.oozie.util.XmlUtils; import org.apache.oozie.util.ParamChecker; import org.apache.oozie.ErrorCode; import org.apache.oozie.service.Services; import org.apache.oozie.service.ActionService; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.Namespace; import org.xml.sax.SAXException; import javax.xml.transform.stream.StreamSource; import javax.xml.validation.Schema; import javax.xml.validation.Validator; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Class to parse and validate workflow xml */ public class LiteWorkflowAppParser { private static final String DECISION_E = "decision"; private static final String ACTION_E = "action"; private static final String END_E = "end"; private static final String START_E = "start"; private static final String JOIN_E = "join"; private static final String FORK_E = "fork"; private static final Object KILL_E = "kill"; private static final String SLA_INFO = "info"; private static final String CREDENTIALS = "credentials"; private static final String NAME_A = "name"; private static final String CRED_A = "cred"; private static final String TO_A = "to"; private static final String FORK_PATH_E = "path"; private static final String FORK_START_A = "start"; private static final String ACTION_OK_E = "ok"; private static final String ACTION_ERROR_E = "error"; private static final String DECISION_SWITCH_E = "switch"; private static final String DECISION_CASE_E = "case"; private static final String DECISION_DEFAULT_E = "default"; private static final String KILL_MESSAGE_E = "message"; private Schema schema; private Class<? extends DecisionNodeHandler> decisionHandlerClass; private Class<? extends ActionNodeHandler> actionHandlerClass; private static enum VisitStatus { VISITING, VISITED } ; public LiteWorkflowAppParser(Schema schema, Class<? extends DecisionNodeHandler> decisionHandlerClass, Class<? extends ActionNodeHandler> actionHandlerClass) throws WorkflowException { this.schema = schema; this.decisionHandlerClass = decisionHandlerClass; this.actionHandlerClass = actionHandlerClass; } /** * Parse and validate xml to {@link LiteWorkflowApp} * * @param reader * @return LiteWorkflowApp * @throws WorkflowException */ public LiteWorkflowApp validateAndParse(Reader reader) throws WorkflowException { try { StringWriter writer = new StringWriter(); IOUtils.copyCharStream(reader, writer); String strDef = writer.toString(); if (schema != null) { Validator validator = schema.newValidator(); validator.validate(new StreamSource(new StringReader(strDef))); } Element wfDefElement = XmlUtils.parseXml(strDef); LiteWorkflowApp app = parse(strDef, wfDefElement); Map<String, VisitStatus> traversed = new HashMap<String, VisitStatus>(); traversed.put(app.getNode(StartNodeDef.START).getName(), VisitStatus.VISITING); validate(app, app.getNode(StartNodeDef.START), traversed); return app; } catch (JDOMException ex) { throw new WorkflowException(ErrorCode.E0700, ex.getMessage(), ex); } catch (SAXException ex) { throw new WorkflowException(ErrorCode.E0701, ex.getMessage(), ex); } catch (IOException ex) { throw new WorkflowException(ErrorCode.E0702, ex.getMessage(), ex); } } /** * Parse xml to {@link LiteWorkflowApp} * * @param strDef * @param root * @return LiteWorkflowApp * @throws WorkflowException */ @SuppressWarnings({"unchecked", "ConstantConditions"}) private LiteWorkflowApp parse(String strDef, Element root) throws WorkflowException { Namespace ns = root.getNamespace(); LiteWorkflowApp def = null; for (Element eNode : (List<Element>) root.getChildren()) { if (eNode.getName().equals(START_E)) { def = new LiteWorkflowApp(root.getAttributeValue(NAME_A), strDef, new StartNodeDef(eNode.getAttributeValue(TO_A))); } else { if (eNode.getName().equals(END_E)) { def.addNode(new EndNodeDef(eNode.getAttributeValue(NAME_A))); } else { if (eNode.getName().equals(KILL_E)) { def.addNode(new KillNodeDef(eNode.getAttributeValue(NAME_A), eNode.getChildText(KILL_MESSAGE_E, ns))); } else { if (eNode.getName().equals(FORK_E)) { List<String> paths = new ArrayList<String>(); for (Element tran : (List<Element>) eNode.getChildren(FORK_PATH_E, ns)) { paths.add(tran.getAttributeValue(FORK_START_A)); } def.addNode(new ForkNodeDef(eNode.getAttributeValue(NAME_A), paths)); } else { if (eNode.getName().equals(JOIN_E)) { def.addNode(new JoinNodeDef(eNode.getAttributeValue(NAME_A), eNode.getAttributeValue(TO_A))); } else { if (eNode.getName().equals(DECISION_E)) { Element eSwitch = eNode.getChild(DECISION_SWITCH_E, ns); List<String> transitions = new ArrayList<String>(); for (Element e : (List<Element>) eSwitch.getChildren(DECISION_CASE_E, ns)) { transitions.add(e.getAttributeValue(TO_A)); } transitions.add(eSwitch.getChild(DECISION_DEFAULT_E, ns).getAttributeValue(TO_A)); String switchStatement = XmlUtils.prettyPrint(eSwitch).toString(); def.addNode(new DecisionNodeDef(eNode.getAttributeValue(NAME_A), switchStatement, decisionHandlerClass, transitions)); } else { if (ACTION_E.equals(eNode.getName())) { String[] transitions = new String[2]; Element eActionConf = null; for (Element elem : (List<Element>) eNode.getChildren()) { if (ACTION_OK_E.equals(elem.getName())) { transitions[0] = elem.getAttributeValue(TO_A); } else { if (ACTION_ERROR_E.equals(elem.getName())) { transitions[1] = elem.getAttributeValue(TO_A); } else { if (SLA_INFO.equals(elem.getName()) || CREDENTIALS.equals(elem.getName())) { continue; } else { eActionConf = elem; } } } } String actionConf = XmlUtils.prettyPrint(eActionConf).toString(); def.addNode(new ActionNodeDef(eNode.getAttributeValue(NAME_A), actionConf, actionHandlerClass, transitions[0], transitions[1], eNode.getAttributeValue(CRED_A))); } else { if (SLA_INFO.equals(eNode.getName()) || CREDENTIALS.equals(eNode.getName())) { // No operation is required } else { throw new WorkflowException(ErrorCode.E0703, eNode.getName()); } } } } } } } } } return def; } /** * Validate workflow xml * * @param app * @param node * @param traversed * @throws WorkflowException */ private void validate(LiteWorkflowApp app, NodeDef node, Map<String, VisitStatus> traversed) throws WorkflowException { if (!(node instanceof StartNodeDef)) { try { ParamChecker.validateActionName(node.getName()); } catch (IllegalArgumentException ex) { throw new WorkflowException(ErrorCode.E0724, ex.getMessage()); } } if (node instanceof ActionNodeDef) { try { Element action = XmlUtils.parseXml(node.getConf()); boolean supportedAction = Services.get().get(ActionService.class).getExecutor(action.getName()) != null; if (!supportedAction) { throw new WorkflowException(ErrorCode.E0723, node.getName(), action.getName()); } } catch (JDOMException ex) { throw new RuntimeException("It should never happen, " + ex.getMessage(), ex); } } if (node instanceof EndNodeDef) { traversed.put(node.getName(), VisitStatus.VISITED); return; } if (node instanceof KillNodeDef) { traversed.put(node.getName(), VisitStatus.VISITED); return; } for (String transition : node.getTransitions()) { if (app.getNode(transition) == null) { throw new WorkflowException(ErrorCode.E0708, node.getName(), transition); } //check if it is a cycle if (traversed.get(app.getNode(transition).getName()) == VisitStatus.VISITING) { throw new WorkflowException(ErrorCode.E0707, app.getNode(transition).getName()); } //ignore validated one if (traversed.get(app.getNode(transition).getName()) == VisitStatus.VISITED) { continue; } traversed.put(app.getNode(transition).getName(), VisitStatus.VISITING); validate(app, app.getNode(transition), traversed); } traversed.put(node.getName(), VisitStatus.VISITED); } }