28.3. 开始实例

这节内容的目的不是给大家开发一个可用的构件,而是教大家如何开发一个自己的构件,或者如何将其它的js构件封装成一个原生构件。学习完本节内容后,大家就可以将用jQuery、Ext、YUI等js库写的构件变成自己的构件,可以像OperaMasks构件一样来使用它。

在学习本节内容时,大家不必着急按着这个步骤来编写代码,因为其中大部分工作都会由OperaMasks Studio来完成。学习时主要要看懂其中的原理,具体的开发过程及Operamasks Studio的使用请看下一节:第 28.3.1.2 节 “使用OperaMasks来开发FusionCharts Column2D构件”

此示例的构件工程及测试案例可以到http://wiki.operamasks.org/pages/viewpage.action?pageId=9437386下载。

28.3.1. FushionCharts Column2D构件

28.3.1.1. 开发过程分析

本节将会讲解一个FusionCharts free的Column2D构件效果的原生构件开发过程。FusionCharts free 是一个跨平台,跨浏览器的flash图表构件解决方案,能够被 ASP.NET, ASP, PHP, JSP, ColdFusion, Ruby on Rails, 简单 HTML 页面甚至PPT调用,其官方网站为http://www.fusionCharts.com。先看一下最终要的效果:

FusionCharts Column2D图表

图 28.2. FusionCharts Column2D图表


28.3.1.1.1. 第一步:写出HTML代码

我们先不考虑构件封装,先看一下上面图中的效果用HTML代码怎么显示一个FusionCharts Column2D图表。

<html>  
    <head>  
        1<script language="JavaScript" src="fusionCharts/FusionCharts.js"></script>  
    </head>    
    <body>  
        2<div id="chartdiv"></div>  
        <script type="text/JavaScript">  
        3var myChart = new FusionCharts("../FusionCharts/Column2D.swf", "myChartId", "600", "500",0 ,0, "FFFFFF","noscale","EN");   
        4myChart.setDataURL("data.xml");   
        5myChart.render("chartdiv");   
        </script>  
    </body>  
</html>
1

这里引入需要的js资源文件

2

图形将出现这个div里,如果div中有其它内容,到时这里的内容将被图形替代。

3

生成一个Column2D图表。其中第一个参数为生成图表要使用的swf文件(它决定了显示什么图表);第二个参数为图表的id(如果一个页面有多个FusionCharts图表,这个必须唯一);第三个参数为图表的宽度;第四个参数为图表的高度;第五个参数为调试模式;第六个参数为是否使用JavaScript注册;第七个参数为背景颜色;第八个参数为刻度模式;第九个参数为语言。

4

这里指定图表的数据来源。

5

这里指定将图表显示在id="chartdiv"的div中。

图表使用data.xml来指定数据来源,它的内容如下

<graph baseFont='宋体' baseFontSize='12'  xAxisName='单位' yAxisName='用电量(万度)'  formatNumberScale='0' bgcolor="FFFFFF">
    <set name='物价局' value='322' color='AFD8F8' />
    <set name='交通局' value='950' color='CC3333' />
    <set name='市容局' value='957' color='F6BD0F' />
</graph>

可以看到代码其实比较简单,FusionCharts所有构件显示的原理基本都是用一个swf来显示一个XML文件的数据成一个Flash图表。

28.3.1.1.2. 第二步:找出可变部分与不变部分

这里的可变与不可变是相对于一个构件来说的,我们考虑一下如果我们要定义另外一个FusionCharts Column2D图表,那些代码中有哪些是可变的?哪些又是不变的?我们先将可变的部分用变量表示出来:

<html>  
    <head>  
        <script language="JavaScript" src="fusionCharts/FusionCharts.js"></script>  
    </head>    
    <body>  
        <div id="{container_div_id}"></div>  
        <script type="text/JavaScript">  
        var {jsvar} = new FusionCharts("../FusionCharts/Column2D.swf", "{chart_id}", "{width}", "{height}",
              {debugModel}, {registerWithJS}, "{bgcolor}", "{scaleMode}", "{lang}");   
        {jsvar}.setDataURL("{dataURL}");
        {jsvar}.render("{container_div_id}");
        </script>  
    </body>  
</html>

注意:对于一个FusionCharts Column2D图表来说,"../FusionCharts/Column2D.swf"是不变的。如果是另一个FusionCharts图表,则它可变。我们要开发一个FusionCharts Column2D构件,它属于不可变部分。

28.3.1.1.3. 第三步:决定需要的构件类应该有哪些属性

通过上面的第二步,我们基本可以确定,我们定义一个FusionCharts Column2D构件,大约需要有11个属性:container_div_id、jsvar、chart_id、width、height、debugModel、registerWithJS、bgcolor、scaleMode、lang、dataURL。

再仔细考虑一下,container_div_id也略显多余,它无非就是图表的容器,我们有了构件的chart_id,我们可以根据chart_id来决定容器的id,这样用户可以少设置一个属性。可以约定一下:{container_div_id}={chart_id}+"_container",这样用户只需要一个chart_id就行了。另外还需要提供一些属性来设置那个div的样式(比如加边框、水平对齐等),当然也可以使用CSS来设置,所以再加两个属性style和styleClass。另外,所有构件除了有id属性外,还应该有binding、renderered属性。

通过这些考虑,我们已经可以写出构件的Base类了。代码如下:

public 1abstract class UIColumn2DChartBase 2extends UIComponentBase {
    3protected String jsvar;
    4protected int width=500;//默认宽度500
    protected int height=400;//默认高度400
    protected boolean debug = false;//默认为false
    protected boolean registerWithJS = false;//默认为false
    protected String bgcolor;
    protected String scaleMode;
    protected String lang;

    protected String style;
    protected String styleClass;  
    5protected String dataURL;
}
1

这个类是构件Base类(Base类的作用上面已经讲过了),真正的构件类继承自此类,不允许用户用new UIColumn2DChartBase()来创建此类的实例,所以此Base类是抽象的。

2

由于构件类要继承Base类,而所有构件类要直接或间接继承自UIComponent类,所以此处Base类继自UIComponentBase(UIComponentBase继承自UIComponent类,并且提供了一些id、binding、renderered等属性)。

3

因为此类继承自UIComponentBase,所以不必再定义id、renderered、binding等属性。此类中所有属性定义成protected的目的是:让Studio自动生成构件类,并将Base类中属性的getter/setter等方法生成到构件类中(要在构件类中使用this.jsvar等来使用Base类的jsvar等属性,所以要设置成protected)。

4

可以设置某些属性的默认值,如果用户最终在xhtml页面中使用这个构件时不设置width属性,width会会默认成500。

5

指定一个xml文件,FusionCharts构件将会根据这个xml文件里的内容来决定要显示的数据。

28.3.1.1.4. 第四步:生成构件类,看看有没有可改进的地方

其实构件类中有很多内容的(getter/setter、saveState/resoreState等),要手工编写比较复杂。OperaMasks Studio为这个需求提供了支持,用户只需要保存Base类,这个类的所有内容会自动生成,用户不需要修改任何代码。这也是我们为什么是将构件类分为Base类和构件类的原因,Base类中所有代码都是无法自动生成的,构件类中所有代码都是可以自动生成的。

生成的构件类也完全满足我们的要求,接下来可以开始写渲染器类了。

28.3.1.1.5. 第五步:为渲染器类准备资源

把所有的资源文件(如js文件、CSS文件、图片文件、音乐文件等)放到META-INF\resource目录中(可以在里面建立子目录)。这个构件中我们使用了2个资源Column2D.swf和FusionCharts.js,将这两个文件放到META-INF\resource里面的fusionchart目录中。其中JS文件需要起别名(CSS和JS需要,其它文件不需要),在“构件工程资源管理器”中为此资源起一个别名(假设叫AJavaer.FusionChart)。

