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 }