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 }