构件工程资源管理器中添加资源文件

图 28.3. 构件工程资源管理器中添加资源文件


28.3.1.1.6. 第六步:确定渲染器需要覆写哪些方法

我们在前面已经讲了一个AJAX渲染器必须直接或间接继承自AjaxRendererBase类,而且里面有许多可以覆写的方法,现在要做的是确定应该在哪里方法里干什么。

再回头看一下第一步中的那个HTML代码:

<html>  
    <head>  
        <script language="JavaScript" src="fusionCharts/FusionCharts.js"></script>  
    </head>    
    <body>  
        <div id="chartdiv"></div>  
        <script type="text/JavaScript">  
        var myChart = new FusionCharts("../FusionCharts/Column2D.swf", "myChartId", "600", "500",0 ,0, "FFFFFF","noscale","EN");   
        myChart.setDataURL("data.xml");   
        myChart.render("chartdiv");   
        </script>  
    </body>  
</html>

首先需要关注的是在head中引入了一个FusionCharts.js文件,通过查看AjaxRendererBase类各方法的说明,发现应该使用public String[] getDependedJSPackages(FacesContext context, UIComponent component);这个方法。

@Override
public String[] getDependedJSPackages(FacesContext context, UIComponent component) {
    return new String[]{"AJavaer.FusionChart"};
}

接下来要关注的是那个div,这是HTML代码,我们应该在encodeHtmlBegin()、encodeHtmlChildren()、encodeHtmlEnd()这三个方法中的某一个中写,具体哪一个要看它是显示在子构件的HTML代码的前面还是后面。我们要开发的Column2D构件是不要放子构件的,所以它放在encodeHtmlBegin()或encodeHtmlEnd()中都可以,但是根据FusionCharts官方说法,这里可以放一些说明文字,比如“图形将出现这个DIV里,到时这里的字将被图形替代。”。

我们应该将放<div id="chartdiv">放在encodeHtmlBegin()中。将</div>放在encodeHtmlEnd()中。

@Override
public void encodeHtmlBegin(FacesContext context, UIComponent component) throws IOException {
    //这里输出一个div出来,它的id=构件id+"_container";
    //如果构件设置了style或styleClass属性,也应该渲染到这个div上
}
@Override
public void encodeHtmlEnd(FacesContext context, UIComponent component) throws IOException {
    //这里输出一个"</div>"出来;
}

接下来要关注的就是一段JavaScript代码。那段JavaScript中有一个jsvar变量myChart,一般jsvar在初始化时声明,所以我们可以在encodeInitScriptBegin中声明这个jsvar。

@Override
public void encodeInitScriptBegin(FacesContext context, ResourceManager rm,
        UIComponent component) throws IOException {
    //声明一个jsvar
}

根据AjaxRendererBase各方法的说明,其它的js代码应该在encodeResourceBegin()、encodeResourceChildren()、encodeResourceEnd()的某个方法中渲染。放在encodeResourceChildren()中显然是不合适的,因为这个方法是构件对子构件的渲染。本示例中放在encodeResourceBegin()和encodeResourceEnd()中都是可以的,我们放在encodeResourceBegin()中算了。

@Override
public void encodeResourceBegin(FacesContext context, ResourceManager rm,
        UIComponent component) throws IOException {
    //渲染JavaScript代码
    //因为这个渲染是许多FusionCharts构件公共的,所以这里可能会根据构件Base类中
    //的getType()方法的返回值不同,渲染不同的swf文件名
}
28.3.1.1.7. 第七步:实现渲染器中各要覆盖方法

根据上面第六步的分析,我们的渲染器Column2DAjaxRenderer应该继承自AjaxRendererBase类,并覆盖其中的getDependedJSPackages()、encodeHtmlBegin()、encodeHtmlEnd()、encodeInitScriptBegin()、encodeResourceBegin()这五个方法。这个渲染器中的代码基本都要用户自己编写。具体代码请看下一节内容。

28.3.1.1.8. 第八步:配置各个配置文件

至此构件基本开发完成了,还要进行一些配置,配置命名空间、tag名、tag名与构件类对应信息、构件与渲染器对应信息等。由于这些功能全部能由OperaMasks Studio完成,就不详细讲了,具体可以看OperaMasks Studio生成的文件的内容。

28.3.1.1.9. 第九步:打包成jar文件

构件全部工作完成,只需要打包成jar文件,就像使用OperaMasks一样使用这个jar文件就可以了,这个工作由OperaMasks Studio完成,将在下一节讲。

28.3.1.2. 使用OperaMasks来开发FusionCharts Column2D构件

上面一节讲得比较复杂,为的是让大家掌握这个步骤,掌握其中的原理。现在我们来看一下如何使用OperaMasks Studio来开发这个构件。

28.3.1.2.1. 第一步:新建一个构件工程

在JavaEE资源管理器中空白地方右键--“新建(W)”--“Apusic工程”。

会弹出如下窗口,选择“构件工程”,然后点“下一步(N)”。

在弹出的窗口中输入“工程名称”,“OperaMasks SDK版本”选择“OperaMasks 3.0 Libraries”,如果需要使用js或css资源文件,则要选中“创建Resource-dependence.xml”前面的checkbox,如果不需要则可以去掉选择(个人经验是先选中,如果构件开发完了后还没用到js或css资源文件时再删除生成的resource-dependences.xml文件即可。因为空上文件里有一些默认代码,如果这里没有选择的话,当构件开发到一半时想使用js或css资源,需要新建这个文件,里面的内容不知道怎么写)。

然后点“下一步(N)”后会出来如下界面,输入命名空间相关的一些信息。

然后点“完成(F)”,就会自动一个构件工程,里面默认生成了一些文件。

还需要把javaee.jar包配置到构建路径中,在工程上右键--“构建路径(B)”--“配置构建路径(C)”。

在弹出的窗口中,切换到“库(L)”标签,点击“添加外部jar(X)”。在弹出的窗口中选择javaee.jar和jaxb-api.jar(要使用这个jar包中的API将Chart对象转换为xml片段)所在的路径(Apusic Server根目录的common文件夹中有这2个文件),然后“打开(O)”,效果如下图。然后“确定”,构件工程就建好了。

28.3.1.2.2. 第二步:添加一个构件

双击打开构件工程中“http://ajavaer.org/fusionchart”目录下的“fc.taglib.xml”文件,内容如下。里面默认配置了一个newTag1构件,选择这个构件删除它。

点击“添加”按钮添加一下新的构件,输入如下信息,然后保存。

点击“发布”按钮来生成构件Base类、构件类、渲染器类及一些配置信息。弹出下图的窗口时点击“是(Y)”。

发布后会生成以下3个文件。

28.3.1.2.3. 第三步:写构件Base类,生成构件类

双击打开构件Base类UIColumn2DBase,里面的内容如下:

package org.ajavaer.fusionchart.component;

import org.operamasks.faces.tools.annotation.ComponentMeta;

@ComponentMeta(tagName="column2D", family="org.ajavaer.fusionchart", rendererType="org.ajavaer.fusionchart.component.Column2D")
public abstract class UIColumn2DBase extends UIComponentBase {
    
}

根据前面的说明,应该在这个类中声明构件类应该有的属性,修改后的内容应该如下:

package org.ajavaer.fusionchart.component;

import javax.faces.component.UIComponentBase;

import org.operamasks.faces.tools.annotation.ComponentMeta;

@ComponentMeta(tagName = "column2D", family = "org.ajavaer.fusionchart", rendererType = "org.ajavaer.fusionchart.component.UIColumn2D")
public abstract class UIColumn2DBase extends UIComponentBase {
    protected String jsvar;
    protected int width = 500;// 默认宽度500
    protected int height = 400;// 默认高度400
    protected boolean debug = false;// 默认为false
    protected boolean registerWithJS = false;// 默认为false
    protected String bgcolor;
    protected String scaleMode;
    protected String lang;

