Oozie workflow.xml 视图解析

    最近半年多一直在做hive相关的开发工作,并且使用Oozie做为hive工作流的引擎,用于管理Hadoop任务。Oozie的任务流包括:croodinator、workflow。workflow用于描述任务的执行顺序,croodinator用于定义oozie的定时任务。workflow定义了两种结点:

  • 控制流结点:主要包括start、end、fork、join等,其中fork、join成对出现,在fork展开。分支,最后在join结点汇聚。
  • Action结点:包括Hadoop任务、SSH、HTTP、EMAIL、OOZIE子任务等。
    workflow.xml用于配置workflow任务动作,当job的脚本较多,解读起来比较困难,并且出现并发的时,解析就更困难了。近期在做hadoop的旧job的优化,涉及比较多Job,其中大多数,都是其他同事开发的,而workflow的解读又工作过程中不得不面对的繁琐工作。于是闲暇之余,写了一个workflow.xml文件解析工具:输入job的名称,能显示该job的流程图。需要解决的问题主要有两方面 :
  1. job的workflow.xml文件的读取、解析。
  2. 结点视图的绘制。
    xml文件的解析,使用dom4j包就能轻松解决。workflow文件的可以直接从svn主干上读取。由于本人对基于j2ee的web开发比较熟悉,最终决定用网页展示结点视图。网上找了个html的绘图插件raphael,用于网页上失量图形。编程思路有了,下面是开始代码的实现,整个流程如下:
  1. svn job代码下载:svn主要的代码不定期的会从其分支合入新代码,需要写一个定时器,每天去全量的同步svn的代码。
  2. dom4j解析workflow.xml,抽象结点对象,视图数据准备阶段。
  3. 将视力数据利用Freemarker模板工具,解析到客户端,客户端根据结点数据,绘制结点。
job搜索
    为了尽量简化用户的操作,在网而上做了一个搜索job的功能,使用jquery 的autocomplete方法实现:当用户输入job关键字时,模糊搜索svn上的的job,显示配置上的job,简化用户输入。

workflow数据解析
    编码的核心工作是第2步,xml文件的解析,使用dom4j很容易就能将workflow.xml解析成结点的链表。每个结点,保存着下一步要执行的结点链表。困难的是,要把链表数据,转化视图展示的坐标,输出到客户端。我们不妨将50px视为一个坐标单位,结点只显示边框,在边框中显示结点名,并且结点与结点之前横向与纵向都间隔一个单位。下面分享我的坐标转化算法:
    结点的高度占一个单位,宽度与结点名文本长度相关(5个字符占一个单位)。利用递归算法,从开始结点遍历结点列表,直到结束结点。我们不妨将结点的上一步执行结点叫结点的前结点,下一步执行结点,叫做结点的后结点。
 结点横坐标=MAX(前结点的横坐标+结点宽度+1)
 结点纵坐标=MAX(前结点的纵坐标)
使用递归的算法,当视图中加入新的结点时,可能会引起视图中的结点的前结点、后结点个数的改变,需要重新调整结点的横、纵坐标。

代码实现
    核心代码如下:
OozieHelper.java
package com.lxr.oozie.workflow;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.dom4j.Document;
import org.dom4j.Element;

public class OozieHelper {

	private Document document;
	private Element root = null;
	private List<WorkflowNode> workflowNodes = new ArrayList<WorkflowNode>();
	private Map<String, WorkflowNode> workflowNodeMap = new HashMap<String, WorkflowNode>();

	public OozieHelper(Document document) {
		this.document = document;
	}

	private Element getNode(String tag) {
		return root.element(tag);
	}

	private Element getNode(String attr, String value) {
		List<Element> nodes = root.elements();
		for (Element el : nodes) {
			if (value.equals(el.attributeValue(attr))) {
				return el;
			}
		}
		return null;
	}

	private Element getNodeByName(String name) {
		return getNode("name", name);
	}

	private String getNextNodeName(Element node) {
		String tagName = node.getName();
		if ("action".equals(tagName)) {
			Element element = node.element("ok");
			return element.attributeValue("to");
		} else {
			return node.attributeValue("to");
		}
	}

