001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *      http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.oozie.workflow.lite;
020
021import java.io.ByteArrayInputStream;
022import java.io.ByteArrayOutputStream;
023import java.io.DataInput;
024import java.io.DataInputStream;
025import java.io.DataOutput;
026import java.io.DataOutputStream;
027import java.io.IOException;
028import java.io.Reader;
029import java.io.StringReader;
030import java.io.StringWriter;
031import java.util.ArrayList;
032import java.util.List;
033import java.util.zip.Deflater;
034import java.util.zip.DeflaterOutputStream;
035import java.util.zip.Inflater;
036import java.util.zip.InflaterInputStream;
037
038import javax.xml.transform.stream.StreamSource;
039import javax.xml.validation.Schema;
040import javax.xml.validation.Validator;
041
042import org.apache.commons.codec.binary.Base64;
043import org.apache.commons.lang.StringUtils;
044import org.apache.hadoop.conf.Configuration;
045import org.apache.hadoop.io.Writable;
046import org.apache.oozie.ErrorCode;
047import org.apache.oozie.action.ActionExecutor;
048import org.apache.oozie.action.hadoop.FsActionExecutor;
049import org.apache.oozie.action.oozie.SubWorkflowActionExecutor;
050import org.apache.oozie.service.ActionService;
051import org.apache.oozie.service.ConfigurationService;
052import org.apache.oozie.service.Services;
053import org.apache.oozie.util.ELUtils;
054import org.apache.oozie.util.IOUtils;
055import org.apache.oozie.util.ParameterVerifier;
056import org.apache.oozie.util.ParameterVerifierException;
057import org.apache.oozie.util.WritableUtils;
058import org.apache.oozie.util.XConfiguration;
059import org.apache.oozie.util.XmlUtils;
060import org.apache.oozie.workflow.WorkflowException;
061import org.jdom.Element;
062import org.jdom.JDOMException;
063import org.jdom.Namespace;
064import org.xml.sax.SAXException;
065
066/**
067 * Class to parse and validate workflow xml
068 */
069public class LiteWorkflowAppParser {
070
071    private static final String LAUNCHER_E = "launcher";
072    private static final String DECISION_E = "decision";
073    private static final String ACTION_E = "action";
074    private static final String END_E = "end";
075    private static final String START_E = "start";
076    private static final String JOIN_E = "join";
077    private static final String FORK_E = "fork";
078    private static final Object KILL_E = "kill";
079
080    private static final String SLA_INFO = "info";
081    private static final String CREDENTIALS = "credentials";
082    private static final String GLOBAL = "global";
083    private static final String PARAMETERS = "parameters";
084
085    private static final String NAME_A = "name";
086    private static final String CRED_A = "cred";
087    private static final String USER_RETRY_MAX_A = "retry-max";
088    private static final String USER_RETRY_INTERVAL_A = "retry-interval";
089    private static final String TO_A = "to";
090    private static final String USER_RETRY_POLICY_A = "retry-policy";
091
092    private static final String FORK_PATH_E = "path";
093    private static final String FORK_START_A = "start";
094
095    private static final String ACTION_OK_E = "ok";
096    private static final String ACTION_ERROR_E = "error";
097
098    private static final String DECISION_SWITCH_E = "switch";
099    private static final String DECISION_CASE_E = "case";
100    private static final String DECISION_DEFAULT_E = "default";
101
102    private static final String SUBWORKFLOW_E = "sub-workflow";
103
104    private static final String KILL_MESSAGE_E = "message";
105    public static final String VALIDATE_FORK_JOIN = "oozie.validate.ForkJoin";
106    public static final String WF_VALIDATE_FORK_JOIN = "oozie.wf.validate.ForkJoin";
107
108    public static final String DEFAULT_NAME_NODE = "oozie.actions.default.name-node";
109    public static final String DEFAULT_JOB_TRACKER = "oozie.actions.default.job-tracker";
110    public static final String OOZIE_GLOBAL = "oozie.wf.globalconf";
111
112    private static final String JOB_TRACKER = "job-tracker";
113    private static final String NAME_NODE = "name-node";
114    private static final String JOB_XML = "job-xml";
115    private static final String CONFIGURATION = "configuration";
116
117    private Schema schema;
118    private Class<? extends ControlNodeHandler> controlNodeHandler;
119    private Class<? extends DecisionNodeHandler> decisionHandlerClass;
120    private Class<? extends ActionNodeHandler> actionHandlerClass;
121
122    private String defaultNameNode;
123    private String defaultJobTracker;
124
125    public LiteWorkflowAppParser(Schema schema,
126                                 Class<? extends ControlNodeHandler> controlNodeHandler,
127                                 Class<? extends DecisionNodeHandler> decisionHandlerClass,
128                                 Class<? extends ActionNodeHandler> actionHandlerClass) throws WorkflowException {
129        this.schema = schema;
130        this.controlNodeHandler = controlNodeHandler;
131        this.decisionHandlerClass = decisionHandlerClass;
132        this.actionHandlerClass = actionHandlerClass;
133
134        defaultNameNode = ConfigurationService.get(DEFAULT_NAME_NODE);
135        if (defaultNameNode != null) {
136            defaultNameNode = defaultNameNode.trim();
137            if (defaultNameNode.isEmpty()) {
138                defaultNameNode = null;
139            }
140        }
141        defaultJobTracker = ConfigurationService.get(DEFAULT_JOB_TRACKER);
142        if (defaultJobTracker != null) {
143            defaultJobTracker = defaultJobTracker.trim();
144            if (defaultJobTracker.isEmpty()) {
145                defaultJobTracker = null;
146            }
147        }
148    }
149
150    public LiteWorkflowApp validateAndParse(Reader reader, Configuration jobConf) throws WorkflowException {
151        return validateAndParse(reader, jobConf, null);
152    }
153
154    /**
155     * Parse and validate xml to {@link LiteWorkflowApp}
156     *
157     * @param reader
158     * @return LiteWorkflowApp
159     * @throws WorkflowException
160     */
161    public LiteWorkflowApp validateAndParse(Reader reader, Configuration jobConf, Configuration configDefault)
162            throws WorkflowException {
163        try {
164            StringWriter writer = new StringWriter();
165            IOUtils.copyCharStream(reader, writer);
166            String strDef = writer.toString();
167
168            if (schema != null) {
169                Validator validator = schema.newValidator();
170                validator.validate(new StreamSource(new StringReader(strDef)));
171            }
172
173            Element wfDefElement = XmlUtils.parseXml(strDef);
174            ParameterVerifier.verifyParameters(jobConf, wfDefElement);
175            LiteWorkflowApp app = parse(strDef, wfDefElement, configDefault, jobConf);
176
177
178            boolean validateForkJoin = false;
179
180            if (jobConf.getBoolean(WF_VALIDATE_FORK_JOIN, true)
181                    && ConfigurationService.getBoolean(VALIDATE_FORK_JOIN)) {
182                validateForkJoin = true;
183            }
184
185            LiteWorkflowValidator validator = new LiteWorkflowValidator();
186            validator.validateWorkflow(app, validateForkJoin);
187
188            return app;
189        }
190        catch (ParameterVerifierException ex) {
191            throw new WorkflowException(ex);
192        }
193        catch (JDOMException ex) {
194            throw new WorkflowException(ErrorCode.E0700, ex.getMessage(), ex);
195        }
196        catch (SAXException ex) {
197            throw new WorkflowException(ErrorCode.E0701, ex.getMessage(), ex);
198        }
199        catch (IOException ex) {
200            throw new WorkflowException(ErrorCode.E0702, ex.getMessage(), ex);
201        }
202    }
203
204    /**
205     * Parse xml to {@link LiteWorkflowApp}
206     *
207     * @param strDef
208     * @param root
209     * @param configDefault
210     * @param jobConf
211     * @return LiteWorkflowApp
212     * @throws WorkflowException
213     */
214    @SuppressWarnings({"unchecked"})
215    private LiteWorkflowApp parse(String strDef, Element root, Configuration configDefault, Configuration jobConf)
216            throws WorkflowException {
217        Namespace ns = root.getNamespace();
218
219        LiteWorkflowApp def = null;
220        GlobalSectionData gData = jobConf.get(OOZIE_GLOBAL) == null ?
221                null : getGlobalFromString(jobConf.get(OOZIE_GLOBAL));
222        boolean serializedGlobalConf = false;
223        for (Element eNode : (List<Element>) root.getChildren()) {
224            if (eNode.getName().equals(START_E)) {
225                def = new LiteWorkflowApp(root.getAttributeValue(NAME_A), strDef,
226                                          new StartNodeDef(controlNodeHandler, eNode.getAttributeValue(TO_A)));
227            } else if (eNode.getName().equals(END_E)) {
228                def.addNode(new EndNodeDef(eNode.getAttributeValue(NAME_A), controlNodeHandler));
229            } else if (eNode.getName().equals(KILL_E)) {
230                def.addNode(new KillNodeDef(eNode.getAttributeValue(NAME_A),
231                                            eNode.getChildText(KILL_MESSAGE_E, ns), controlNodeHandler));
232            } else if (eNode.getName().equals(FORK_E)) {
233                List<String> paths = new ArrayList<String>();
234                for (Element tran : (List<Element>) eNode.getChildren(FORK_PATH_E, ns)) {
235                    paths.add(tran.getAttributeValue(FORK_START_A));
236                }
237                def.addNode(new ForkNodeDef(eNode.getAttributeValue(NAME_A), controlNodeHandler, paths));
238            } else if (eNode.getName().equals(JOIN_E)) {
239                def.addNode(new JoinNodeDef(eNode.getAttributeValue(NAME_A), controlNodeHandler, eNode.getAttributeValue(TO_A)));
240            } else if (eNode.getName().equals(DECISION_E)) {
241                Element eSwitch = eNode.getChild(DECISION_SWITCH_E, ns);
242                List<String> transitions = new ArrayList<String>();
243                for (Element e : (List<Element>) eSwitch.getChildren(DECISION_CASE_E, ns)) {
244                    transitions.add(e.getAttributeValue(TO_A));
245                }
246                transitions.add(eSwitch.getChild(DECISION_DEFAULT_E, ns).getAttributeValue(TO_A));
247
248                String switchStatement = XmlUtils.prettyPrint(eSwitch).toString();
249                def.addNode(new DecisionNodeDef(eNode.getAttributeValue(NAME_A), switchStatement, decisionHandlerClass,
250                                                transitions));
251            } else if (ACTION_E.equals(eNode.getName())) {
252                String[] transitions = new String[2];
253                Element eActionConf = null;
254                for (Element elem : (List<Element>) eNode.getChildren()) {
255                    if (ACTION_OK_E.equals(elem.getName())) {
256                        transitions[0] = elem.getAttributeValue(TO_A);
257                    } else if (ACTION_ERROR_E.equals(elem.getName())) {
258                        transitions[1] = elem.getAttributeValue(TO_A);
259                    } else if (SLA_INFO.equals(elem.getName()) || CREDENTIALS.equals(elem.getName())) {
260                        continue;
261                    } else {
262                        if (!serializedGlobalConf && elem.getName().equals(SubWorkflowActionExecutor.ACTION_TYPE) &&
263                                elem.getChild(("propagate-configuration"), ns) != null && gData != null) {
264                            serializedGlobalConf = true;
265                            jobConf.set(OOZIE_GLOBAL, getGlobalString(gData));
266                        }
267                        eActionConf = elem;
268                        if (SUBWORKFLOW_E.equals(elem.getName())) {
269                            handleDefaultsAndGlobal(gData, null, elem, ns);
270                        }
271                        else {
272                            handleDefaultsAndGlobal(gData, configDefault, elem, ns);
273                        }
274                    }
275                }
276
277                String credStr = eNode.getAttributeValue(CRED_A);
278                String userRetryMaxStr = eNode.getAttributeValue(USER_RETRY_MAX_A);
279                String userRetryIntervalStr = eNode.getAttributeValue(USER_RETRY_INTERVAL_A);
280                String userRetryPolicyStr = eNode.getAttributeValue(USER_RETRY_POLICY_A);
281                try {
282                    if (!StringUtils.isEmpty(userRetryMaxStr)) {
283                        userRetryMaxStr = ELUtils.resolveAppName(userRetryMaxStr, jobConf);
284                    }
285                    if (!StringUtils.isEmpty(userRetryIntervalStr)) {
286                        userRetryIntervalStr = ELUtils.resolveAppName(userRetryIntervalStr, jobConf);
287                    }
288                    if (!StringUtils.isEmpty(userRetryPolicyStr)) {
289                        userRetryPolicyStr = ELUtils.resolveAppName(userRetryPolicyStr, jobConf);
290                    }
291                }
292                catch (Exception e) {
293                    throw new WorkflowException(ErrorCode.E0703, e.getMessage());
294                }
295
296                String actionConf = XmlUtils.prettyPrint(eActionConf).toString();
297                def.addNode(new ActionNodeDef(eNode.getAttributeValue(NAME_A), actionConf, actionHandlerClass,
298                        transitions[0], transitions[1], credStr, userRetryMaxStr, userRetryIntervalStr,
299                        userRetryPolicyStr));
300            } else if (SLA_INFO.equals(eNode.getName()) || CREDENTIALS.equals(eNode.getName())) {
301                // No operation is required
302            } else if (eNode.getName().equals(GLOBAL)) {
303                if(jobConf.get(OOZIE_GLOBAL) != null) {
304                    gData = getGlobalFromString(jobConf.get(OOZIE_GLOBAL));
305                    handleDefaultsAndGlobal(gData, null, eNode, ns);
306                }
307
308                gData = parseGlobalSection(ns, eNode);
309
310            } else if (eNode.getName().equals(PARAMETERS)) {
311                // No operation is required
312            } else {
313                throw new WorkflowException(ErrorCode.E0703, eNode.getName());
314            }
315        }
316        return def;
317    }
318
319    /**
320     * Read the GlobalSectionData from Base64 string.
321     * @param globalStr
322     * @return GlobalSectionData
323     * @throws WorkflowException
324     */
325    private GlobalSectionData getGlobalFromString(String globalStr) throws WorkflowException {
326        GlobalSectionData globalSectionData = new GlobalSectionData();
327        try {
328            byte[] data = Base64.decodeBase64(globalStr);
329            Inflater inflater = new Inflater();
330            DataInputStream ois = new DataInputStream(new InflaterInputStream(new ByteArrayInputStream(data), inflater));
331            globalSectionData.readFields(ois);
332            ois.close();
333        } catch (Exception ex) {
334            throw new WorkflowException(ErrorCode.E0700, "Error while processing global section conf");
335        }
336        return globalSectionData;
337    }
338
339
340    /**
341     * Write the GlobalSectionData to a Base64 string.
342     * @param globalSectionData
343     * @return String
344     * @throws WorkflowException
345     */
346    private String getGlobalString(GlobalSectionData globalSectionData) throws WorkflowException {
347        ByteArrayOutputStream baos = new ByteArrayOutputStream();
348        DataOutputStream oos = null;
349        try {
350            Deflater def = new Deflater();
351            oos = new DataOutputStream(new DeflaterOutputStream(baos, def));
352            globalSectionData.write(oos);
353            oos.close();
354        } catch (IOException e) {
355            throw new WorkflowException(ErrorCode.E0700, "Error while processing global section conf");
356        }
357        return Base64.encodeBase64String(baos.toByteArray());
358    }
359
360    private void addChildElement(Element parent, Namespace ns, String childName, String childValue) {
361        Element child = new Element(childName, ns);
362        child.setText(childValue);
363        parent.addContent(child);
364    }
365
366    private class GlobalSectionData implements Writable {
367        String jobTracker;
368        String nameNode;
369        List<String> jobXmls;
370        Configuration conf;
371
372        public GlobalSectionData() {
373        }
374
375        public GlobalSectionData(String jobTracker, String nameNode, List<String> jobXmls, Configuration conf) {
376            this.jobTracker = jobTracker;
377            this.nameNode = nameNode;
378            this.jobXmls = jobXmls;
379            this.conf = conf;
380        }
381
382        @Override
383        public void write(DataOutput dataOutput) throws IOException {
384            WritableUtils.writeStr(dataOutput, jobTracker);
385            WritableUtils.writeStr(dataOutput, nameNode);
386
387            if(jobXmls != null && !jobXmls.isEmpty()) {
388                dataOutput.writeInt(jobXmls.size());
389                for (String content : jobXmls) {
390                    WritableUtils.writeStr(dataOutput, content);
391                }
392            } else {
393                dataOutput.writeInt(0);
394            }
395            if(conf != null) {
396                WritableUtils.writeStr(dataOutput, XmlUtils.prettyPrint(conf).toString());
397            } else {
398                WritableUtils.writeStr(dataOutput, null);
399            }
400        }
401
402        @Override
403        public void readFields(DataInput dataInput) throws IOException {
404            jobTracker = WritableUtils.readStr(dataInput);
405            nameNode = WritableUtils.readStr(dataInput);
406            int length = dataInput.readInt();
407            if (length > 0) {
408                jobXmls = new ArrayList<String>();
409                for (int i = 0; i < length; i++) {
410                    jobXmls.add(WritableUtils.readStr(dataInput));
411                }
412            }
413            String confString = WritableUtils.readStr(dataInput);
414            if(confString != null) {
415                conf = new XConfiguration(new StringReader(confString));
416            }
417        }
418    }
419
420    private GlobalSectionData parseGlobalSection(Namespace ns, Element global) throws WorkflowException {
421        GlobalSectionData gData = null;
422        if (global != null) {
423            String globalJobTracker = null;
424            Element globalJobTrackerElement = global.getChild(JOB_TRACKER, ns);
425            if (globalJobTrackerElement != null) {
426                globalJobTracker = globalJobTrackerElement.getValue();
427            }
428
429            String globalNameNode = null;
430            Element globalNameNodeElement = global.getChild(NAME_NODE, ns);
431            if (globalNameNodeElement != null) {
432                globalNameNode = globalNameNodeElement.getValue();
433            }
434
435            List<String> globalJobXmls = null;
436            @SuppressWarnings("unchecked")
437            List<Element> globalJobXmlElements = global.getChildren(JOB_XML, ns);
438            if (!globalJobXmlElements.isEmpty()) {
439                globalJobXmls = new ArrayList<String>(globalJobXmlElements.size());
440                for(Element jobXmlElement: globalJobXmlElements) {
441                    globalJobXmls.add(jobXmlElement.getText());
442                }
443            }
444
445            Configuration globalConf = new XConfiguration();
446            Element globalConfigurationElement = global.getChild(CONFIGURATION, ns);
447            if (globalConfigurationElement != null) {
448                try {
449                    globalConf = new XConfiguration(new StringReader(XmlUtils.prettyPrint(globalConfigurationElement).toString()));
450                } catch (IOException ioe) {
451                    throw new WorkflowException(ErrorCode.E0700, "Error while processing global section conf");
452                }
453            }
454
455            Element globalLauncherElement = global.getChild(LAUNCHER_E, ns);
456            if (globalLauncherElement != null) {
457                LauncherConfigHandler launcherConfigHandler = new LauncherConfigHandler(globalConf, globalLauncherElement, ns);
458                launcherConfigHandler.processSettings();
459            }
460            gData = new GlobalSectionData(globalJobTracker, globalNameNode, globalJobXmls, globalConf);
461        }
462        return gData;
463    }
464
465    private void handleDefaultsAndGlobal(GlobalSectionData gData, Configuration configDefault, Element actionElement, Namespace ns)
466            throws WorkflowException {
467
468        ActionExecutor ae = Services.get().get(ActionService.class).getExecutor(actionElement.getName());
469        if (ae == null && !GLOBAL.equals(actionElement.getName())) {
470            throw new WorkflowException(ErrorCode.E0723, actionElement.getName(), ActionService.class.getName());
471        }
472
473        Namespace actionNs = actionElement.getNamespace();
474
475        // If this is the global section or ActionExecutor.requiresNameNodeJobTracker() returns true, we parse the action's
476        // <name-node> and <job-tracker> fields.  If those aren't defined, we take them from the <global> section.  If those
477        // aren't defined, we take them from the oozie-site defaults.  If those aren't defined, we throw a WorkflowException.
478        // However, for the SubWorkflow and FS Actions, as well as the <global> section, we don't throw the WorkflowException.
479        // Also, we only parse the NN (not the JT) for the FS Action.
480        if (SubWorkflowActionExecutor.ACTION_TYPE.equals(actionElement.getName()) ||
481                FsActionExecutor.ACTION_TYPE.equals(actionElement.getName()) ||
482                GLOBAL.equals(actionElement.getName()) || ae.requiresNameNodeJobTracker()) {
483            if (actionElement.getChild(NAME_NODE, actionNs) == null) {
484                if (gData != null && gData.nameNode != null) {
485                    addChildElement(actionElement, actionNs, NAME_NODE, gData.nameNode);
486                } else if (defaultNameNode != null) {
487                    addChildElement(actionElement, actionNs, NAME_NODE, defaultNameNode);
488                } else if (!(SubWorkflowActionExecutor.ACTION_TYPE.equals(actionElement.getName()) ||
489                        FsActionExecutor.ACTION_TYPE.equals(actionElement.getName()) ||
490                        GLOBAL.equals(actionElement.getName()))) {
491                    throw new WorkflowException(ErrorCode.E0701, "No " + NAME_NODE + " defined");
492                }
493            }
494            if (actionElement.getChild(JOB_TRACKER, actionNs) == null &&
495                    !FsActionExecutor.ACTION_TYPE.equals(actionElement.getName())) {
496                if (gData != null && gData.jobTracker != null) {
497                    addChildElement(actionElement, actionNs, JOB_TRACKER, gData.jobTracker);
498                } else if (defaultJobTracker != null) {
499                    addChildElement(actionElement, actionNs, JOB_TRACKER, defaultJobTracker);
500                } else if (!(SubWorkflowActionExecutor.ACTION_TYPE.equals(actionElement.getName()) ||
501                        GLOBAL.equals(actionElement.getName()))) {
502                    throw new WorkflowException(ErrorCode.E0701, "No " + JOB_TRACKER + " defined");
503                }
504            }
505        }
506
507        // If this is the global section or ActionExecutor.supportsConfigurationJobXML() returns true, we parse the action's
508        // <configuration> and <job-xml> fields.  We also merge this with those from the <global> section, if given.  If none are
509        // defined, empty values are placed.  Exceptions are thrown if there's an error parsing, but not if they're not given.
510        if (GLOBAL.equals(actionElement.getName()) || ae.supportsConfigurationJobXML()) {
511            @SuppressWarnings("unchecked")
512            List<Element> actionJobXmls = actionElement.getChildren(JOB_XML, actionNs);
513            if (gData != null && gData.jobXmls != null) {
514                for(String gJobXml : gData.jobXmls) {
515                    boolean alreadyExists = false;
516                    for (Element actionXml : actionJobXmls) {
517                        if (gJobXml.equals(actionXml.getText())) {
518                            alreadyExists = true;
519                            break;
520                        }
521                    }
522                    if (!alreadyExists) {
523                        Element ejobXml = new Element(JOB_XML, actionNs);
524                        ejobXml.setText(gJobXml);
525                        actionElement.addContent(ejobXml);
526                    }
527                }
528            }
529
530            try {
531                XConfiguration actionConf = new XConfiguration();
532                if (configDefault != null)
533                    XConfiguration.copy(configDefault, actionConf);
534                if (gData != null && gData.conf != null) {
535                    XConfiguration.copy(gData.conf, actionConf);
536                }
537
538                Element launcherConfiguration = actionElement.getChild(LAUNCHER_E, actionNs);
539                if (launcherConfiguration != null) {
540                    LauncherConfigHandler launcherConfigHandler = new LauncherConfigHandler(actionConf, launcherConfiguration, actionNs);
541                    launcherConfigHandler.processSettings();
542                }
543
544                Element actionConfiguration = actionElement.getChild(CONFIGURATION, actionNs);
545                if (actionConfiguration != null) {
546                    //copy and override
547                    XConfiguration.copy(new XConfiguration(new StringReader(XmlUtils.prettyPrint(actionConfiguration).toString())),
548                            actionConf);
549                }
550
551                int position = actionElement.indexOf(actionConfiguration);
552                actionElement.removeContent(actionConfiguration); //replace with enhanced one
553                Element eConfXml = XmlUtils.parseXml(actionConf.toXmlString(false));
554                eConfXml.detach();
555                eConfXml.setNamespace(actionNs);
556                if (position > 0) {
557                    actionElement.addContent(position, eConfXml);
558                }
559                else {
560                    actionElement.addContent(eConfXml);
561                }
562            }
563            catch (IOException e) {
564                throw new WorkflowException(ErrorCode.E0700, "Error while processing action conf");
565            }
566            catch (JDOMException e) {
567                throw new WorkflowException(ErrorCode.E0700, "Error while processing action conf");
568            }
569        }
570    }
571}