    protected String style;
    protected String styleClass;
    protected String dataURL;
}

修改完成保存该类,Operamasks Studio会自动生成对应的构件类UIColumn2D.java,其代码如下:

/**
 * UIColumn2D.java
 * DO NOT EDIT THIS FILE
 * This file was automatically generated by org.operamasks.faces.tools.apt.ComponentAnnotationProcessorFactory
 * at Mon Apr 12 13:53:48 CST 2010
 */

package org.ajavaer.fusionchart.component;

import org.ajavaer.fusionchart.component.UIColumn2DBase;
import org.operamasks.faces.component.GenComponentUtils;
import java.lang.reflect.Method;
import java.util.Map;

@javax.annotation.Generated(value="org.operamasks.faces.tools.apt.ComponentAnnotationProcessorFactory", date="2010-04-12T13:53:48Z")
public class UIColumn2D extends UIColumn2DBase {
    public static final String COMPONENT_FAMILY = "org.ajavaer.fusionchart";
    public static final String COMPONENT_TYPE = "org.ajavaer.fusionchart.component.UIColumn2D";
    private final static Map<String,Method> operationMethodsMap = GenComponentUtils.createOperationMethodsMap();

    public UIColumn2D() {
        super.setRendererType("org.ajavaer.fusionchart.component.UIColumn2D");
    }

    protected Map<String,Method> getOperationMethodsMap() {
        return operationMethodsMap;
    }

    @Override
    public String getFamily() {
        return COMPONENT_FAMILY;
    }

    public java.lang.String getJsvar() {
        if (this.jsvar != null) {
            return this.jsvar;
        }
        Object exprValue = GenComponentUtils.getValueFromExpression(this, "jsvar");
        if(exprValue != null){
            return (java.lang.String)exprValue;
        }
        return null;
    }

    public void setJsvar(java.lang.String value) {
        this.jsvar = value;
    }

    private boolean width_set;

    public boolean isWidthSet(){
        return width_set;
    }

    public int getWidth() {
        if (this.width_set) {
            return this.width;
        }
        Object exprValue = GenComponentUtils.getValueFromExpression(this, "width");
        if(exprValue!=null){
            return (Integer)exprValue;
        }
        return this.width;
    }

    public void setWidth(int value) {
        this.width = value;
        this.width_set = true;
    }

    private boolean height_set;

    public boolean isHeightSet(){
        return height_set;
    }

    public int getHeight() {
        if (this.height_set) {
            return this.height;
        }
        Object exprValue = GenComponentUtils.getValueFromExpression(this, "height");
        if(exprValue!=null){
            return (Integer)exprValue;
        }
        return this.height;
    }

    public void setHeight(int value) {
        this.height = value;
        this.height_set = true;
    }

    private boolean debug_set;

    public boolean isDebugSet(){
        return debug_set;
    }

    public boolean getDebug() {
        if (this.debug_set) {
            return this.debug;
        }
        Object exprValue = GenComponentUtils.getValueFromExpression(this, "debug");
        if(exprValue!=null){
            return (Boolean)exprValue;
        }
        return this.debug;
    }

    public boolean isDebug() {
        return getDebug();
    }

    public void setDebug(boolean value) {
        this.debug = value;
        this.debug_set = true;
    }

    private boolean registerWithJS_set;

    public boolean isRegisterWithJSSet(){
        return registerWithJS_set;
    }

    public boolean getRegisterWithJS() {
        if (this.registerWithJS_set) {
            return this.registerWithJS;
        }
        Object exprValue = GenComponentUtils.getValueFromExpression(this, "registerWithJS");
        if(exprValue!=null){
            return (Boolean)exprValue;
        }
        return this.registerWithJS;
    }

    public boolean isRegisterWithJS() {
        return getRegisterWithJS();
    }

    public void setRegisterWithJS(boolean value) {
        this.registerWithJS = value;
        this.registerWithJS_set = true;
    }

    public java.lang.String getBgcolor() {
        if (this.bgcolor != null) {
            return this.bgcolor;
        }
        Object exprValue = GenComponentUtils.getValueFromExpression(this, "bgcolor");
        if(exprValue != null){
            return (java.lang.String)exprValue;
        }
        return null;
    }

    public void setBgcolor(java.lang.String value) {
        this.bgcolor = value;
    }

    public java.lang.String getScaleMode() {
        if (this.scaleMode != null) {
            return this.scaleMode;
        }
        Object exprValue = GenComponentUtils.getValueFromExpression(this, "scaleMode");
        if(exprValue != null){
            return (java.lang.String)exprValue;
        }
        return null;
    }

    public void setScaleMode(java.lang.String value) {
        this.scaleMode = value;
    }

    public java.lang.String getLang() {
        if (this.lang != null) {
            return this.lang;
        }
        Object exprValue = GenComponentUtils.getValueFromExpression(this, "lang");
        if(exprValue != null){
            return (java.lang.String)exprValue;
        }
        return null;
    }

    public void setLang(java.lang.String value) {
        this.lang = value;
    }

    public java.lang.String getStyle() {
        if (this.style != null) {
            return this.style;
        }
        Object exprValue = GenComponentUtils.getValueFromExpression(this, "style");
        if(exprValue != null){
            return (java.lang.String)exprValue;
        }
        return null;
    }

    public void setStyle(java.lang.String value) {
        this.style = value;
    }

    public java.lang.String getStyleClass() {
        if (this.styleClass != null) {
            return this.styleClass;
        }
        Object exprValue = GenComponentUtils.getValueFromExpression(this, "styleClass");
        if(exprValue != null){
            return (java.lang.String)exprValue;
        }
        return null;
    }

    public void setStyleClass(java.lang.String value) {
        this.styleClass = value;
    }

    public java.lang.String getDataURL() {
        if (this.dataURL != null) {
            return this.dataURL;
        }
        Object exprValue = GenComponentUtils.getValueFromExpression(this, "dataURL");
        if(exprValue != null){
            return (java.lang.String)exprValue;
        }
        return null;
    }

    public void setDataURL(java.lang.String value) {
        this.dataURL = value;
    }

    @Override
    public Object saveState(javax.faces.context.FacesContext context) {
        return new Object[] {
                super.saveState(context),
                this.jsvar,
                this.width,
                this.width_set,
                this.height,
                this.height_set,
                this.debug,
                this.debug_set,
                this.registerWithJS,
                this.registerWithJS_set,
                this.bgcolor,
                this.scaleMode,
                this.lang,
                this.style,
                this.styleClass,
                this.dataURL
        };
    }

    @Override
    public void restoreState(javax.faces.context.FacesContext context, Object state) {
        Object[] values = (Object[])state;
        super.restoreState(context, values[0]);
        this.jsvar = (java.lang.String)values[1];
        this.width = (Integer)values[2];
        this.width_set = (Boolean)values[3];
        this.height = (Integer)values[4];
        this.height_set = (Boolean)values[5];
        this.debug = (Boolean)values[6];
        this.debug_set = (Boolean)values[7];
        this.registerWithJS = (Boolean)values[8];
        this.registerWithJS_set = (Boolean)values[9];
        this.bgcolor = (java.lang.String)values[10];
        this.scaleMode = (java.lang.String)values[11];
        this.lang = (java.lang.String)values[12];
        this.style = (java.lang.String)values[13];
        this.styleClass = (java.lang.String)values[14];
        this.dataURL = (java.lang.String)values[15];
    }

}

里面的代码不用做任何改动。

28.3.1.2.4. 第四步:准备资源

上面已经说了,这个构件我们需要两个资源Column2D.swf和FusionCharts.js,我们把它们放在META-INF\resource中任意位置,这里我放在了fusionchart目录中。

前面说过了对于使用的js和css资源,我们要给它起个别名,接下来为FusionCharts.js起个别名。双击“资源描述符”(也可以双击“META-INF\resource\resource-dependences.xml”),出来如下界面。