	private List<Element> getNextNodes(Element node) {
		if (null == node) {
			return null;
		}
		List<Element> nextNodes = new ArrayList<Element>();
		String nextNodeName = getNextNodeName(node);
		if (null != nextNodeName) {
			Element nextNode = getNodeByName(nextNodeName);
			String tagName = nextNode.getName();
			if ("fork".equalsIgnoreCase(tagName)) {
				List<Element> elements = nextNode.elements();
				for (Element el : elements) {
					nextNodeName = el.attributeValue("start");
					if (null != nextNodeName) {
						nextNode = getNodeByName(nextNodeName);
						if (null != nextNode) {
							nextNodes.add(nextNode);
						}
					}
				}
			} else if ("join".equals(tagName)) {
				nextNode = getNodeByName(nextNode.attributeValue("to"));
				nextNodes.add(nextNode);
			} else {
				nextNodes.add(nextNode);
			}
		}
		return nextNodes;
	}

	private void adjustToAddNode(WorkflowNode workflowNode) {
		for (WorkflowNode node : workflowNodes) {
			if (node.equals(workflowNode)) {
				return;
			}
		}
		workflowNodes.add(workflowNode);
	}

	private void genNextWorkflowNodes(WorkflowNode parent) {
		List<Element> nextNodes = getNextNodes(parent.getElement());
		if (0 != nextNodes.size()) {
			for (int i = 0, len = nextNodes.size(); i < len; i++) {
				Element el = nextNodes.get(i);
				String nodeName = el.attributeValue("name");
				WorkflowNode subWorkflowNode = workflowNodeMap.get(nodeName);
				int subX = parent.getX() + parent.getLength() + 1;
				int subY = parent.getY() + 2;
				if (null == subWorkflowNode) {
					subWorkflowNode = new WorkflowNode(el);
					subWorkflowNode.setName(nodeName);

					subWorkflowNode.setX(subX);
					subWorkflowNode.setY(subY);

					genNextWorkflowNodes(subWorkflowNode);
					workflowNodes.add(subWorkflowNode);
					// adjustToAddNode(subWorkflowNode);
				} else {
					subWorkflowNode.setX(Math.max(subX, subWorkflowNode.getX()));
					if (subY > subWorkflowNode.getY()) {
						subWorkflowNode.adjustNextNodesY(subY - subWorkflowNode.getY());
						subWorkflowNode.setY(subY);
					}
				}
				subWorkflowNode.previousNodes().add(parent);
				parent.nextNodes().add(subWorkflowNode);
				workflowNodeMap.put(nodeName, subWorkflowNode);
			}
		}
	}

	public List<WorkflowNode> parse() {
		root = document.getRootElement();
		Element startNode = getNode("start");
		if (null != startNode) {
			WorkflowNode start = new WorkflowNode(startNode);
			start.setName("start");
			start.setX(0);
			start.setY(0);
			workflowNodes.add(start);
			genNextWorkflowNodes(start);
		} else {
			System.out.println("未找到开始结点。");
		}
		return workflowNodes;
	}
}
view-workflow.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Oozie Workflow - ${jobName}</title>
<script type="text/javascript" src="../js/jquery.min.js"></script>
<script type="text/javascript" src="../js/raphael/raphael.min.js"></script>
<script type="text/javascript" src="../js/raphael/raphael.ext.js"></script>
<script type="text/javascript" src="../js/oozie.wf.draw.js"></script>
</head>

<body>
	<label style="position: absolute;">job名称:${jobName}</label>
	<div class="workflow-holder" id="${jobName}" margin-left="100"
		margin-top="100" grid-padding-left="15" grid-padding-top="10"
		grid-width="50" grid-height="25">
		<#list nodes as node>
		<div id="${node.name}" class="branch-node workflow-node"
			xx="${node.x}" yy="${node.y}" length="${node.length}"
			next-node="${node.nextJobNames!}">${node.name}</div>
		</#list>
	</div>
</body>
</html>
oozie.wf.draw.js
jQuery.fn.attrn = function(attr) {
	return parseInt($(this).attr(attr));
}

HTMLDivElement.prototype.$$ = function(attr) {
	return $(this).attr(attr);
}

HTMLDivElement.prototype.$$n = function(attr) {
	return parseInt(this.$$(attr));
}

