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 }