选择上图中“JS”,然后点“添加”,然后输入如下“资源名”,即我们要的别名。“资源文件”这里可以点击“浏览...”选择我们刚才放到META-INF\resource\fusionchart中的FusionCharts.js文件,然后“确定”,并按CTRL+S保存我们的配置。

28.3.1.2.5. 第五步:写渲染器类

上面已经说过了,这个构件需要一个渲染器类,并且我们也分析了应该覆盖AjaxRendererBase的哪些方法,现在我们来覆盖它们。双击打开org.ajavaer.fusionchart.component包中Column2DAjaxRenderer.java文件,将代码改为这样:

package org.ajavaer.fusionchart.component;
import java.io.IOException;
import java.util.Formatter;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;

import org.operamasks.faces.developer.util.FacesUtils;
import org.operamasks.faces.render.common.AjaxRendererBase;
import org.operamasks.faces.render.html.HtmlResponseWriter;
import org.operamasks.faces.render.resource.ComponentResource;
import org.operamasks.faces.render.resource.ResourceManager;
import org.operamasks.faces.util.HtmlEncoder;

public class Column2DAjaxRenderer extends AjaxRendererBase {
  
    @Override
    public String[] getDependedJSPackages(FacesContext context, UIComponent component) {
        // 我们需要使用别名为"AJavaer.FusionChart"的FusionCharts.js文件
        return new String[] {"AJavaer.FusionChart"};
    }
    
    @Override
    public void encodeHtmlBegin(FacesContext context, UIComponent component)
            throws IOException {
        UIColumn2D chartComp = (UIColumn2D) component;
        HtmlResponseWriter responseWriter = (HtmlResponseWriter) context.getResponseWriter();
        //得到构件的客户端id
        String clientId = chartComp.getClientId(context);
        responseWriter.startElement("div", chartComp);//渲染一个div
        responseWriter.writeAttribute("id", clientId, null);//为div添加id属性
        //将构件的style属性写入div的style属性中
        String style=chartComp.getStyle();
        if (style != null && style.trim().length() > 0) {
            responseWriter.writeAttribute("style", style, null);//为div添加style属性
        }
        //将构件的styleClass属性写入div的class属性中
        String styleClass=chartComp.getStyleClass();
        if (styleClass != null && styleClass.trim().length() > 0) {
            responseWriter.writeAttribute("class", styleClass, null);
        }
    }

    @Override
    public void encodeHtmlEnd(FacesContext context, UIComponent component)
            throws IOException {
        //输出</div>
        HtmlResponseWriter responseWriter = (HtmlResponseWriter) context.getResponseWriter();
        responseWriter.endElement("div");
    }

    @Override
    public void encodeInitScriptBegin(FacesContext context, ResourceManager rm,
            UIComponent component) throws IOException {
        //声明一个jsvar变量
        ComponentResource resource = ComponentResource.getResourceInstance(rm);
        resource.allocVariable(component);
    }

    @Override
    public void encodeResourceBegin(FacesContext context, ResourceManager rm,
            UIComponent component) throws IOException {
        UIColumn2D chartComp = (UIColumn2D) component;
        String jsvar = FacesUtils.getJsvar(context, chartComp);
        StringBuffer buf = new StringBuffer();
        Formatter fmt = new Formatter(buf);
        //取出各个属性的值
        String swfUrl = rm.getResourceURL("/fusionchart/Column2D.swf");
        String clientId = chartComp.getClientId(context);
        String width = String.valueOf(chartComp.getWidth());
        String height = String.valueOf(chartComp.getHeight());
        String debug = chartComp.isDebug() ? "1" : "0";
        String registerWithJS = chartComp.isRegisterWithJS() ? "1" : "0";
        String bgColor = chartComp.getBgcolor();
        if(bgColor==null)
            bgColor="null";
        String scaleMode = chartComp.getScaleMode();
        if(scaleMode==null)
            scaleMode="null";
        String lang = chartComp.getLang();
        if(lang==null)
            lang="null";
        //渲染js代码
        fmt.format("%s=new FusionCharts(%s,%s,%s,%s,%s,%s,%s,%s,%s);\n", 
                jsvar,
                HtmlEncoder.enquote(swfUrl), 
                HtmlEncoder.enquote(chartComp.getId()),
                HtmlEncoder.enquote(width), 
                HtmlEncoder.enquote(height),
                HtmlEncoder.enquote(debug),
                HtmlEncoder.enquote(registerWithJS), 
                HtmlEncoder.enquote(bgColor),
                HtmlEncoder.enquote(scaleMode),
                HtmlEncoder.enquote(lang));
        String dataUrl = chartComp.getDataURL();
        fmt.format("%s.setDataURL(%s);\n",jsvar, HtmlEncoder.enquote(dataUrl));
        fmt.format("%s.render(%s);\n",jsvar, HtmlEncoder.enquote(clientId));
        //将拼凑的js代码输出到响应中
        ComponentResource resource = ComponentResource.getResourceInstance(rm);
        resource.addInitScript(fmt.toString());
    }
  
}
28.3.1.2.6. 第六步:打包成jar文件

到这里构件工程已经全部完成,现在只需要打包成jar文件,自己在别的工程里使用或者交给别人使用就可以了。

在工程上右键--“导出(O)...”,在弹出的窗口中选择“构件jar文件”,如下图。

点击“下一步(N)>”后出现如下界面,点击“浏览...”选择一个保存目录和文件名后,点击“完成(F)”,jar包就好了。

28.3.1.3. 使用自定义的构件

构件已经开发好了,我们怎么使用或测试它呢?跟使用其它jar包一样。首先需要一个Apusic标准工程,并且有web模块(这些内容请参考第 11.2 节 “建立一个新的Apusic标准工程”)。

只需要将构件工程导出的jar包复制到测试工程的web模块的WebContent\WEB-INF\lib中即可。然后就可以开始写xhtml页面和LiteBean了。

测试页面Column2D.xhtml代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<f:view xmlns:f="http://java.sun.com/jsf/core"
  xmlns="http://www.w3.org/1999/xhtml"
  xmlns:w="http://www.apusic.com/jsf/widget"
  xmlns:fc="http://org.ajavaer.fusionchart" 
  renderKitId="AJAX">
  <w:page title="FusionCharts Demos">
    <fc:column2D id="chart1" width="500" height="400"
      dataURL="data.xml" />
  </w:page>
</f:view>

不需要后台LiteBean,其中data.xml与前文提到的一样。运行后的效果与我们需要的完全一样。

至此这个构件基本满足了我们的要求。下一节我们将对这个构件进行改进。

28.3.1.4. 对Column2D构件进行改进

上文中开发的fc:column2D构件基本可以使用了,但是这种通过一个xml文件来为图表构件指定数据在多数情况下并不好用,一般用户需要的是可以显示动态数据(比如数据库中的数据)的图表构件,虽然我们可以通过Java代码将动态数据生成一个xml文件,然后让图表构件使用那个xml文件来达到要求。但是这种写法十分别扭,而且生成的xml文件名难以确定,而且生成的文件如果长时间存在于服务器端,将极大地占用硬盘资源。我们希望我们的图表构件能够显示内存xml数据,而且用户使用这个图表构件时根本不应该感觉到位xml的存在。这种功能能不能实现呢?

其实在OperaMasks SDK中已经有很多这样的构件,比如w:tree、w:dataGrid、w:editDataGrid、w:dataView等。这种构件在使用时一般只要给它定义一个dataProvider,并在dataProvider中返回一个对象或对象集合(数组或List)就可以了。而且这些dataProvider取数的构件还有一个共同的特点:它们都是采用Delegate二次取数的方式进行构件数据和渲染的。下面我们将对上一节中开发的fc:column2D构件进行修改,让它使用dataProvider提供数据,并用Delegate进行二次取数渲染。