$(function() {
	var $holder = $('.workflow-holder');
	var $nodes = $holder.find('.workflow-node');
	var nodes = [];
	$nodes.each(function(i, el) {
		nodes[i] = {
			id : $(el).attr('id'),
			index : i,
			xx : el.$$n('xx'),
			yy : el.$$n('yy'),
			length: el.$$n('length'),
			$instance : $(el)
		}
	});
	var workflowCfg = {
		id : $holder.attr('id'),
		margin : {
			left : $holder.attrn('margin-left'),
			top : $holder.attrn('margin-top')
		},
		grid : {
			paddingLeft : $holder.attrn('grid-padding-left'),
			paddingTop : $holder.attrn('grid-padding-top'),
			width : $holder.attrn('grid-width'),
			height : $holder.attrn('grid-height')
		},
		nodes : nodes,
		nodesMap : (function() {
			var map = {};
			for (var i = 0; i < nodes.length; i++) {
				var node = nodes[i];
				map[node.$instance.attr('id')] = node;
			}
			return map;
		})()
	};

	console.log(workflowCfg)

	// 用来存储节点的顺序
	var connections = [];
	// 拖动节点开始时的事件
	var dragger = function() {
		this.ox = this.attr('x');
		this.oy = this.attr('y');
		this.animate({
			'fill-opacity' : .2
		}, 500);
	};
	// 拖动事件
	var move = function(dx, dy) {
		var att = {
			x : this.ox + dx,
			y : this.oy + dy
		};
		this.attr(att);
		$holder.find("#" + this.id).offset({
			top : this.oy + dy + workflowCfg.grid.paddingTop,
			left : this.ox + dx + workflowCfg.grid.paddingLeft
		});
		for (var i = connections.length; i--;) {
			r.drawArr(connections[i]);
		}
	};
	// 拖动结束后的事件
	var up = function() {
		this.animate({
			'fill-opacity' : 0
		}, 500);
	};
	// 创建绘图对象
	var r = Raphael(workflowCfg.id, $(window).width(), $(window).height());
	// 绘制节点
	var shapes = [];
	var maxRight = 0;
	for (var i = 0, len = workflowCfg.nodes.length; i < len; i++) {
		var node = workflowCfg.nodes[i];
		node.left = workflowCfg.margin.left + node.xx * workflowCfg.grid.width;
		node.top = workflowCfg.margin.top + node.yy * workflowCfg.grid.height;
		node.width = workflowCfg.grid.width * node.length;
		node.height = workflowCfg.grid.height;
		shapes[i] = r.rect(node.left, node.top, node.width, node.height, 4);
		// 定位节点上的文字
		node.$instance.offset({
			top : node.top + workflowCfg.grid.paddingTop,
			left : node.left + workflowCfg.grid.paddingLeft
		});
		var right = node.$instance.offset().left + node.width;
		maxRight = maxRight > right? maxRight : right;
	}
	var $svg = $holder.find('svg');
	var svnWidth = maxRight + workflowCfg.grid.paddingLeft;
	var _svnWidth = $svg.attrn('width');
	$svg.attr('width', svnWidth > _svnWidth? svnWidth : _svnWidth);
	// 为节点添加样式和事件,并且绘制节点之间的箭头
	for (var i = 0, ii = shapes.length; i < ii; i++) {
		var color = Raphael.getColor();
		shapes[i].attr({
			fill : color,
			stroke : color,
			'fill-opacity' : 0,
			'stroke-width' : 2,
			cursor : 'move'
		});
		shapes[i].id = workflowCfg.nodes[i].id;
		shapes[i].drag(move, dragger, up);
		shapes[i].dblclick(function() {
			alert(this.id)
		})
	}
	// 节点连线
	for (var i = 0; i < workflowCfg.nodes.length; i++) {
		var node = workflowCfg.nodes[i];
		var nextNodeIds = node.$instance.attr('next-node');
		if (nextNodeIds) {
			var nextNodeIdArr = nextNodeIds.split(',');
			for (var j = 0; j < nextNodeIdArr.length; j++) {
				var nextNodeId = nextNodeIdArr[j];
				var nextNode = workflowCfg.nodesMap[nextNodeId];
				connections.push(r.drawArr({
					obj1 : shapes[node.index],
					obj2 : shapes[nextNode.index]
				}));
			}
		}
	}
});
运行效果如下:

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值