在正式修改之前,我们先定义一个dataProvider————FusionChartDataProvider,该类是一个接口,里面只有一个getChart()方法,该方法返回一个Chart类型的对象,该对象代表FusionChart所需要的xml数据。代码如下:

package org.ajavaer.fusionchart.component;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;

import org.ajavaer.fusionchart.modal.Chart;

public interface FusionChartDataProvider {
  public Chart getChart(FacesContext context, UIComponent component);
}

其中org.ajavaer.fusionchart.modal.Chart类是根据FusionCharts官方提供的data.xml的xsd文件生成的Java POJO类,具体代码就不给了,可以下载本章《构件开发》示例工程,里面有相应的源代码。

接下来修改构件Base类,去掉dataURL属性,并添加dataProvider属性。修改后的代码如下:

package org.ajavaer.fusionchart.component;
import javax.faces.component.UIComponentBase;

import org.operamasks.faces.tools.annotation.ComponentMeta;

@ComponentMeta(tagName="column2D", family="org.ajavaer.fusionchart", rendererType="org.ajavaer.fusionchart.component.Column2D")
public abstract class UIColumn2DBase extends UIComponentBase {
    protected String jsvar;
    protected int width = 500;// 默认宽度500
    protected int height = 400;// 默认高度400
    protected boolean debug = false;// 默认为false
    protected boolean registerWithJS = false;// 默认为false
    protected String bgcolor;
    protected String scaleMode;
    protected String lang;
    protected String style;
    protected String styleClass;
    1private FusionChartDataProvider dataProvider;
    
    2public org.ajavaer.fusionchart.component.FusionChartDataProvider getDataProvider() {
        if (this.dataProvider != null) {
            return this.dataProvider;
        }
        Object exprValue = GenComponentUtils.getValueFromExpression(this,
                "dataProvider");
        if (exprValue != null) {
            return (org.ajavaer.fusionchart.component.FusionChartDataProvider) exprValue;
        }
        return null;
    }

    public void setDataProvider(org.ajavaer.fusionchart.component.FusionChartDataProvider value) {
        this.dataProvider = value;
    }
}
1

这里将原来的dataURL属性改为了dataProvider。注意这个属性是private的、而其它属性是protected的。

2

这里给出了private的dataProvider的getter/setter方法。由于在构件保存状态时,dataProvider没有必要保存在视图树中,所以这个属性不需要出现在构件类的saveState()/restoreState()方法中。因此这个属性特殊处理,前面已经说过不想让OperaMasks Studio生成的特殊方法可以定义在构件Base类中。

修改完后保存,OperaMasks Studio会自动生成对应的构件类UIColumn2D,不需要修改。

接下来修改渲染器类,将原来根据构件的dataURL属性渲染js部分代码改为使用二次取数方式,修改后的代码如下:

package org.ajavaer.fusionchart.component;
import java.io.IOException;
import java.util.Formatter;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.servlet.http.HttpServletRequest;

import org.operamasks.faces.developer.util.FacesUtils;
import org.operamasks.faces.render.common.AjaxRendererBase;
import org.operamasks.faces.render.delegate.ViewDelegate;
import org.operamasks.faces.render.html.HtmlResponseWriter;
import org.operamasks.faces.render.resource.ComponentResource;
import org.operamasks.faces.render.resource.ResourceManager;
import org.operamasks.faces.util.HtmlEncoder;

public class Column2DAjaxRenderer extends AjaxRendererBase {
  
    1public static final String DELEGATE_PARAM = "__fusionchart__";

    @Override
    public String[] getDependedJSPackages(FacesContext context, UIComponent component) {
        return new String[]{"AJavaer.FusionChart"};
    }
    
    @Override
    public void encodeHtmlBegin(FacesContext context, UIComponent component)
            throws IOException {
        UIColumn2D chartComp = (UIColumn2D) component;
        HtmlResponseWriter responseWriter = (HtmlResponseWriter) context.getResponseWriter();
        //得到构件的客户端id
        String clientId = chartComp.getClientId(context);
        responseWriter.startElement("div", chartComp);//渲染一个div
        responseWriter.writeAttribute("id", clientId, null);//为div添加id属性
        //将构件的style属性写入div的style属性中
        String style=chartComp.getStyle();
        if (style != null && style.trim().length() > 0) {
            responseWriter.writeAttribute("style", style, null);//为div添加style属性
        }
        //将构件的styleClass属性写入div的class属性中
        String styleClass=chartComp.getStyleClass();
        if (styleClass != null && styleClass.trim().length() > 0) {
            responseWriter.writeAttribute("class", styleClass, null);
        }
    }

    @Override
    public void encodeHtmlEnd(FacesContext context, UIComponent component)
            throws IOException {
        //输出</div>
        HtmlResponseWriter responseWriter = (HtmlResponseWriter) context.getResponseWriter();
        responseWriter.endElement("div");
    }

    @Override
    public void encodeInitScriptBegin(FacesContext context, ResourceManager rm,
            UIComponent component) throws IOException {
        //声明一个jsvar变量
        ComponentResource resource = ComponentResource.getResourceInstance(rm);
        resource.allocVariable(component);
    }

    @Override
    public void encodeResourceBegin(FacesContext context, ResourceManager rm,
            UIComponent component) throws IOException {
        UIColumn2D chartComp = (UIColumn2D) component;
        String jsvar = FacesUtils.getJsvar(context, chartComp);
        StringBuffer buf = new StringBuffer();
        Formatter fmt = new Formatter(buf);
        //取出各个属性的值
        String swfUrl = rm.getResourceURL("/fusionchart/Column2D.swf");
        String clientId = chartComp.getClientId(context);
        String width = String.valueOf(chartComp.getWidth());
        String height = String.valueOf(chartComp.getHeight());
        String debug = chartComp.isDebug() ? "1" : "0";
        String registerWithJS = chartComp.isRegisterWithJS() ? "1" : "0";
        String bgColor = chartComp.getBgcolor();
        String scaleMode = chartComp.getScaleMode();
        String lang = chartComp.getLang();
        //渲染js代码
        fmt.format("%s=new FusionCharts(%s,%s,%s,%s,%s,%s,%s,%s,%s);\n", 
                jsvar,
                HtmlEncoder.enquote(swfUrl), 
                HtmlEncoder.enquote(chartComp.getId()),
                HtmlEncoder.enquote(width), 
                HtmlEncoder.enquote(height),
                HtmlEncoder.enquote(debug),
                HtmlEncoder.enquote(registerWithJS), 
                bgColor == null ? "null": HtmlEncoder.enquote(bgColor),
                scaleMode == null ? "null" : HtmlEncoder.enquote(scaleMode),
                lang == null ? "null" : HtmlEncoder.enquote(lang));
        
        HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
        //这个dataUrl中包含ViewDelegate.VIEW_DELEGATE_REQUEST说明是二次取数
        //这个dataUrl中还传递了一个参数DELEGATE_PARAM,值为构件的id,二次取数时会使用这个参数
        2String dataUrl = request.getRequestURL() + "?"+DELEGATE_PARAM+"=" + chartComp.getId() + 
                "%26"+ ViewDelegate.VIEW_DELEGATE_REQUEST + "=%26" + FacesUtils.STREAM_RESPONSE + "="; 
        fmt.format("%s.setDataURL(%s);\n",jsvar, HtmlEncoder.enquote(dataUrl));
        fmt.format("%s.render(%s);\n",jsvar, HtmlEncoder.enquote(clientId));
        //将拼凑的js代码输出到响应中
        ComponentResource resource = ComponentResource.getResourceInstance(rm);
        resource.addInitScript(fmt.toString());
    }
  
}

可以看到,dataUrl发生了变化,原来从构件的dataURL中读取xml文件URL的代码改为Delegate方式。

再看一下Delegate的代码:

package org.ajavaer.fusionchart.component;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.Map;

import javax.faces.application.ViewHandler;
import javax.faces.component.UIComponent;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.servlet.http.HttpServletResponse;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.PropertyException;

import org.ajavaer.fusionchart.component.modal.Chart;
import org.ajavaer.fusionchart.component.modal.ChartObjectFactory;
import org.operamasks.faces.application.ViewBuilder;
import org.operamasks.faces.component.ajax.AjaxUpdater;
import org.operamasks.faces.developer.util.FacesUtils;
import org.operamasks.faces.render.ajax.AjaxUpdaterRenderer;
import org.operamasks.faces.render.delegate.ViewDelegate;

public class FusionChartDelegate implements ViewDelegate {

  public void delegate(FacesContext context) throws IOException {
    //从二次取数url中取出DELEGATE_PARAM参数,参数结果是构件的id
    //具体见AjaxFusionChartRenderer.encodeResourceBegin()方法中代码
    ExternalContext ectx = context.getExternalContext();
    Map<String, String> paramMap = ectx.getRequestParameterMap();
    String fusionChartId = paramMap.get(Column2DAjaxRenderer.DELEGATE_PARAM);
    //如果没有找到参数或参数为null,不知道要对哪个构件进行代理,则返回
    if (fusionChartId == null) {
      return;
    }
    
    UIComponent component = null;
    //找到参数表示的构件(UIComponent对象)
    component = FacesUtils.getForComponent(context, fusionChartId, context.getViewRoot());
    //如果没有找到,看看是不是在哪个ajax:updater引入的页面里面
    if (component == null) {
      try {
        component = findFromUpdater(context, fusionChartId,UIColumn2D.class);
      } catch (Exception ex) {
        // ignore;
      }
    }
    //如果找到构件,而且是FusionChart构件,则开始取数
    if (component != null && (component instanceof UIColumn2D)) {
        UIColumn2D chartComp = (UIColumn2D) component;
      //得到用户定义的dataProvider
      FusionChartDataProvider dataProvider = chartComp.getDataProvider();
      if (dataProvider != null) {
          //从dataProvider中取数
        Chart chart = dataProvider.getChart(context, component);
        //设置响应头
        HttpServletResponse response = (HttpServletResponse) context.getExternalContext().getResponse();
        response.setHeader("Cache-Control", "no-cache");
        response.setHeader("Pragma", "no-cache");
        response.setHeader("Connection", "close");
        //设置响应类型为text/xml、编码为UTF-8
        response.setContentType( "text/xml; charset=UTF-8" );
        
        OutputStream out = response.getOutputStream();
        out.write( new byte[]{(byte)0xEF, (byte)0xBB, (byte)0xBF} );
        try {
          JAXBContext jc = JAXBContext.newInstance(ChartObjectFactory.class);
          Marshaller m = jc.createMarshaller();
          m.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
          m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
          //将dataProvider中取出的数据转换为xml并输出
          m.marshal(chart, out);
          out.flush();
          //二次请求的响应完成
          context.responseComplete();
        } catch (PropertyException e) {
          e.printStackTrace();
        } catch (JAXBException e) {
          e.printStackTrace();
        }
      }
    }

  }
  
  /**
   * 从该页面所有的ajax:updater引入的页面中查找一个构件类型为 cls,且id为fusionChartId的构件
   */
  public UIComponent findFromUpdater(FacesContext context,
      String fusionChartId, Class<UIColumn2D> cls)
      throws IOException {
    //得到页面中所有构件的Iterator,不包含UIViewRoot根节点
      Iterator<UIComponent> kids = FacesUtils.createChildrenIterator(context.getViewRoot(), false);
    //从页面中所有AjaxUpdater中找这个构件
    while (kids.hasNext()) {
      UIComponent kid = kids.next();
      if (kid instanceof AjaxUpdater) {
          AjaxUpdater updater=(AjaxUpdater) kid;
          //如果ajax:updater引入了一个其它页面,则开始查找
          if (updater.getSubviewId() != null) {
                  //生成AjaxUpdater引入的页面的视图树
                  AjaxUpdaterRenderer renderer = (AjaxUpdaterRenderer) FacesUtils.getRenderer(context, updater);
                  ViewHandler vh = context.getApplication().getViewHandler();
                  if (vh instanceof ViewBuilder) {
                      renderer.buildSubviewTree(context, (ViewBuilder) vh,updater);
                  }
                  //从生成的视图树中找到构件类为cls的构件
                  UIColumn2D fusionChart = FacesUtils.findComponent(updater, cls);
                  //如果找到的构件的id为fusionChartId,则返回这个构件
                  if (fusionChart != null&& fusionChartId.equals(fusionChart.getId())) {
                      return fusionChart;
                  }
              }
      }
    }
    return null;
  }

}

首先它是ViewDelegate接口的实现,所以需要实现ViewDelegate接口的public void delegate(FacesContext context) throws IOException方法。这个FusionChartDelegate的原理是从二次取数的URL中读取参数————构件id,找到这个构件并调用它的dataProvider的public Chart getChart(FacesContext context, UIComponent component)方法,得到要显示的数据对象,然后用jaxb将这个对象转换为xml并写入响应中,从而达到了二次取数的目的和使用内存xml文件的目的。

OperaMasks引擎怎么知道二次取数时要使用这个Delegate,而不使用其它Delegate?这个要通过配置文件配置一下,在META-INF中新建一个文件,文件名为view_delegate.cfg(必须用这个名字,否则OperaMasks引擎不会读取这个文件的内容),其内容为我们定义的那个Delegate的完整限定名:

org.ajavaer.fusionchart.component.FusionChartDelegate

至此,修改工作完成。我们可以将修改后的工程导出为“构件jar文件”。然后修改一下测试案例代码看一下它是否能正常工作。将页面Column2D.xhtml代码改为:

<?xml version="1.0" encoding="UTF-8"?>
<f:view xmlns:f="http://java.sun.com/jsf/core"
  xmlns="http://www.w3.org/1999/xhtml"
  xmlns:h="http://java.sun.com/jsf/html"
  xmlns:w="http://www.apusic.com/jsf/widget"
  xmlns:ajax="http://www.apusic.com/jsf/ajax"
  xmlns:layout="http://www.apusic.com/jsf/layout"
  xmlns:fc="http://org.ajavaer.fusionchart" renderKitId="AJAX">
  <w:page title="FusionCharts Demos">
    <fc:column2D id="chart1" width="500" height="400"
      dataProvider="#{Column2DBean.chartDataProvider}" />
  </w:page>
</f:view>

将LiteBean代码改为:

package org.operamasks;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;

import org.ajavaer.fusionchart.component.FusionChartDataProvider;
import org.ajavaer.fusionchart.modal.Chart;
import org.ajavaer.fusionchart.modal.ChartObjectFactory;
import org.ajavaer.fusionchart.modal.Dset;
import org.operamasks.faces.annotation.Accessible;
import org.operamasks.faces.annotation.ManagedBean;
import org.operamasks.faces.annotation.ManagedBeanScope;

@ManagedBean(name = "Column2DBean", scope = ManagedBeanScope.REQUEST)
public class Column2DBean {
  //下面这个属性为Column2D构件提供数据
  @Accessible
  private FusionChartDataProvider chartDataProvider = new FusionChartDataProvider() {
    public Chart getChart(FacesContext context, UIComponent component) {
      ChartObjectFactory fac = ChartObjectFactory.getInstance();
      // 可以用DSL风格创建对象
      Chart chart = fac.createChart().baseFont("宋体").baseFontSize(12).xAxisName("单位")
        .yAxisName("用电量(万度)").formatNumberScale(false);
      Dset dset = fac.createDset().name("物价局").value(322f).color("AFD8F8");
      chart.getSetOrVLine().add(dset);
      dset = fac.createDset().name("交通局").value(950f).color("CC3333");
      chart.getSetOrVLine().add(dset);
      // 也可以用swing风格创建对象
      dset = fac.createDset();
      dset.setName("市容局");
      dset.setValue(957f);
      dset.setColor("F6BD0F");
      chart.getSetOrVLine().add(dset);

      return chart;
    }
  };
}

这个代码中定义了一个FusionChartDataProvider类型的chartDataProvider属性。并在页面中用EL使用这个chartDataProvider属性。运行一下测试页面,效果如下:

28.3.2. FusionCharts Area2D构件

对于开发一个构件来说,上面的fc:column2D已经比较好了,也没有改进的空间。但是在另外一些情况下,也许还可能对它进行修改。本节我们要再开发另一个FusionChart构件————Area2D构件。

其HTML代码如下:

<html>  
    <head>  
        <script language="JavaScript" src="fusionCharts/FusionCharts.js"></script>  
    </head>    
    <body>  
        <div id="chartdiv"></div>  
        <script type="text/JavaScript">  
        var myChart = new FusionCharts("../FusionCharts/Area2D.swf", "myChartId", "600", "500",0 ,0, "FFFFFF","noscale","EN");   
        myChart.setDataURL("data.xml");   
        myChart.render("chartdiv");   
        </script>  
    </body>  
</html>

data.xml与column2D构件的一样。

我们发现,这个构件与fc:column2D构件几乎一样,唯一的区别是那个swf文件的url不同,column2D构件使用的是Column2D.swf,而这个构件使用的是Area2D.swf。

我们可以想象,这个构件的Base类会与fc:column2D构件的Base类一模一样,我们也使用dataProvider方式来为这个图表构件提供数据。而且渲染器类也基本与fc:column2D构件的渲染器一样,唯一的区别在于渲染器使用的swf不同。

那么我们可以对构件Base类进行改进、使用一个公共的构件Base类,这个公共Base类继承自UIComponentBase,然后fc:column2D和fc:area2D的Base类继承自这个公共的Base类生成的构件类(继承自公共构件类而不继承自公共Base类的作用是让一部分代码由OperaMasks Studio自动生成,减少开发的工作量)。

同样我们可以对组渲染器类进行修改,有2种办法:第一种办法,建一个公共渲染器类,然后添加抽象方法public String getSwfUrl(),fc:column2D和fc:area2D构件的渲染器类继承这个公共渲染器类并实现其中的抽象方法,然后正确的swf文件路径;另一种办法是所有的构件使用一个渲染器类,这种swf路径的差别在构件类中体现。第一种办法的优点是保持思路和结构清晰,而且swf的不同本来就是渲染器要决定的事,相比第二种办法交给构件类来决定要好些;第二种办法的优点是只不管多少个构件,只需要一个渲染器类就够了。

第一种办法还有一个优点是能较好地适应变化,如果某天要对fc:area2D构件进行改进,改为使用JFreeChart构件,改动比较方便,对一个构件的改动不会影响到其它构件。我们可以使用第一种办法来改进。

除此以外,fc:area2D构件还依赖于Area2D.swf这个资源文件,我们将它复制到与Column2D.swf相同的目录中。再看一下修改后的构件类和渲染器类。

公共Base类代码如下(注意:它继承自UIComponentBase类,而且dataProvider属性进行了特殊处理):

package org.ajavaer.fusionchart.component;

import javax.faces.component.UIComponentBase;

import org.operamasks.faces.component.GenComponentUtils;

public abstract class UIAbstractFusionChartBase extends UIComponentBase {

    protected String jsvar;
    protected int width = 500;// 默认宽度500
    protected int height = 400;// 默认高度400
    protected boolean debug = false;// 默认为false
    protected boolean registerWithJS = false;// 默认为false
    protected String bgcolor;
    protected String scaleMode;
    protected String lang;
    protected String style;
    protected String styleClass;
    private FusionChartDataProvider dataProvider;

    public FusionChartDataProvider getDataProvider() {
        if (this.dataProvider != null) {
            return this.dataProvider;
        }
        Object exprValue = GenComponentUtils.getValueFromExpression(this, "dataProvider");
        if (exprValue != null) {
            return (FusionChartDataProvider) exprValue;
        }
        return null;
    }

    public void setDataProvider(FusionChartDataProvider value) {
        this.dataProvider = value;
    }

}

fc:column2D的Base类代码如下:

package org.ajavaer.fusionchart.component;

import org.operamasks.faces.tools.annotation.ComponentMeta;

@ComponentMeta(tagName="column2D", family="org.ajavaer.fusionchart", rendererType="org.ajavaer.fusionchart.component.Column2D")
public abstract class UIColumn2DBase extends UIAbstractFusionChart {
  
}

fc:area2D的Base类代码如下:

package org.ajavaer.fusionchart.component;

import org.operamasks.faces.tools.annotation.ComponentMeta;

@ComponentMeta(tagName="area2D", family="org.ajavaer.fusionchart", rendererType="org.ajavaer.fusionchart.component.Area2D")
public abstract class UIArea2DBase extends UIAbstractFusionChart {
  
}

这3个Base类对应的构件类由Operamasks Studio自动生成,不需要修改,代码就不在这里给出了。

再来修改渲染器类,注意一定要将fc:area2D构件依赖的Area2D.swf放到META-INF中。将原来的渲染器代码放到公共渲染器类中,对于swf文件路径的处理使用抽象方法。修改后的代码如下:

package org.ajavaer.fusionchart.component;

import java.io.IOException;
import java.util.Formatter;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.servlet.http.HttpServletRequest;

import org.operamasks.faces.developer.util.FacesUtils;
import org.operamasks.faces.render.common.AjaxRendererBase;
import org.operamasks.faces.render.delegate.ViewDelegate;
import org.operamasks.faces.render.html.HtmlResponseWriter;
import org.operamasks.faces.render.resource.ComponentResource;
import org.operamasks.faces.render.resource.ResourceManager;
import org.operamasks.faces.util.HtmlEncoder;

public 1abstract class FusionChartAjaxRenderer extends AjaxRendererBase {
  
  public static final String DELEGATE_PARAM = "__fusionchart__";

  @Override
  public String[] getDependedJSPackages(FacesContext context, UIComponent component) {
      return new String[]{"AJavaer.FusionChart"};
  }
  
  @Override
  public void encodeHtmlBegin(FacesContext context, UIComponent component)
      throws IOException {
    UIAbstractFusionChart chartComp = (UIAbstractFusionChart) component;
    HtmlResponseWriter responseWriter = (HtmlResponseWriter) context.getResponseWriter();
    //得到构件的客户端id
    String clientId = chartComp.getClientId(context);
    responseWriter.startElement("div", chartComp);//渲染一个div
    responseWriter.writeAttribute("id", clientId, null);//为div添加id属性
    //将构件的style属性写入div的style属性中
    String style=chartComp.getStyle();
    if (style != null && style.trim().length() > 0) {
      responseWriter.writeAttribute("style", style, null);//为div添加style属性
    }
    //将构件的styleClass属性写入div的class属性中
    String styleClass=chartComp.getStyleClass();
    if (styleClass != null && styleClass.trim().length() > 0) {
      responseWriter.writeAttribute("class", styleClass, null);
    }
  }

    @Override
  public void encodeHtmlEnd(FacesContext context, UIComponent component)
      throws IOException {
        //输出</div>
    HtmlResponseWriter responseWriter = (HtmlResponseWriter) context.getResponseWriter();
    responseWriter.endElement("div");
  }

  @Override
  public void encodeInitScriptBegin(FacesContext context, ResourceManager rm,
      UIComponent component) throws IOException {
      //声明一个jsvar变量
    ComponentResource resource = ComponentResource.getResourceInstance(rm);
    resource.allocVariable(component);
  }

  @Override
  public void encodeResourceBegin(FacesContext context, ResourceManager rm,
      UIComponent component) throws IOException {
      UIAbstractFusionChart chartComp = (UIAbstractFusionChart) component;
      String jsvar = FacesUtils.getJsvar(context, chartComp);
    StringBuffer buf = new StringBuffer();
    Formatter fmt = new Formatter(buf);
    //根据构件的不同,渲染不同的swf文件名
    String swfUrl = rm.getResourceURL(2getSwfURL());
    
    String clientId = chartComp.getClientId(context);
    String width = String.valueOf(chartComp.getWidth());
    String height = String.valueOf(chartComp.getHeight());
    String debug = chartComp.isDebug() ? "1" : "0";
    String registerWithJS = chartComp.isRegisterWithJS() ? "1" : "0";
    String bgColor = chartComp.getBgcolor();
    String scaleMode = chartComp.getScaleMode();
    String lang = chartComp.getLang();
    fmt.format("%s=new FusionCharts(%s,%s,%s,%s,%s,%s,%s,%s,%s);\n", jsvar,
        HtmlEncoder.enquote(swfUrl), HtmlEncoder.enquote(chartComp.getId()),
        HtmlEncoder.enquote(width), HtmlEncoder.enquote(height),
        HtmlEncoder.enquote(debug),
        HtmlEncoder.enquote(registerWithJS), 
        bgColor == null ? "null": HtmlEncoder.enquote(bgColor),
        scaleMode == null ? "null" : HtmlEncoder.enquote(scaleMode),
        lang == null ? "null" : HtmlEncoder.enquote(lang));
    
    HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
    //这个dataUrl中包含ViewDelegate.VIEW_DELEGATE_REQUEST说明是二次取数
    //这个dataUrl中还传递了一个参数DELEGATE_PARAM,值为构件的id
    String dataUrl = request.getRequestURL() + "?"+DELEGATE_PARAM+"=" + chartComp.getId() + 
            "%26"+ ViewDelegate.VIEW_DELEGATE_REQUEST + "=%26" + FacesUtils.STREAM_RESPONSE + "="; 
    fmt.format("%s.setDataURL(%s);\n",jsvar, HtmlEncoder.enquote(dataUrl));
    fmt.format("%s.render(%s);\n",jsvar, HtmlEncoder.enquote(clientId));
    //将拼凑的js代码输出到响应中
    ComponentResource resource = ComponentResource.getResourceInstance(rm);
    resource.addInitScript(fmt.toString());
  }
  
  3protected abstract String getSwfURL();
}
1

这个类是抽象的,因为只有抽象类才能有抽象方法。

2

使用抽象方法得到不同的swf路径。

3

定义了一个抽象方法,由子类实现。

修改后fc:column2D构件的渲染器类Column2DAjaxRenderer的代码如下:

package org.ajavaer.fusionchart.component;

public class Column2DAjaxRenderer extends FusionChartAjaxRenderer {
    @Override
    protected String getSwfURL() {
        return "/fusionchart/Column2D.swf";
    }
}

同样,fc:area2D构件的渲染器类Area2DAjaxRenderer的代码如下:

package org.ajavaer.fusionchart.component;

public class Area2DAjaxRenderer extends FusionChartAjaxRenderer {
    @Override
    protected String getSwfURL() {
        return "/fusionchart/Area2D.swf";
    }
}

至此,构件开发工作完成。再来将这个工程打包成“构件jar文件”,放到测试工程的WEB-INF\lib中,然后测试一下这两个构件。

fc:column2D构件的测试页面及LiteBean代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<f:view xmlns:f="http://java.sun.com/jsf/core"
	xmlns="http://www.w3.org/1999/xhtml"
	xmlns:w="http://www.apusic.com/jsf/widget"
	xmlns:fc="http://org.ajavaer.fusionchart" renderKitId="AJAX">
	<w:page title="FusionCharts Demos">
		<fc:column2D id="chart1" width="500" height="400" 
			dataProvider="#{Column2DBean.chartDataProvider}" />
	</w:page>
</f:view>
package org.operamasks;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;

import org.ajavaer.fusionchart.component.FusionChartDataProvider;
import org.ajavaer.fusionchart.modal.Chart;
import org.ajavaer.fusionchart.modal.ChartObjectFactory;
import org.ajavaer.fusionchart.modal.Dset;
import org.operamasks.faces.annotation.Accessible;
import org.operamasks.faces.annotation.ManagedBean;
import org.operamasks.faces.annotation.ManagedBeanScope;

@ManagedBean(name = "Column2DBean", scope = ManagedBeanScope.REQUEST)
public class Column2DBean {
	//下面这个属性为Column2D构件提供数据
	@Accessible
	private FusionChartDataProvider chartDataProvider = new FusionChartDataProvider() {
		public Chart getChart(FacesContext context, UIComponent component) {
			ChartObjectFactory fac = ChartObjectFactory.getInstance();
			// 可以用DSL风格创建对象
			Chart chart = fac.createChart().baseFont("宋体").baseFontSize(12).xAxisName("单位")
				.yAxisName("用电量(万度)").formatNumberScale(false);
			Dset dset = fac.createDset().name("物价局").value(322f).color("AFD8F8");
			chart.getSetOrVLine().add(dset);
			dset = fac.createDset().name("交通局").value(950f).color("CC3333");
			chart.getSetOrVLine().add(dset);
			// 也可以用swing风格创建对象
			dset = fac.createDset();
			dset.setName("市容局");
			dset.setValue(957f);
			dset.setColor("F6BD0F");
			chart.getSetOrVLine().add(dset);

			return chart;
		}
	};
}

运行后效果如下:

fc:area2D构件的测试页面及LiteBean代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<f:view xmlns:f="http://java.sun.com/jsf/core"
	xmlns="http://www.w3.org/1999/xhtml"
	xmlns:w="http://www.apusic.com/jsf/widget"
	xmlns:fc="http://org.ajavaer.fusionchart" renderKitId="AJAX">
	<w:page title="FusionCharts Demos">
		<fc:area2D id="chart1" width="500" height="400" 
			dataProvider="#{Area2DBean.chartDataProvider}" />
	</w:page>
</f:view>
package org.operamasks;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;

import org.ajavaer.fusionchart.component.FusionChartDataProvider;
import org.ajavaer.fusionchart.modal.Chart;
import org.ajavaer.fusionchart.modal.ChartObjectFactory;
import org.ajavaer.fusionchart.modal.Dset;
import org.operamasks.faces.annotation.Accessible;
import org.operamasks.faces.annotation.ManagedBean;
import org.operamasks.faces.annotation.ManagedBeanScope;
@ManagedBean(name = "Area2DBean", scope = ManagedBeanScope.REQUEST)
public class Area2DBean {
  //下面这个属性为Column2D构件提供数据
    @Accessible
    private FusionChartDataProvider chartDataProvider = new FusionChartDataProvider() {
        public Chart getChart(FacesContext context, UIComponent component) {
            ChartObjectFactory fac = ChartObjectFactory.getInstance();
            // 可以用DSL风格创建对象
            Chart chart = fac.createChart().baseFont("宋体").baseFontSize(12).xAxisName("单位")
                .yAxisName("用电量(万度)").formatNumberScale(false);
            Dset dset = fac.createDset().name("物价局").value(322f).color("AFD8F8");
            chart.getSetOrVLine().add(dset);
            dset = fac.createDset().name("交通局").value(950f).color("CC3333");
            chart.getSetOrVLine().add(dset);
            // 也可以用swing风格创建对象
            dset = fac.createDset();
            dset.setName("市容局");
            dset.setValue(957f);
            dset.setColor("F6BD0F");
            chart.getSetOrVLine().add(dset);

            return chart;
        }
    };
}

运行后效果如下:

此示例最终的构件工程及测试案例可以到http://wiki.operamasks.org/pages/viewpage.action?pageId=9437386下载。